Merge branch 'main' of https://github.com/mahdahar/clqms-be
This commit is contained in:
commit
23681a2dbf
86
AGENTS.md
Normal file
86
AGENTS.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Valueset Data Scanning - Agent Instructions
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Database Tables:**
|
||||
- [`valuesetdef`](app/Models/ValueSet/ValueSetDefModel.php:8) - Category definitions (VSetID, VSName, VSDesc)
|
||||
- [`valueset`](app/Models/ValueSet/ValueSetModel.php:8) - Actual values (VID, VSetID, VValue, VDesc, VOrder)
|
||||
|
||||
## Agent Workflow for Valueset Queries
|
||||
|
||||
### Step 1: Identify the Request Type
|
||||
|
||||
**Category Search:**
|
||||
```
|
||||
User: "Show me all values in [CATEGORY_NAME]"
|
||||
Agent: Search valuesetdef by VSName → Get VSetID → Query values by VSetID
|
||||
```
|
||||
|
||||
**Reference Search:**
|
||||
```
|
||||
User: "Show me values for [TABLE.COLUMN]"
|
||||
Agent: Search valuesetdef by VSDesc → Get VSetID → Query values by VSetID
|
||||
```
|
||||
|
||||
**ID Search:**
|
||||
```
|
||||
User: "Show me values for VSetDefID [ID]"
|
||||
Agent: Directly query values by VSetID
|
||||
```
|
||||
|
||||
**Filtered Search:**
|
||||
```
|
||||
User: "Find values containing [TERM] in [CATEGORY]"
|
||||
Agent: Search valuesetdef by VSName → Get VSetID → Query values with LIKE filter
|
||||
```
|
||||
|
||||
### Step 2: Execute Query
|
||||
|
||||
**Model Methods:**
|
||||
- [`ValueSetDefModel::getValueSetDefs($param)`](app/Models/ValueSet/ValueSetDefModel.php:18) - Search categories
|
||||
- [`ValueSetModel::getValueSetByValueSetDef($VSetID)`](app/Models/ValueSet/ValueSetModel.php:52) - Get values by category
|
||||
- [`ValueSetModel::getValueSets($param, $page, $limit, $VSetID)`](app/Models/ValueSet/ValueSetModel.php:18) - Get values with filters
|
||||
|
||||
### Step 3: Return Results
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"VSetID": 27,
|
||||
"VSName": "Test Type",
|
||||
"values": [
|
||||
{ "VID": 1, "VValue": "TEST", "VDesc": "Test", "VOrder": 1 },
|
||||
{ "VID": 2, "VValue": "PARAM", "VDesc": "Parameter", "VOrder": 2 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Valuesets
|
||||
|
||||
| VSetDefID | VSName | VSDesc | Search Keywords |
|
||||
|-----------|--------|--------|-----------------|
|
||||
| 3 | Gender | - | gender, sex |
|
||||
| 27 | Test Type | `testdefsite.TestType` | test, type, testdefsite |
|
||||
| 15 | Specimen Type | - | specimen, type, blood, urine |
|
||||
| 31 | Range Types | `refnum.RangeType` | range, refnum |
|
||||
| 46 | Num Ref Type | `refnum.NumRefType` | numeric, reference |
|
||||
|
||||
## Example Agent Conversations
|
||||
|
||||
**User:** "Show me Gender values"
|
||||
**Agent:**
|
||||
1. `getValueSetDefs("Gender")` → VSetID = 3
|
||||
2. `getValueSetByValueSetDef(3)` → Returns values
|
||||
3. Output: Female, Male, Unknown
|
||||
|
||||
**User:** "What values for testdefsite.TestType?"
|
||||
**Agent:**
|
||||
1. `getValueSetDefs("testdefsite.TestType")` → VSetID = 27
|
||||
2. `getValueSetByValueSetDef(27)` → Returns values
|
||||
3. Output: TEST, PARAM, CALC, GROUP, TITLE
|
||||
|
||||
**User:** "Find values with 'STAT' in Priority"
|
||||
**Agent:**
|
||||
1. `getValueSetDefs("Priority")` → VSetID = 1
|
||||
2. `getValueSets("STAT", null, 50, 1)` → Returns matching values
|
||||
3. Output: STAT, STAT2 (if exists)
|
||||
205
README.md
205
README.md
@ -34,6 +34,7 @@ The system is currently undergoing a strategic **Architectural Redesign** to con
|
||||
| **Security** | JWT (JSON Web Tokens) Authorization |
|
||||
| **Database** | MySQL (Optimized Schema Migration in progress) |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📂 Documentation & Specifications
|
||||
@ -49,6 +50,210 @@ Key documents:
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Valueset Reference (VSetDefID)
|
||||
|
||||
When working on UI components or dropdowns, **always check for existing ValueSets** before hardcoding options. Use the API endpoint `/api/valueset/valuesetdef/{ID}` to fetch options dynamically.
|
||||
|
||||
| VSetDefID | Purpose | Usage |
|
||||
|-----------|---------|-------|
|
||||
| 27 | Test Types | TEST, PARAM, GROUP, CALC, TITLE |
|
||||
| 28 | Methods | Lab test methods |
|
||||
| 29 | Specimen Types | Blood, Urine, etc. |
|
||||
| 30 | Ref Types | NMRC (Numeric), TEXT, LIST |
|
||||
| 31 | Range Types | STD (Standard), AGSX (Age/Sex), COND |
|
||||
|
||||
> **Important:** Always use ValueSet lookups for configurable options. This ensures consistency and allows administrators to modify options without code changes.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Master Data Management
|
||||
|
||||
CLQMS provides comprehensive master data management for laboratory operations. All master data is accessible via the V2 UI at `/v2/master/*` endpoints.
|
||||
|
||||
### 🧪 Laboratory Tests (`/v2/master/tests`)
|
||||
|
||||
The Test Definitions module manages all laboratory test configurations including parameters, calculated tests, and test panels.
|
||||
|
||||
#### Test Types
|
||||
|
||||
| Type Code | Description | Table |
|
||||
|-----------|-------------|-------|
|
||||
| `TEST` | Individual laboratory test with technical specs | `testdefsite` + `testdeftech` |
|
||||
| `PARAM` | Parameter value (non-lab measurement) | `testdefsite` + `testdeftech` |
|
||||
| `CALC` | Calculated test with formula | `testdefsite` + `testdefcal` |
|
||||
| `GROUP` | Panel/profile containing multiple tests | `testdefsite` + `testdefgrp` |
|
||||
| `TITLE` | Section title for report organization | `testdefsite` |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/tests` | List all tests with optional filtering |
|
||||
| `GET` | `/api/tests/{id}` | Get test details with type-specific data |
|
||||
| `POST` | `/api/tests` | Create new test definition |
|
||||
| `PATCH` | `/api/tests` | Update existing test |
|
||||
| `DELETE` | `/api/tests` | Soft delete test (sets EndDate) |
|
||||
|
||||
#### Filtering Parameters
|
||||
|
||||
- `TestSiteName` - Search by test name (partial match)
|
||||
- `TestType` - Filter by test type VID (1-5)
|
||||
- `VisibleScr` - Filter by screen visibility (0/1)
|
||||
- `VisibleRpt` - Filter by report visibility (0/1)
|
||||
|
||||
#### Test Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Data fetched successfully",
|
||||
"data": [
|
||||
{
|
||||
"TestSiteID": 1,
|
||||
"TestSiteCode": "CBC",
|
||||
"TestSiteName": "Complete Blood Count",
|
||||
"TestType": 4,
|
||||
"TypeCode": "GROUP",
|
||||
"TypeName": "Group Test",
|
||||
"SeqScr": 50,
|
||||
"VisibleScr": 1,
|
||||
"VisibleRpt": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 📏 Reference Ranges (`/v2/master/refrange`)
|
||||
|
||||
Reference Ranges define normal and critical values for test results. The system supports multiple reference range types based on patient demographics.
|
||||
|
||||
#### Reference Range Types
|
||||
|
||||
| Type | Table | Description |
|
||||
|------|-------|-------------|
|
||||
| Numeric | `refnum` | Numeric ranges with age/sex criteria |
|
||||
| Threshold | `refthold` | Critical threshold values |
|
||||
| Text | `reftxt` | Text-based reference values |
|
||||
| Value Set | `refvset` | Coded reference values |
|
||||
|
||||
#### Numeric Reference Range Structure
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `NumRefType` | Type: REF (Reference), CRTC (Critical), VAL (Validation), RERUN |
|
||||
| `RangeType` | RANGE or THOLD |
|
||||
| `Sex` | Gender filter (0=All, 1=Female, 2=Male) |
|
||||
| `AgeStart` | Minimum age (years) |
|
||||
| `AgeEnd` | Maximum age (years) |
|
||||
| `LowSign` | Low boundary sign (=, <, <=) |
|
||||
| `Low` | Low boundary value |
|
||||
| `HighSign` | High boundary sign (=, >, >=) |
|
||||
| `High` | High boundary value |
|
||||
| `Flag` | Result flag (H, L, A, etc.) |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/refnum` | List numeric reference ranges |
|
||||
| `GET` | `/api/refnum/{id}` | Get reference range details |
|
||||
| `POST` | `/api/refnum` | Create reference range |
|
||||
| `PATCH` | `/api/refnum` | Update reference range |
|
||||
| `DELETE` | `/api/refnum` | Soft delete reference range |
|
||||
|
||||
### 📑 Value Sets (`/v2/master/valuesets`)
|
||||
|
||||
Value Sets are configurable dropdown options used throughout the system. Each Value Set Definition (VSetDef) contains multiple Value Set Values (ValueSet).
|
||||
|
||||
#### Value Set Hierarchy
|
||||
|
||||
```
|
||||
valuesetdef (VSetDefID, VSName, VSDesc)
|
||||
└── valueset (VID, VSetID, VValue, VDesc, VOrder, VCategory)
|
||||
```
|
||||
|
||||
#### Common Value Sets
|
||||
|
||||
| VSetDefID | Name | Example Values |
|
||||
|-----------|------|----------------|
|
||||
| 1 | Priority | STAT (S), ASAP (A), Routine (R), Preop (P) |
|
||||
| 2 | Enable/Disable | Disabled (0), Enabled (1) |
|
||||
| 3 | Gender | Female (1), Male (2), Unknown (3) |
|
||||
| 10 | Order Status | STC, SCtd, SArrv, SRcvd, SAna, etc. |
|
||||
| 15 | Specimen Type | BLD, SER, PLAS, UR, CSF, etc. |
|
||||
| 16 | Unit | L, mL, g/dL, mg/dL, etc. |
|
||||
| 27 | Test Type | TEST, PARAM, CALC, GROUP, TITLE |
|
||||
| 28 | Result Unit | g/dL, g/L, mg/dL, x10^6/mL, etc. |
|
||||
| 35 | Test Activity | Order, Analyse, VER, REV, REP |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/valuesetdef` | List all value set definitions |
|
||||
| `GET` | `/api/valuesetdef/{id}` | Get valueset with all values |
|
||||
| `GET` | `/api/valuesetdef/{id}/values` | Get values for specific valueset |
|
||||
| `POST` | `/api/valuesetdef` | Create new valueset definition |
|
||||
| `PATCH` | `/api/valuesetdef` | Update valueset definition |
|
||||
| `DELETE` | `/api/valuesetdef` | Delete valueset definition |
|
||||
|
||||
#### Value Set Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"VSetDefID": 27,
|
||||
"VSName": "Test Type",
|
||||
"VSDesc": "testdefsite.TestType",
|
||||
"values": [
|
||||
{ "VID": 1, "VValue": "TEST", "VDesc": "Test", "VOrder": 1 },
|
||||
{ "VID": 2, "VValue": "PARAM", "VDesc": "Parameter", "VOrder": 2 },
|
||||
{ "VID": 3, "VValue": "CALC", "VDesc": "Calculated Test", "VOrder": 3 },
|
||||
{ "VID": 4, "VValue": "GROUP", "VDesc": "Group Test", "VOrder": 4 },
|
||||
{ "VID": 5, "VValue": "TITLE", "VDesc": "Title", "VOrder": 5 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 📊 Database Tables Summary
|
||||
|
||||
| Category | Tables | Purpose |
|
||||
|----------|--------|---------|
|
||||
| Tests | `testdefsite`, `testdeftech`, `testdefcal`, `testdefgrp`, `testmap` | Test definitions |
|
||||
| Reference Ranges | `refnum`, `refthold`, `reftxt`, `refvset` | Result validation |
|
||||
| Value Sets | `valuesetdef`, `valueset` | Configurable options |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Edge API - Instrument Integration
|
||||
|
||||
The **Edge API** provides endpoints for integrating laboratory instruments via the `tiny-edge` middleware. Results from instruments are staged in the `edgeres` table before processing into the main patient results (`patres`).
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/edge/results` | Receive instrument results (stored in `edgeres`) |
|
||||
| `GET` | `/api/edge/orders` | Fetch pending orders for an instrument |
|
||||
| `POST` | `/api/edge/orders/:id/ack` | Acknowledge order delivery to instrument |
|
||||
| `POST` | `/api/edge/status` | Log instrument status updates |
|
||||
|
||||
### Workflow
|
||||
|
||||
```
|
||||
Instrument → tiny-edge → POST /api/edge/results → edgeres table → [Manual/Auto Processing] → patres table
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- **Staging Table:** All results land in `edgeres` first for validation
|
||||
- **Rerun Handling:** Duplicate `SampleID` + `TestSiteCode` increments `AspCnt` in `patres`
|
||||
- **Configurable Processing:** Auto or manual processing based on settings
|
||||
- **Status Tracking:** Full audit trail via `edgestatus` and `edgeack` tables
|
||||
|
||||
---
|
||||
|
||||
### 📜 Usage Notice
|
||||
This repository contains proprietary information intended for the 5Panda Team and authorized collaborators.
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ class App extends BaseConfig
|
||||
*
|
||||
* E.g., http://example.com/
|
||||
*/
|
||||
public string $baseURL = 'http://localhost:8080/';
|
||||
public string $baseURL = '';
|
||||
|
||||
/**
|
||||
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
|
||||
@ -40,7 +40,8 @@ class App extends BaseConfig
|
||||
* something else. If you have configured your web server to remove this file
|
||||
* from your site URIs, set this variable to an empty string.
|
||||
*/
|
||||
public string $indexPage = 'index.php';
|
||||
#public string $indexPage = 'index.php';
|
||||
public string $indexPage = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
@ -44,7 +44,7 @@ class Exceptions extends BaseConfig
|
||||
*
|
||||
* Default: APPPATH.'Views/errors'
|
||||
*/
|
||||
public string $errorViewPath = APPPATH . 'Views/errors';
|
||||
public string $errorViewPath = __DIR__ . '/../Views/errors';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
@ -53,7 +53,7 @@ class Filters extends BaseFilters
|
||||
*/
|
||||
public array $required = [
|
||||
'before' => [
|
||||
'forcehttps', // Force Global Secure Requests
|
||||
// 'forcehttps', // Force Global Secure Requests - disabled for localhost
|
||||
'pagecache', // Web Page Caching
|
||||
],
|
||||
'after' => [
|
||||
|
||||
@ -5,171 +5,287 @@ use CodeIgniter\Router\RouteCollection;
|
||||
/**
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->options('(:any)', function() { return ''; });
|
||||
$routes->get('/', 'Home::index');
|
||||
$routes->get('/', function () {
|
||||
return redirect()->to('/v2');
|
||||
});
|
||||
|
||||
// Frontend Pages
|
||||
$routes->get('/login', 'Pages\AuthPage::login');
|
||||
$routes->get('/logout', 'Pages\AuthPage::logout');
|
||||
$routes->get('/dashboard', 'Pages\DashboardPage::index');
|
||||
$routes->options('(:any)', function () {
|
||||
return '';
|
||||
});
|
||||
|
||||
$routes->group('api', ['filter' => 'auth'], function ($routes) {
|
||||
$routes->get('dashboard', 'DashboardController::index');
|
||||
$routes->get('result', 'ResultController::index');
|
||||
$routes->get('sample', 'SampleController::index');
|
||||
});
|
||||
|
||||
// Public Routes (no auth required)
|
||||
$routes->get('/v2/login', 'PagesController::login');
|
||||
|
||||
// V2 Auth API Routes (public - no auth required)
|
||||
$routes->group('v2/auth', function ($routes) {
|
||||
$routes->post('login', 'AuthV2Controller::login');
|
||||
$routes->post('register', 'AuthV2Controller::register');
|
||||
$routes->get('check', 'AuthV2Controller::checkAuth');
|
||||
$routes->post('logout', 'AuthV2Controller::logout');
|
||||
});
|
||||
|
||||
// Protected Page Routes - V2 (requires auth)
|
||||
$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('master/valuesets', 'PagesController::masterValueSets');
|
||||
});
|
||||
|
||||
// Faker
|
||||
$routes->get('faker/faker-patient/(:num)', 'faker\FakerPatient::sendMany/$1');
|
||||
|
||||
$routes->group('api', ['filter' => 'auth'], function($routes) {
|
||||
$routes->get('dashboard', 'Dashboard::index');
|
||||
$routes->get('result', 'Result::index');
|
||||
$routes->get('sample', 'Sample::index');
|
||||
$routes->group('api', function ($routes) {
|
||||
// Auth
|
||||
$routes->group('auth', function ($routes) {
|
||||
$routes->post('login', 'AuthController::login');
|
||||
$routes->post('change_pass', 'AuthController::change_pass');
|
||||
$routes->post('register', 'AuthController::register');
|
||||
$routes->get('check', 'AuthController::checkAuth');
|
||||
$routes->post('logout', 'AuthController::logout');
|
||||
});
|
||||
|
||||
// Patient
|
||||
$routes->group('patient', function ($routes) {
|
||||
$routes->get('/', 'Patient\PatientController::index');
|
||||
$routes->post('/', 'Patient\PatientController::create');
|
||||
$routes->get('(:num)', 'Patient\PatientController::show/$1');
|
||||
$routes->delete('/', 'Patient\PatientController::delete');
|
||||
$routes->patch('/', 'Patient\PatientController::update');
|
||||
$routes->get('check', 'Patient\PatientController::patientCheck');
|
||||
});
|
||||
|
||||
// PatVisit
|
||||
$routes->group('patvisit', function ($routes) {
|
||||
$routes->get('/', 'PatVisitController::index');
|
||||
$routes->post('/', 'PatVisitController::create');
|
||||
$routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1');
|
||||
$routes->get('(:any)', 'PatVisitController::show/$1');
|
||||
$routes->delete('/', 'PatVisitController::delete');
|
||||
$routes->patch('/', 'PatVisitController::update');
|
||||
});
|
||||
|
||||
$routes->group('patvisitadt', function ($routes) {
|
||||
$routes->post('/', 'PatVisitController::createADT');
|
||||
$routes->patch('/', 'PatVisitController::updateADT');
|
||||
});
|
||||
|
||||
// Master Data
|
||||
$routes->group('race', function ($routes) {
|
||||
$routes->get('/', 'Race::index');
|
||||
$routes->get('(:num)', 'Race::show/$1');
|
||||
});
|
||||
|
||||
$routes->group('country', function ($routes) {
|
||||
$routes->get('/', 'Country::index');
|
||||
$routes->get('(:num)', 'Country::show/$1');
|
||||
});
|
||||
|
||||
$routes->group('religion', function ($routes) {
|
||||
$routes->get('/', 'Religion::index');
|
||||
$routes->get('(:num)', 'Religion::show/$1');
|
||||
});
|
||||
|
||||
$routes->group('ethnic', function ($routes) {
|
||||
$routes->get('/', 'Ethnic::index');
|
||||
$routes->get('(:num)', 'Ethnic::show/$1');
|
||||
});
|
||||
|
||||
// Location
|
||||
$routes->group('location', function ($routes) {
|
||||
$routes->get('/', 'LocationController::index');
|
||||
$routes->get('(:num)', 'LocationController::show/$1');
|
||||
$routes->post('/', 'LocationController::create');
|
||||
$routes->patch('/', 'LocationController::update');
|
||||
$routes->delete('/', 'LocationController::delete');
|
||||
});
|
||||
|
||||
// Contact
|
||||
$routes->group('contact', function ($routes) {
|
||||
$routes->get('/', 'Contact\ContactController::index');
|
||||
$routes->get('(:num)', 'Contact\ContactController::show/$1');
|
||||
$routes->post('/', 'Contact\ContactController::create');
|
||||
$routes->patch('/', 'Contact\ContactController::update');
|
||||
$routes->delete('/', 'Contact\ContactController::delete');
|
||||
});
|
||||
|
||||
$routes->group('occupation', function ($routes) {
|
||||
$routes->get('/', 'Contact\OccupationController::index');
|
||||
$routes->get('(:num)', 'Contact\OccupationController::show/$1');
|
||||
$routes->post('/', 'Contact\OccupationController::create');
|
||||
$routes->patch('/', 'Contact\OccupationController::update');
|
||||
//$routes->delete('/', 'Contact\OccupationController::delete');
|
||||
});
|
||||
|
||||
$routes->group('medicalspecialty', function ($routes) {
|
||||
$routes->get('/', 'Contact\MedicalSpecialtyController::index');
|
||||
$routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1');
|
||||
$routes->post('/', 'Contact\MedicalSpecialtyController::create');
|
||||
$routes->patch('/', 'Contact\MedicalSpecialtyController::update');
|
||||
});
|
||||
|
||||
// ValueSet
|
||||
$routes->group('valueset', function ($routes) {
|
||||
$routes->get('/', 'ValueSet\ValueSetController::index');
|
||||
$routes->get('(:num)', 'ValueSet\ValueSetController::show/$1');
|
||||
$routes->get('valuesetdef/(:num)', 'ValueSet\ValueSetController::showByValueSetDef/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSetController::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSetController::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSetController::delete');
|
||||
});
|
||||
|
||||
$routes->group('valuesetdef', function ($routes) {
|
||||
$routes->get('/', 'ValueSet\ValueSetDefController::index');
|
||||
$routes->get('(:segment)', 'ValueSet\ValueSetDefController::show/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSetDefController::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSetDefController::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSetDefController::delete');
|
||||
});
|
||||
|
||||
// Counter
|
||||
$routes->group('counter', function ($routes) {
|
||||
$routes->get('/', 'CounterController::index');
|
||||
$routes->get('(:num)', 'CounterController::show/$1');
|
||||
$routes->post('/', 'CounterController::create');
|
||||
$routes->patch('/', 'CounterController::update');
|
||||
$routes->delete('/', 'CounterController::delete');
|
||||
});
|
||||
|
||||
// AreaGeo
|
||||
$routes->group('areageo', function ($routes) {
|
||||
$routes->get('/', 'AreaGeoController::index');
|
||||
$routes->get('provinces', 'AreaGeoController::getProvinces');
|
||||
$routes->get('cities', 'AreaGeoController::getCities');
|
||||
});
|
||||
|
||||
// Organization
|
||||
$routes->group('organization', function ($routes) {
|
||||
// Account
|
||||
$routes->group('account', function ($routes) {
|
||||
$routes->get('/', 'Organization\AccountController::index');
|
||||
$routes->get('(:num)', 'Organization\AccountController::show/$1');
|
||||
$routes->post('/', 'Organization\AccountController::create');
|
||||
$routes->patch('/', 'Organization\AccountController::update');
|
||||
$routes->delete('/', 'Organization\AccountController::delete');
|
||||
});
|
||||
|
||||
// Site
|
||||
$routes->group('site', function ($routes) {
|
||||
$routes->get('/', 'Organization\SiteController::index');
|
||||
$routes->get('(:num)', 'Organization\SiteController::show/$1');
|
||||
$routes->post('/', 'Organization\SiteController::create');
|
||||
$routes->patch('/', 'Organization\SiteController::update');
|
||||
$routes->delete('/', 'Organization\SiteController::delete');
|
||||
});
|
||||
|
||||
// Discipline
|
||||
$routes->group('discipline', function ($routes) {
|
||||
$routes->get('/', 'Organization\DisciplineController::index');
|
||||
$routes->get('(:num)', 'Organization\DisciplineController::show/$1');
|
||||
$routes->post('/', 'Organization\DisciplineController::create');
|
||||
$routes->patch('/', 'Organization\DisciplineController::update');
|
||||
$routes->delete('/', 'Organization\DisciplineController::delete');
|
||||
});
|
||||
|
||||
// Department
|
||||
$routes->group('department', function ($routes) {
|
||||
$routes->get('/', 'Organization\DepartmentController::index');
|
||||
$routes->get('(:num)', 'Organization\DepartmentController::show/$1');
|
||||
$routes->post('/', 'Organization\DepartmentController::create');
|
||||
$routes->patch('/', 'Organization\DepartmentController::update');
|
||||
$routes->delete('/', 'Organization\DepartmentController::delete');
|
||||
});
|
||||
|
||||
// Workstation
|
||||
$routes->group('workstation', function ($routes) {
|
||||
$routes->get('/', 'Organization\WorkstationController::index');
|
||||
$routes->get('(:num)', 'Organization\WorkstationController::show/$1');
|
||||
$routes->post('/', 'Organization\WorkstationController::create');
|
||||
$routes->patch('/', 'Organization\WorkstationController::update');
|
||||
$routes->delete('/', 'Organization\WorkstationController::delete');
|
||||
});
|
||||
});
|
||||
|
||||
// Specimen
|
||||
$routes->group('specimen', function ($routes) {
|
||||
$routes->group('containerdef', function ($routes) {
|
||||
$routes->get('/', 'Specimen\ContainerDefController::index');
|
||||
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
|
||||
$routes->post('/', 'Specimen\ContainerDefController::create');
|
||||
$routes->patch('/', 'Specimen\ContainerDefController::update');
|
||||
});
|
||||
|
||||
$routes->group('prep', function ($routes) {
|
||||
$routes->get('/', 'Specimen\SpecimenPrepController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenPrepController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenPrepController::update');
|
||||
});
|
||||
|
||||
$routes->group('status', function ($routes) {
|
||||
$routes->get('/', 'Specimen\SpecimenStatusController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenStatusController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenStatusController::update');
|
||||
});
|
||||
|
||||
$routes->group('collection', function ($routes) {
|
||||
$routes->get('/', 'Specimen\SpecimenCollectionController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenCollectionController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenCollectionController::update');
|
||||
});
|
||||
|
||||
$routes->get('/', 'Specimen\SpecimenController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenController::update');
|
||||
});
|
||||
|
||||
// Tests
|
||||
$routes->group('tests', function ($routes) {
|
||||
$routes->get('/', 'TestsController::index');
|
||||
$routes->get('(:num)', 'TestsController::show/$1');
|
||||
$routes->post('/', 'TestsController::create');
|
||||
$routes->patch('/', 'TestsController::update');
|
||||
});
|
||||
|
||||
// Edge API - Integration with tiny-edge
|
||||
$routes->group('edge', function ($routes) {
|
||||
$routes->post('results', 'EdgeController::results');
|
||||
$routes->get('orders', 'EdgeController::orders');
|
||||
$routes->post('orders/(:num)/ack', 'EdgeController::ack/$1');
|
||||
$routes->post('status', 'EdgeController::status');
|
||||
});
|
||||
});
|
||||
|
||||
$routes->post('/api/auth/login', 'Auth::login');
|
||||
$routes->post('/api/auth/change_pass', 'Auth::change_pass');
|
||||
$routes->post('/api/auth/register', 'Auth::register');
|
||||
$routes->get('/api/auth/check', 'Auth::checkAuth');
|
||||
$routes->post('/api/auth/logout', 'Auth::logout');
|
||||
|
||||
$routes->get('/api/patient', 'Patient\Patient::index');
|
||||
$routes->post('/api/patient', 'Patient\Patient::create');
|
||||
$routes->get('/api/patient/(:num)', 'Patient\Patient::show/$1');
|
||||
$routes->delete('/api/patient', 'Patient\Patient::delete');
|
||||
$routes->patch('/api/patient', 'Patient\Patient::update');
|
||||
$routes->get('/api/patient/check', 'Patient\Patient::patientCheck');
|
||||
|
||||
$routes->get('/api/patvisit', 'PatVisit::index');
|
||||
$routes->post('/api/patvisit', 'PatVisit::create');
|
||||
$routes->get('/api/patvisit/patient/(:num)', 'PatVisit::showByPatient/$1');
|
||||
$routes->get('/api/patvisit/(:any)', 'PatVisit::show/$1');
|
||||
$routes->delete('/api/patvisit', 'PatVisit::delete');
|
||||
$routes->patch('/api/patvisit', 'PatVisit::update');
|
||||
$routes->post('/api/patvisitadt', 'PatVisit::createADT');
|
||||
$routes->patch('/api/patvisitadt', 'PatVisit::updateADT');
|
||||
|
||||
$routes->get('/api/race', 'Race::index');
|
||||
$routes->get('/api/race/(:num)', 'Race::show/$1');
|
||||
|
||||
$routes->get('/api/country', 'Country::index');
|
||||
$routes->get('/api/country/(:num)', 'Country::show/$1');
|
||||
|
||||
$routes->get('/api/religion', 'Religion::index');
|
||||
$routes->get('/api/religion/(:num)', 'Religion::show/$1');
|
||||
|
||||
$routes->get('/api/ethnic', 'Ethnic::index');
|
||||
$routes->get('/api/ethnic/(:num)', 'Ethnic::show/$1');
|
||||
|
||||
$routes->get('/api/location', 'Location::index');
|
||||
$routes->get('/api/location/(:num)', 'Location::show/$1');
|
||||
$routes->post('/api/location', 'Location::create');
|
||||
$routes->patch('/api/location', 'Location::update');
|
||||
$routes->delete('/api/location', 'Location::delete');
|
||||
|
||||
$routes->get('/api/contact', 'Contact\Contact::index');
|
||||
$routes->get('/api/contact/(:num)', 'Contact\Contact::show/$1');
|
||||
$routes->post('/api/contact', 'Contact\Contact::create');
|
||||
$routes->patch('/api/contact', 'Contact\Contact::update');
|
||||
$routes->delete('/api/contact', 'Contact\git Contact::delete');
|
||||
|
||||
$routes->get('/api/occupation', 'Contact\Occupation::index');
|
||||
$routes->get('/api/occupation/(:num)', 'Contact\Occupation::show/$1');
|
||||
$routes->post('/api/occupation', 'Contact\Occupation::create');
|
||||
$routes->patch('/api/occupation', 'Contact\Occupation::update');
|
||||
//$routes->delete('/api/occupation', 'Contact\Occupation::delete');
|
||||
|
||||
$routes->get('/api/medicalspecialty', 'Contact\MedicalSpecialty::index');
|
||||
$routes->get('/api/medicalspecialty/(:num)', 'Contact\MedicalSpecialty::show/$1');
|
||||
$routes->post('/api/medicalspecialty', 'Contact\MedicalSpecialty::create');
|
||||
$routes->patch('/api/medicalspecialty', 'Contact\MedicalSpecialty::update');
|
||||
|
||||
$routes->get('/api/valueset', 'ValueSet\ValueSet::index');
|
||||
$routes->get('/api/valueset/(:num)', 'ValueSet\ValueSet::show/$1');
|
||||
$routes->get('/api/valueset/valuesetdef/(:num)', 'ValueSet\ValueSet::showByValueSetDef/$1');
|
||||
$routes->post('/api/valueset', 'ValueSet\ValueSet::create');
|
||||
$routes->patch('/api/valueset', 'ValueSet\ValueSet::update');
|
||||
$routes->delete('/api/valueset', 'ValueSet\ValueSet::delete');
|
||||
|
||||
$routes->get('/api/valuesetdef/', 'ValueSet\ValueSetDef::index');
|
||||
$routes->get('/api/valuesetdef/(:num)', 'ValueSet\ValueSetDef::show/$1');
|
||||
$routes->post('/api/valuesetdef', 'ValueSet\ValueSetDef::create');
|
||||
$routes->patch('/api/valuesetdef', 'ValueSet\ValueSetDef::update');
|
||||
$routes->delete('/api/valuesetdef', 'ValueSet\ValueSetDef::delete');
|
||||
|
||||
$routes->get('/api/counter/', 'Counter::index');
|
||||
$routes->get('/api/counter/(:num)', 'Counter::show/$1');
|
||||
$routes->post('/api/counter', 'Counter::create');
|
||||
$routes->patch('/api/counter', 'Counter::update');
|
||||
$routes->delete('/api/counter', 'Counter::delete');
|
||||
|
||||
$routes->get('/api/areageo', 'AreaGeo::index');
|
||||
$routes->get('/api/areageo/provinces', 'AreaGeo::getProvinces');
|
||||
$routes->get('/api/areageo/cities', 'AreaGeo::getCities');
|
||||
|
||||
//organization
|
||||
// account
|
||||
$routes->get('/api/organization/account/', 'Organization\Account::index');
|
||||
$routes->get('/api/organization/account/(:num)', 'Organization\Account::show/$1');
|
||||
$routes->post('/api/organization/account', 'Organization\Account::create');
|
||||
$routes->patch('/api/organization/account', 'Organization\Account::update');
|
||||
$routes->delete('/api/organization/account', 'Organization\Account::delete');
|
||||
// site
|
||||
$routes->get('/api/organization/site/', 'Organization\Site::index');
|
||||
$routes->get('/api/organization/site/(:num)', 'Organization\Site::show/$1');
|
||||
$routes->post('/api/organization/site', 'Organization\Site::create');
|
||||
$routes->patch('/api/organization/site', 'Organization\Site::update');
|
||||
$routes->delete('/api/organization/site', 'Organization\Site::delete');
|
||||
// discipline
|
||||
$routes->get('/api/organization/discipline/', 'Organization\Discipline::index');
|
||||
$routes->get('/api/organization/discipline/(:num)', 'Organization\Discipline::show/$1');
|
||||
$routes->post('/api/organization/discipline', 'Organization\Discipline::create');
|
||||
$routes->patch('/api/organization/discipline', 'Organization\Discipline::update');
|
||||
$routes->delete('/api/organization/discipline', 'Organization\Discipline::delete');
|
||||
// department
|
||||
$routes->get('/api/organization/department/', 'Organization\Department::index');
|
||||
$routes->get('/api/organization/department/(:num)', 'Organization\Department::show/$1');
|
||||
$routes->post('/api/organization/department', 'Organization\Department::create');
|
||||
$routes->patch('/api/organization/department', 'Organization\Department::update');
|
||||
$routes->delete('/api/organization/department', 'Organization\Department::delete');
|
||||
// workstation
|
||||
$routes->get('/api/organization/workstation/', 'Organization\Workstation::index');
|
||||
$routes->get('/api/organization/workstation/(:num)', 'Organization\Workstation::show/$1');
|
||||
$routes->post('/api/organization/workstation', 'Organization\Workstation::create');
|
||||
$routes->patch('/api/organization/workstation', 'Organization\Workstation::update');
|
||||
$routes->delete('/api/organization/workstation', 'Organization\Workstation::delete');
|
||||
|
||||
$routes->group('api/specimen', function($routes) {
|
||||
$routes->get('containerdef/(:num)', 'Specimen\ContainerDef::show/$1');
|
||||
$routes->post('containerdef', 'Specimen\ContainerDef::create');
|
||||
$routes->patch('containerdef', 'Specimen\ContainerDef::update');
|
||||
$routes->get('containerdef', 'Specimen\ContainerDef::index');
|
||||
|
||||
$routes->get('prep/(:num)', 'Specimen\Prep::show/$1');
|
||||
$routes->post('prep', 'Specimen\Prep::create');
|
||||
$routes->patch('prep', 'Specimen\Prep::update');
|
||||
$routes->get('prep', 'Specimen\Prep::index');
|
||||
|
||||
$routes->get('status/(:num)', 'Specimen\Status::show/$1');
|
||||
$routes->post('status', 'Specimen\Status::create');
|
||||
$routes->patch('status', 'Specimen\Status::update');
|
||||
$routes->get('status', 'Specimen\Status::index');
|
||||
|
||||
$routes->get('collection/(:num)', 'Specimen\Collection::show/$1');
|
||||
$routes->post('collection', 'Specimen\Collection::create');
|
||||
$routes->patch('collection', 'Specimen\Collection::update');
|
||||
$routes->get('collection', 'Specimen\Collection::index');
|
||||
|
||||
$routes->get('(:num)', 'Specimen\Specimen::show/$1');
|
||||
$routes->post('', 'Specimen\Specimen::create');
|
||||
$routes->patch('', 'Specimen\Specimen::update');
|
||||
$routes->get('', 'Specimen\Specimen::index');
|
||||
});
|
||||
|
||||
$routes->post('/api/tests', 'Tests::create');
|
||||
$routes->patch('/api/tests', 'Tests::update');
|
||||
$routes->get('/api/tests/(:any)', 'Tests::show/$1');
|
||||
$routes->get('/api/tests', 'Tests::index');
|
||||
|
||||
// Khusus
|
||||
/*
|
||||
$routes->get('/api/zones', 'Zones::index');
|
||||
$routes->get('/api/zones/synchronize', 'Zones::synchronize');
|
||||
$routes->get('/api/zones/provinces', 'Zones::getProvinces');
|
||||
$routes->get('/api/zones/cities', 'Zones::getCities');
|
||||
*/
|
||||
*/
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\AreaGeoModel;
|
||||
|
||||
class AreaGeo extends BaseController {
|
||||
class AreaGeoController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -44,4 +44,4 @@ class AreaGeo extends BaseController {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -12,23 +12,26 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Auth extends Controller {
|
||||
class AuthController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
// ok
|
||||
public function __construct() {
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
// ok
|
||||
public function checkAuth() {
|
||||
public function checkAuth()
|
||||
{
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Jika token FE tidak ada langsung kabarkan failed
|
||||
if (!$token) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'No token found'
|
||||
], 401);
|
||||
}
|
||||
@ -38,43 +41,101 @@ class Auth extends Controller {
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
|
||||
} catch (ExpiredException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Token expired',
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
|
||||
} catch (SignatureInvalidException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid token signature',
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
|
||||
} catch (BeforeValidException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Token not valid yet',
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid token: ' . $e->getMessage(),
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
|
||||
// ok
|
||||
public function login() {
|
||||
// public function login() {
|
||||
|
||||
// // Ambil dari JSON Form dan Key .env
|
||||
// $username = $this->request->getVar('username');
|
||||
// $password = $this->request->getVar('password');
|
||||
// $key = getenv('JWT_SECRET');
|
||||
|
||||
// if (!$username) {
|
||||
// return $this->fail('Username required.', 400);
|
||||
// }
|
||||
|
||||
// $sql = "SELECT * FROM users WHERE username=" . $this->db->escape($username);
|
||||
// $query = $this->db->query($sql);
|
||||
// $row = $query->getResultArray();
|
||||
|
||||
// if (!$row) { return $this->fail('User not found.', 401); }
|
||||
// $row = $row[0];
|
||||
// if (!password_verify($password, $row['password'])) {
|
||||
// return $this->fail('Invalid password.', 401);
|
||||
// }
|
||||
|
||||
// // Buat JWT payload
|
||||
// $exp = time() + 864000;
|
||||
// $payload = [
|
||||
// 'userid' => $row['id'],
|
||||
// 'roleid' => $row['role_id'],
|
||||
// 'username' => $row['username'],
|
||||
// 'exp' => $exp
|
||||
// ];
|
||||
|
||||
// try {
|
||||
// // Melakukan Hash terhadap Payload dengan Kunci .env menggunakan Algortima HMAC + SHA-256
|
||||
// $jwt = JWT::encode($payload, $key, 'HS256');
|
||||
// } catch (Exception $e) {
|
||||
// return $this->fail('Error generating JWT: ' . $e->getMessage(), 500);
|
||||
// }
|
||||
|
||||
// // Kirim Respon ke HttpOnly yg akan disimpan di browser dan tidak akan dapat diakses oleh siapapun
|
||||
// // $isSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
// $this->response->setCookie([
|
||||
// // 'name' => 'token', // nama token
|
||||
// // 'value' => $jwt, // value dari jwt yg sudah di hash
|
||||
// // 'expire' => 864000, // 10 hari
|
||||
// // 'path' => '/', // valid untuk semua path
|
||||
// // 'secure' => $isSecure, // true for HTTPS, false for HTTP (localhost)
|
||||
// // 'httponly' => true, // dipakai agar cookie berikut tidak dapat diakses oleh javascript
|
||||
// // 'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
// ]);
|
||||
|
||||
|
||||
// // Response tanpa token di body
|
||||
// return $this->respond([
|
||||
// 'status' => 'success',
|
||||
// 'code' => 200,
|
||||
// 'message' => 'Login successful'
|
||||
// ], 200);
|
||||
// }
|
||||
public function login()
|
||||
{
|
||||
|
||||
// Ambil dari JSON Form dan Key .env
|
||||
$username = $this->request->getVar('username');
|
||||
@ -89,7 +150,9 @@ class Auth extends Controller {
|
||||
$query = $this->db->query($sql);
|
||||
$row = $query->getResultArray();
|
||||
|
||||
if (!$row) { return $this->fail('User not found.', 401); }
|
||||
if (!$row) {
|
||||
return $this->fail('User not found.', 401);
|
||||
}
|
||||
$row = $row[0];
|
||||
if (!password_verify($password, $row['password'])) {
|
||||
return $this->fail('Invalid password.', 401);
|
||||
@ -98,10 +161,10 @@ class Auth extends Controller {
|
||||
// Buat JWT payload
|
||||
$exp = time() + 864000;
|
||||
$payload = [
|
||||
'userid' => $row['id'],
|
||||
'userid' => $row['id'],
|
||||
'roleid' => $row['role_id'],
|
||||
'username' => $row['username'],
|
||||
'exp' => $exp
|
||||
'exp' => $exp
|
||||
];
|
||||
|
||||
try {
|
||||
@ -113,44 +176,64 @@ class Auth extends Controller {
|
||||
|
||||
// Kirim Respon ke HttpOnly yg akan disimpan di browser dan tidak akan dapat diakses oleh siapapun
|
||||
$this->response->setCookie([
|
||||
'name' => 'token', // nama token
|
||||
'value' => $jwt, // value dari jwt yg sudah di hash
|
||||
'expire' => 864000, // 10 hari
|
||||
'path' => '/', // valid untuk semua path
|
||||
'secure' => true, // set true kalau sudah HTTPS
|
||||
'name' => 'token', // nama token
|
||||
'value' => $jwt, // value dari jwt yg sudah di hash
|
||||
'expire' => 864000, // 10 hari
|
||||
'path' => '/', // valid untuk semua path
|
||||
'secure' => true, // set true kalau sudah HTTPS
|
||||
'httponly' => true, // dipakai agar cookie berikut tidak dapat diakses oleh javascript
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
]);
|
||||
|
||||
// Response tanpa token di body
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Login successful'
|
||||
], 200);
|
||||
}
|
||||
|
||||
// ok
|
||||
public function logout() {
|
||||
// public function logout() {
|
||||
// // Definisikan ini pada cookies browser, harus sama dengan cookies login
|
||||
// // $isSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
// return $this->response->setCookie([
|
||||
// 'name' => 'token',
|
||||
// 'value' => '',
|
||||
// 'expire' => time() - 3600,
|
||||
// 'path' => '/',
|
||||
// 'secure' => $isSecure,
|
||||
// 'httponly' => true,
|
||||
// 'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
|
||||
// ])->setJSON([
|
||||
// 'status' => 'success',
|
||||
// 'code' => 200,
|
||||
// 'message' => 'Logout successful'
|
||||
// ], 200);
|
||||
// }
|
||||
public function logout()
|
||||
{
|
||||
// Definisikan ini pada cookies browser, harus sama dengan cookies login
|
||||
return $this->response->setCookie([
|
||||
'name' => 'token',
|
||||
'value' => '',
|
||||
'expire' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'name' => 'token',
|
||||
'value' => '',
|
||||
'expire' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
|
||||
|
||||
])->setJSON([
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Logout successful'
|
||||
], 200);
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Logout successful'
|
||||
], 200);
|
||||
}
|
||||
|
||||
// ok
|
||||
public function register() {
|
||||
public function register()
|
||||
{
|
||||
|
||||
$username = strtolower($this->request->getJsonVar('username'));
|
||||
$password = $this->request->getJsonVar('password');
|
||||
@ -158,7 +241,7 @@ class Auth extends Controller {
|
||||
// Validasi Awal Dari BE
|
||||
if (empty($username) || empty($password)) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'code' => 400,
|
||||
'message' => 'Username and password are required'
|
||||
], 400); // Gunakan 400 Bad Request
|
||||
@ -167,11 +250,11 @@ class Auth extends Controller {
|
||||
// Cek Duplikasi Username
|
||||
$exists = $this->db->query("SELECT id FROM users WHERE username = ?", [$username])->getRow();
|
||||
if ($exists) {
|
||||
return $this->respond(['status' => 'failed', 'code'=>409,'message' => 'Username already exists'], 409);
|
||||
return $this->respond(['status' => 'failed', 'code' => 409, 'message' => 'Username already exists'], 409);
|
||||
}
|
||||
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
|
||||
// Mulai transaksi Insert
|
||||
$this->db->transStart();
|
||||
$this->db->query(
|
||||
@ -183,8 +266,8 @@ class Auth extends Controller {
|
||||
// Cek status transaksi
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'code' => 500,
|
||||
'status' => 'error',
|
||||
'code' => 500,
|
||||
'message' => 'Failed to create user. Please try again later.'
|
||||
], 500);
|
||||
}
|
||||
@ -194,7 +277,7 @@ class Auth extends Controller {
|
||||
'status' => 'success',
|
||||
'code' => 201,
|
||||
'message' => 'User ' . $username . ' successfully created.'
|
||||
], 201);
|
||||
], 201);
|
||||
|
||||
}
|
||||
|
||||
@ -219,19 +302,20 @@ class Auth extends Controller {
|
||||
// return $this->respond($response);
|
||||
// }
|
||||
|
||||
public function coba() {
|
||||
public function coba()
|
||||
{
|
||||
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Decode Token dengan Key yg ada di .env
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
238
app/Controllers/AuthV2Controller.php
Normal file
238
app/Controllers/AuthV2Controller.php
Normal file
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
/**
|
||||
* AuthV2 Controller
|
||||
*
|
||||
* Handles authentication for V2 UI
|
||||
* Separate from the main Auth controller to avoid conflicts
|
||||
*/
|
||||
class AuthV2Controller extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authentication status
|
||||
* GET /v2/auth/check
|
||||
*/
|
||||
public function checkAuth()
|
||||
{
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
if (!$token) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'No token found'
|
||||
], 401);
|
||||
}
|
||||
|
||||
try {
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
|
||||
} catch (ExpiredException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Token expired'
|
||||
], 401);
|
||||
|
||||
} catch (SignatureInvalidException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid token signature'
|
||||
], 401);
|
||||
|
||||
} catch (BeforeValidException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Token not valid yet'
|
||||
], 401);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid token: ' . $e->getMessage()
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login user
|
||||
* POST /v2/auth/login
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$username = $this->request->getVar('username');
|
||||
$password = $this->request->getVar('password');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Validate username
|
||||
if (!$username) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Username is required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Find user
|
||||
$sql = "SELECT * FROM users WHERE username = " . $this->db->escape($username);
|
||||
$query = $this->db->query($sql);
|
||||
$row = $query->getResultArray();
|
||||
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'User not found'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$row = $row[0];
|
||||
|
||||
// Verify password
|
||||
if (!password_verify($password, $row['password'])) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid password'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Create JWT payload
|
||||
$exp = time() + 864000; // 10 days
|
||||
$payload = [
|
||||
'userid' => $row['id'],
|
||||
'roleid' => $row['role_id'],
|
||||
'username' => $row['username'],
|
||||
'exp' => $exp
|
||||
];
|
||||
|
||||
try {
|
||||
$jwt = JWT::encode($payload, $key, 'HS256');
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Error generating JWT: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
// Detect if HTTPS is being used
|
||||
$isSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
|
||||
// Set HTTP-only cookie
|
||||
$this->response->setCookie([
|
||||
'name' => 'token',
|
||||
'value' => $jwt,
|
||||
'expire' => 864000,
|
||||
'path' => '/',
|
||||
'secure' => $isSecure, // false for localhost HTTP
|
||||
'httponly' => true,
|
||||
'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
]);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Login successful',
|
||||
'data' => [
|
||||
'username' => $row['username'],
|
||||
'role_id' => $row['role_id']
|
||||
]
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
* POST /v2/auth/logout
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
// Detect if HTTPS is being used
|
||||
$isSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
|
||||
// Clear the token cookie
|
||||
return $this->response->setCookie([
|
||||
'name' => 'token',
|
||||
'value' => '',
|
||||
'expire' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => $isSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
])->setJSON([
|
||||
'status' => 'success',
|
||||
'message' => 'Logout successful'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new user
|
||||
* POST /v2/auth/register
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$username = strtolower($this->request->getJsonVar('username'));
|
||||
$password = $this->request->getJsonVar('password');
|
||||
|
||||
// Validate input
|
||||
if (empty($username) || empty($password)) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Username and password are required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check for existing username
|
||||
$exists = $this->db->query("SELECT id FROM users WHERE username = ?", [$username])->getRow();
|
||||
if ($exists) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Username already exists'
|
||||
], 409);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// Insert user
|
||||
$this->db->transStart();
|
||||
$this->db->query(
|
||||
"INSERT INTO users(username, password, role_id) VALUES(?, ?, ?)",
|
||||
[$username, $hashedPassword, 1]
|
||||
);
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Failed to create user'
|
||||
], 500);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'User ' . $username . ' successfully created'
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Contact\ContactModel;
|
||||
|
||||
class Contact extends BaseController {
|
||||
class ContactController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -33,13 +33,13 @@ class Contact extends BaseController {
|
||||
|
||||
public function show($ContactID = null) {
|
||||
$model = new ContactModel();
|
||||
$rows = $model->getContactWithDetail($ContactID);
|
||||
$row = $model->getContactWithDetail($ContactID);
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@ -76,4 +76,4 @@ class Contact extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Contact\MedicalSpecialtyModel;
|
||||
|
||||
class MedicalSpecialty extends BaseController {
|
||||
class MedicalSpecialtyController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -32,11 +32,11 @@ class MedicalSpecialty extends BaseController {
|
||||
|
||||
public function show($SpecialtyID = null) {
|
||||
$model = new MedicalSpecialtyModel();
|
||||
$rows = $model->find($SpecialtyID);
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data."], 200);
|
||||
$row = $model->find($SpecialtyID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
@ -61,4 +61,4 @@ class MedicalSpecialty extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Contact\OccupationModel;
|
||||
|
||||
class Occupation extends BaseController {
|
||||
class OccupationController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -32,11 +32,11 @@ class Occupation extends BaseController {
|
||||
|
||||
public function show($OccupationID = null) {
|
||||
$model = new OccupationModel();
|
||||
$rows = $model->find($OccupationID);
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data."], 200);
|
||||
$row = $model->find($OccupationID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
@ -61,4 +61,4 @@ class Occupation extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\CounterModel;
|
||||
|
||||
class Counter extends BaseController {
|
||||
class CounterController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -24,13 +24,13 @@ class Counter extends BaseController {
|
||||
}
|
||||
|
||||
public function show($CounterID = null) {
|
||||
$rows = $this->model->find($CounterID);
|
||||
$row = $this->model->find($CounterID);
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "No Data.", 'data' => [] ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "No Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
@ -62,4 +62,4 @@ class Counter extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,23 +12,25 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Sample extends Controller {
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
public function index()
|
||||
{
|
||||
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Decode Token dengan Key yg ada di .env
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
169
app/Controllers/EdgeController.php
Normal file
169
app/Controllers/EdgeController.php
Normal file
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class EdgeController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $edgeResModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->edgeResModel = new \App\Models\EdgeResModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/edge/results
|
||||
* Receive results from tiny-edge
|
||||
*/
|
||||
public function results()
|
||||
{
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (empty($input)) {
|
||||
return $this->failValidationErrors('Invalid JSON payload');
|
||||
}
|
||||
|
||||
// Extract key fields from payload
|
||||
$sampleId = $input['sample_id'] ?? null;
|
||||
$instrumentId = $input['instrument_id'] ?? null;
|
||||
$patientId = $input['patient_id'] ?? null;
|
||||
|
||||
// Store in edgeres table
|
||||
$data = [
|
||||
'SiteID' => 1, // Default site, can be configured
|
||||
'InstrumentID' => $instrumentId,
|
||||
'SampleID' => $sampleId,
|
||||
'PatientID' => $patientId,
|
||||
'Payload' => json_encode($input),
|
||||
'Status' => 'pending',
|
||||
'AutoProcess' => 0, // Default to manual processing
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$id = $this->edgeResModel->insert($data);
|
||||
|
||||
if (!$id) {
|
||||
return $this->failServerError('Failed to save result');
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Result received and queued',
|
||||
'data' => [
|
||||
'edge_res_id' => $id,
|
||||
'sample_id' => $sampleId,
|
||||
'instrument_id' => $instrumentId
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Error processing result: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/edge/orders
|
||||
* Return pending orders for an instrument
|
||||
*/
|
||||
public function orders()
|
||||
{
|
||||
try {
|
||||
$instrumentId = $this->request->getGet('instrument');
|
||||
|
||||
if (!$instrumentId) {
|
||||
return $this->failValidationErrors('instrument parameter is required');
|
||||
}
|
||||
|
||||
// TODO: Implement order fetching logic
|
||||
// For now, return empty array
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Orders fetched',
|
||||
'data' => []
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Error fetching orders: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/edge/orders/:id/ack
|
||||
* Acknowledge order delivery
|
||||
*/
|
||||
public function ack($orderId = null)
|
||||
{
|
||||
try {
|
||||
if (!$orderId) {
|
||||
return $this->failValidationErrors('Order ID is required');
|
||||
}
|
||||
|
||||
$input = $this->request->getJSON(true);
|
||||
$instrumentId = $input['instrument_id'] ?? null;
|
||||
|
||||
// Log acknowledgment
|
||||
$this->db->table('edgeack')->insert([
|
||||
'OrderID' => $orderId,
|
||||
'InstrumentID' => $instrumentId,
|
||||
'AckDate' => date('Y-m-d H:i:s'),
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Order acknowledged',
|
||||
'data' => [
|
||||
'order_id' => $orderId
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Error acknowledging order: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/edge/status
|
||||
* Log instrument status
|
||||
*/
|
||||
public function status()
|
||||
{
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
$instrumentId = $input['instrument_id'] ?? null;
|
||||
$status = $input['status'] ?? null;
|
||||
$lastActivity = $input['last_activity'] ?? null;
|
||||
$timestamp = $input['timestamp'] ?? date('Y-m-d H:i:s');
|
||||
|
||||
if (!$instrumentId || !$status) {
|
||||
return $this->failValidationErrors('instrument_id and status are required');
|
||||
}
|
||||
|
||||
// Store status log
|
||||
$this->db->table('edgestatus')->insert([
|
||||
'InstrumentID' => $instrumentId,
|
||||
'Status' => $status,
|
||||
'LastActivity' => $lastActivity,
|
||||
'Timestamp' => $timestamp,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Status logged'
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Error logging status: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Home extends Controller {
|
||||
class HomeController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Location\LocationModel;
|
||||
|
||||
class Location extends BaseController {
|
||||
class LocationController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -31,20 +31,20 @@ class Location extends BaseController {
|
||||
}
|
||||
|
||||
public function show($LocationID = null) {
|
||||
$rows = $this->model->getLocation($LocationID);
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
$row = $this->model->getLocation($LocationID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||
try {
|
||||
$id = $this->model->saveLocation($input);
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data created successfully', 'data' => $id ], 201);
|
||||
$result = $this->model->saveLocation($input);
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data created successfully', 'data' => $result ], 201);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
@ -54,8 +54,8 @@ class Location extends BaseController {
|
||||
$input = $this->request->getJSON(true);
|
||||
try {
|
||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors()); }
|
||||
$id = $this->model->saveLocation($input, true);
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 201);
|
||||
$result = $this->model->saveLocation($input, true);
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $result ], 201);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
@ -72,4 +72,4 @@ class Location extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
|
||||
class OrderTest extends Controller {
|
||||
class OrderTestController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function __construct() {
|
||||
@ -34,13 +34,13 @@ class OrderTest extends Controller {
|
||||
}
|
||||
|
||||
public function show($OrderID = null) {
|
||||
$row=$this->db->table('ordertest')->select("*")->where('OrderID=', $OrderID)->get()->getResultArray();
|
||||
$row=$this->db->table('ordertest')->select("*")->where('OrderID', $OrderID)->get()->getRowArray();
|
||||
|
||||
if (empty($row)) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Data not found.",
|
||||
'data' => [],
|
||||
'data' => null,
|
||||
], 200);
|
||||
}
|
||||
|
||||
@ -214,4 +214,4 @@ class OrderTest extends Controller {
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\AccountModel;
|
||||
|
||||
class Account extends BaseController {
|
||||
class AccountController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -34,13 +34,13 @@ class Account extends BaseController {
|
||||
|
||||
public function show($AccountID = null) {
|
||||
//$rows = $this->model->where('AccountID', $AccountID)->findAll();
|
||||
$rows = $this->model->getAccount($AccountID);
|
||||
$row = $this->model->getAccount($AccountID);
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@ -76,4 +76,4 @@ class Account extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\DepartmentModel;
|
||||
|
||||
class Department extends BaseController {
|
||||
class DepartmentController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -32,13 +32,12 @@ class Department extends BaseController {
|
||||
}
|
||||
|
||||
public function show($DepartmentID = null) {
|
||||
//$rows = $this->model->where('DepartmentID', $DepartmentID)->findAll();
|
||||
$rows = $this->model->getDepartment($DepartmentID);
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
$row = $this->model->getDepartment($DepartmentID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@ -73,4 +72,4 @@ class Department extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\DisciplineModel;
|
||||
|
||||
class Discipline extends BaseController {
|
||||
class DisciplineController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -32,13 +32,13 @@ class Discipline extends BaseController {
|
||||
}
|
||||
|
||||
public function show($DisciplineID = null) {
|
||||
$rows = $this->model->where('DisciplineID', $DisciplineID)->findAll();
|
||||
$row = $this->model->where('DisciplineID', $DisciplineID)->first();
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@ -79,4 +79,4 @@ class Discipline extends BaseController {
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\SiteModel;
|
||||
|
||||
class Site extends BaseController {
|
||||
class SiteController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -33,13 +33,13 @@ class Site extends BaseController {
|
||||
|
||||
public function show($SiteID = null) {
|
||||
//$rows = $this->model->where('SiteID', $SiteID)->findAll();
|
||||
$rows = $this->model->getSite($SiteID);
|
||||
$row = $this->model->getSite($SiteID);
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@ -75,4 +75,4 @@ class Site extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\WorkstationModel;
|
||||
|
||||
class Workstation extends BaseController {
|
||||
class WorkstationController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -32,13 +32,13 @@ class Workstation extends BaseController {
|
||||
}
|
||||
|
||||
public function show($WorkstationID = null) {
|
||||
$rows = $this->model->getWorkstation($WorkstationID);
|
||||
$row = $this->model->getWorkstation($WorkstationID);
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@ -73,4 +73,4 @@ class Workstation extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Pages;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
/**
|
||||
* Auth Pages Controller
|
||||
* Handles rendering of authentication-related pages
|
||||
*/
|
||||
class AuthPage extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login page
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
// Check if user is already authenticated
|
||||
$token = $this->request->getCookie('token');
|
||||
|
||||
if ($token) {
|
||||
// If token exists, redirect to dashboard
|
||||
return redirect()->to('/dashboard');
|
||||
}
|
||||
|
||||
return view('pages/login', [
|
||||
'title' => 'Login',
|
||||
'description' => 'Sign in to your CLQMS account'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout - clear cookie and redirect
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
// Delete the token cookie
|
||||
$response = service('response');
|
||||
$response->deleteCookie('token');
|
||||
|
||||
return redirect()->to('/login');
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Pages;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
|
||||
/**
|
||||
* Dashboard Page Controller
|
||||
* Handles rendering of the main dashboard
|
||||
*/
|
||||
class DashboardPage extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the dashboard page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Check authentication
|
||||
$token = $this->request->getCookie('token');
|
||||
|
||||
if (!$token) {
|
||||
return redirect()->to('/login');
|
||||
}
|
||||
|
||||
try {
|
||||
$key = getenv('JWT_SECRET');
|
||||
$decoded = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return view('pages/dashboard', [
|
||||
'title' => 'Dashboard',
|
||||
'description' => 'CLQMS Dashboard - Overview',
|
||||
'user' => $decoded
|
||||
]);
|
||||
} catch (ExpiredException $e) {
|
||||
// Token expired, redirect to login
|
||||
$response = service('response');
|
||||
$response->deleteCookie('token');
|
||||
return redirect()->to('/login');
|
||||
} catch (\Exception $e) {
|
||||
// Invalid token
|
||||
$response = service('response');
|
||||
$response->deleteCookie('token');
|
||||
return redirect()->to('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
178
app/Controllers/PagesController.php
Normal file
178
app/Controllers/PagesController.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
/**
|
||||
* PagesController - Serves view pages
|
||||
*
|
||||
* This controller only returns views. No business logic.
|
||||
* All data is fetched via API calls from the frontend.
|
||||
*/
|
||||
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'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Master Data - Value Sets
|
||||
*/
|
||||
public function masterValueSets()
|
||||
{
|
||||
return view('v2/master/valuesets/valuesets_index', [
|
||||
'pageTitle' => 'Value Sets',
|
||||
'activePage' => 'master-valuesets'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login page
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
return view('v2/auth/login', [
|
||||
'pageTitle' => 'Login',
|
||||
'activePage' => ''
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
use App\Models\PatVisit\PatVisitModel;
|
||||
use App\Models\PatVisit\PatVisitADTModel;
|
||||
|
||||
class PatVisit extends BaseController {
|
||||
class PatVisitController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -18,9 +18,10 @@ class PatVisit extends BaseController {
|
||||
public function show($PVID = null) {
|
||||
try {
|
||||
$row = $this->model->show($PVID);
|
||||
if($row == []) { $message = "data not found"; }
|
||||
else { $message = "data found"; }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> $message, 'data' => $row ], 200);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data found", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong '.$e->getMessage());
|
||||
}
|
||||
@ -81,4 +82,4 @@ class PatVisit extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\Controller;
|
||||
|
||||
use App\Models\Patient\PatientModel;
|
||||
|
||||
class Patient extends Controller {
|
||||
class PatientController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -66,9 +66,9 @@ class Patient extends Controller {
|
||||
|
||||
public function show($InternalPID = null) {
|
||||
try {
|
||||
$rows = $this->model->getPatient($InternalPID);
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "data not found." ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message' => "data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->getPatient($InternalPID);
|
||||
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "data not found.", 'data' => null ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message' => "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
@ -214,4 +214,4 @@ class Patient extends Controller {
|
||||
return $this->failServerError('Something went wrong.'.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Dashboard extends Controller {
|
||||
class ResultController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
@ -12,7 +12,7 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Result extends Controller {
|
||||
class SampleController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\ContainerDefModel;
|
||||
|
||||
class ContainerDef extends BaseController {
|
||||
class ContainerDefController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -37,8 +37,11 @@ class ContainerDef extends BaseController {
|
||||
|
||||
public function show($ConDefID) {
|
||||
try {
|
||||
$rows = $this->model->getContainer($ConDefID);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->getContainer($ConDefID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
}
|
||||
@ -66,4 +69,4 @@ class ContainerDef extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\SpecimenCollectionModel;
|
||||
|
||||
class SpecimenCollection extends BaseController {
|
||||
class SpecimenCollectionController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -30,8 +30,11 @@ class SpecimenCollection extends BaseController {
|
||||
|
||||
public function show($id) {
|
||||
try {
|
||||
$rows = $this->model->where('SpcColID', $id)->findAll();
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->where('SpcColID', $id)->first();
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
}
|
||||
@ -59,4 +62,4 @@ class SpecimenCollection extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\SpecimenModel;
|
||||
|
||||
class Specimen extends BaseController {
|
||||
class SpecimenController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -30,8 +30,11 @@ class Specimen extends BaseController {
|
||||
|
||||
public function show($id) {
|
||||
try {
|
||||
$rows = $this->model->where('SID',$id)->findAll();
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->where('SID',$id)->first();
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
}
|
||||
@ -59,4 +62,4 @@ class Specimen extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\SpecimenPrepModel;
|
||||
|
||||
class SpecimenPrep extends BaseController {
|
||||
class SpecimenPrepController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -30,8 +30,11 @@ class SpecimenPrep extends BaseController {
|
||||
|
||||
public function show($id) {
|
||||
try {
|
||||
$rows = $this->model->where('SpcPrpID', $id)->findAll();
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->where('SpcPrpID', $id)->first();
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
}
|
||||
@ -59,4 +62,4 @@ class SpecimenPrep extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -30,8 +30,11 @@ class ContainerDef extends BaseController {
|
||||
|
||||
public function show($id) {
|
||||
try {
|
||||
$rows = $this->model->where('SpcStaID', $id)->findAll();
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->where('SpcStaID', $id)->first();
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
}
|
||||
@ -59,4 +62,4 @@ class ContainerDef extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Test\TestMapModel;
|
||||
|
||||
class TestMap extends BaseController {
|
||||
class TestMapController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -24,9 +24,9 @@ class TestMap extends BaseController {
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
$rows = $this->model->where('TestMapID',$id)->findAll();
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->where('TestMapID',$id)->first();
|
||||
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
@ -53,4 +53,4 @@ class TestMap extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class Tests extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $rules;
|
||||
protected $model;
|
||||
protected $modelCal;
|
||||
protected $modelTech;
|
||||
protected $modelGrp;
|
||||
protected $modelValueSet;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->model = new \App\Models\Test\TestDefSiteModel;
|
||||
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
||||
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
||||
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
||||
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
||||
|
||||
// Basic validation for the main part
|
||||
$this->rules = [
|
||||
'TestSiteCode' => 'required',
|
||||
'TestSiteName' => 'required',
|
||||
'TestType' => 'required'
|
||||
];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$rows = $this->model->getTests();
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
if (!$id) return $this->failValidationErrors('ID is required');
|
||||
$rows = $this->model->getTest($id);
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (!$this->validateData($input, $this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Insert into Main Table (testdefsite)
|
||||
$id = $this->model->insert($input);
|
||||
if (!$id) {
|
||||
throw new \Exception("Failed to insert main test definition");
|
||||
}
|
||||
|
||||
// 2. Handle Details based on TestType
|
||||
$this->handleDetails($id, $input, 'insert');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data created successfully", 'data'=> $id ]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) { $id = $input["TestSiteID"]; }
|
||||
if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); }
|
||||
|
||||
// Optional validation
|
||||
// if (!$this->validateData($input, $this->rules)) { ... }
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Update Main Table
|
||||
$this->model->update($id, $input);
|
||||
|
||||
// 2. Handle Details
|
||||
$this->handleDetails($id, $input, 'update');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data updated successfully", 'data'=> $id ]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to handle inserting/updating sub-tables based on TestType
|
||||
*/
|
||||
private function handleDetails($testSiteID, $input, $action) {
|
||||
$testTypeID = $input['TestType'] ?? null;
|
||||
|
||||
// If update and TestType not in payload, fetch from DB
|
||||
if (!$testTypeID && $action === 'update') {
|
||||
$existing = $this->model->find($testSiteID);
|
||||
$testTypeID = $existing['TestType'] ?? null;
|
||||
}
|
||||
|
||||
if (!$testTypeID) return; // Should not happen if required
|
||||
|
||||
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
||||
$vs = $this->modelValueSet->find($testTypeID);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// Get details data if present (for 'details' key in unified JSON)
|
||||
// We accept both flat (top-level) and nested 'details' for flexibility, prefer 'details'
|
||||
$details = $input['details'] ?? $input;
|
||||
$details['TestSiteID'] = $testSiteID; // Ensure foreign key is set
|
||||
$details['SiteID'] = $input['SiteID'] ?? 1;
|
||||
|
||||
switch ($typeCode) {
|
||||
case 'CALC':
|
||||
$this->saveSubTable($this->modelCal, $testSiteID, $details, $action, 'TestCalID');
|
||||
break;
|
||||
|
||||
case 'GROUP':
|
||||
// Groups are special: List of members
|
||||
// Payload expected: details: { members: [{Member: 1}, {Member: 2}] }
|
||||
if ($action === 'update') {
|
||||
$this->modelGrp->where('TestSiteID', $testSiteID)->delete();
|
||||
}
|
||||
|
||||
$members = $details['members'] ?? ($input['Members'] ?? []);
|
||||
if (is_array($members)) {
|
||||
foreach ($members as $m) {
|
||||
$memberID = is_array($m) ? ($m['Member'] ?? null) : $m;
|
||||
if ($memberID) {
|
||||
$this->modelGrp->insert([
|
||||
'SiteID' => $details['SiteID'],
|
||||
'TestSiteID' => $testSiteID,
|
||||
'Member' => $memberID
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 'TEST':
|
||||
case 'PARAM':
|
||||
default:
|
||||
// Default to TestDefTech for 'TEST' and 'PARAM'
|
||||
$this->saveSubTable($this->modelTech, $testSiteID, $details, $action, 'TestTechID');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function saveSubTable($model, $testSiteID, $data, $action, $pkName) {
|
||||
if ($action === 'update') {
|
||||
// Check existence
|
||||
$exists = $model->where('TestSiteID', $testSiteID)->first();
|
||||
if ($exists) {
|
||||
$model->update($exists[$pkName], $data);
|
||||
} else {
|
||||
$model->insert($data);
|
||||
}
|
||||
} else {
|
||||
$model->insert($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
741
app/Controllers/TestsController.php
Normal file
741
app/Controllers/TestsController.php
Normal file
@ -0,0 +1,741 @@
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class TestsController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $rules;
|
||||
protected $model;
|
||||
protected $modelCal;
|
||||
protected $modelTech;
|
||||
protected $modelGrp;
|
||||
protected $modelMap;
|
||||
protected $modelValueSet;
|
||||
protected $modelRefNum;
|
||||
protected $modelRefTxt;
|
||||
|
||||
// Valueset ID constants
|
||||
const VALUESET_REF_TYPE = 44; // testdeftech.RefType
|
||||
const VALUESET_RANGE_TYPE = 45; // refnum.RangeType
|
||||
const VALUESET_NUM_REF_TYPE = 46; // refnum.NumRefType
|
||||
const VALUESET_TXT_REF_TYPE = 47; // reftxt.TxtRefType
|
||||
const VALUESET_SEX = 3; // Sex values
|
||||
const VALUESET_MATH_SIGN = 41; // LowSign, HighSign
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->model = new \App\Models\Test\TestDefSiteModel;
|
||||
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
||||
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
||||
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
||||
$this->modelMap = new \App\Models\Test\TestMapModel;
|
||||
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
||||
$this->modelRefNum = new \App\Models\RefRange\RefNumModel;
|
||||
$this->modelRefTxt = new \App\Models\RefRange\RefTxtModel;
|
||||
|
||||
// Validation rules for main test definition
|
||||
$this->rules = [
|
||||
'TestSiteCode' => 'required|min_length[3]|max_length[6]',
|
||||
'TestSiteName' => 'required',
|
||||
'TestType' => 'required',
|
||||
'SiteID' => 'required'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests
|
||||
* GET /v1/tests/site
|
||||
* List all tests with optional filtering
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$siteId = $this->request->getGet('SiteID');
|
||||
$testType = $this->request->getGet('TestType');
|
||||
$visibleScr = $this->request->getGet('VisibleScr');
|
||||
$visibleRpt = $this->request->getGet('VisibleRpt');
|
||||
$keyword = $this->request->getGet('TestSiteName');
|
||||
|
||||
$builder = $this->db->table('testdefsite')
|
||||
->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where('testdefsite.EndDate IS NULL');
|
||||
|
||||
if ($siteId) {
|
||||
$builder->where('testdefsite.SiteID', $siteId);
|
||||
}
|
||||
|
||||
if ($testType) {
|
||||
$builder->where('testdefsite.TestType', $testType);
|
||||
}
|
||||
|
||||
if ($visibleScr !== null) {
|
||||
$builder->where('testdefsite.VisibleScr', $visibleScr);
|
||||
}
|
||||
|
||||
if ($visibleRpt !== null) {
|
||||
$builder->where('testdefsite.VisibleRpt', $visibleRpt);
|
||||
}
|
||||
|
||||
if ($keyword) {
|
||||
$builder->like('testdefsite.TestSiteName', $keyword);
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('testdefsite.SeqScr', 'ASC')->get()->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => []], 200);
|
||||
}
|
||||
return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $rows], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests/{id}
|
||||
* GET /v1/tests/site/{id}
|
||||
* Get single test by ID with all related details
|
||||
*/
|
||||
public function show($id = null)
|
||||
{
|
||||
if (!$id)
|
||||
return $this->failValidationErrors('TestSiteID is required');
|
||||
|
||||
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where("testdefsite.TestSiteID", $id)
|
||||
->find($id);
|
||||
|
||||
if (!$row) {
|
||||
return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => null], 200);
|
||||
}
|
||||
|
||||
// Load related details based on TestType
|
||||
$typeCode = $row['TypeCode'] ?? '';
|
||||
|
||||
if ($typeCode === 'CALC') {
|
||||
// Load calculation details
|
||||
$row['testdefcal'] = $this->db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left')
|
||||
->where('testdefcal.TestSiteID', $id)
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
// Load group members with test details
|
||||
$row['testdefgrp'] = $this->db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $id)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'TITLE') {
|
||||
// Load test mappings only for TITLE type
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} else {
|
||||
// TEST or PARAM - load technical details
|
||||
$row['testdeftech'] = $this->db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $id)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
// Load refnum/reftxt based on RefType
|
||||
if (!empty($row['testdeftech'])) {
|
||||
$techData = $row['testdeftech'][0];
|
||||
$refType = (int) $techData['RefType'];
|
||||
|
||||
// Load refnum for NMRC type (RefType = 1)
|
||||
if ($refType === 1) {
|
||||
$refnumData = $this->modelRefNum
|
||||
->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('Display', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// Add VValue for display
|
||||
$row['refnum'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefNumID' => $r['RefNumID'],
|
||||
'NumRefType' => (int) $r['NumRefType'],
|
||||
'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']),
|
||||
'RangeType' => (int) $r['RangeType'],
|
||||
'RangeTypeVValue' => $this->getVValue(45, $r['RangeType']),
|
||||
'Sex' => (int) $r['Sex'],
|
||||
'SexVValue' => $this->getVValue(3, $r['Sex']),
|
||||
'AgeStart' => (int) $r['AgeStart'],
|
||||
'AgeEnd' => (int) $r['AgeEnd'],
|
||||
'LowSign' => $r['LowSign'] !== null ? (int) $r['LowSign'] : null,
|
||||
'LowSignVValue' => $this->getVValue(41, $r['LowSign']),
|
||||
'Low' => $r['Low'] !== null ? (int) $r['Low'] : null,
|
||||
'HighSign' => $r['HighSign'] !== null ? (int) $r['HighSign'] : null,
|
||||
'HighSignVValue' => $this->getVValue(41, $r['HighSign']),
|
||||
'High' => $r['High'] !== null ? (int) $r['High'] : null,
|
||||
'Flag' => $r['Flag']
|
||||
];
|
||||
}, $refnumData ?? []);
|
||||
|
||||
$row['numRefTypeOptions'] = $this->getValuesetOptions(46);
|
||||
$row['rangeTypeOptions'] = $this->getValuesetOptions(45);
|
||||
}
|
||||
|
||||
// Load reftxt for TEXT type (RefType = 2)
|
||||
if ($refType === 2) {
|
||||
$reftxtData = $this->modelRefTxt
|
||||
->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('RefTxtID', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$row['reftxt'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefTxtID' => $r['RefTxtID'],
|
||||
'TxtRefType' => (int) $r['TxtRefType'],
|
||||
'TxtRefTypeVValue' => $this->getVValue(47, $r['TxtRefType']),
|
||||
'Sex' => (int) $r['Sex'],
|
||||
'SexVValue' => $this->getVValue(3, $r['Sex']),
|
||||
'AgeStart' => (int) $r['AgeStart'],
|
||||
'AgeEnd' => (int) $r['AgeEnd'],
|
||||
'RefTxt' => $r['RefTxt'],
|
||||
'Flag' => $r['Flag']
|
||||
];
|
||||
}, $reftxtData ?? []);
|
||||
|
||||
$row['txtRefTypeOptions'] = $this->getValuesetOptions(47);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include valueset options for dropdowns
|
||||
$row['refTypeOptions'] = $this->getValuesetOptions(self::VALUESET_REF_TYPE);
|
||||
$row['sexOptions'] = $this->getValuesetOptions(self::VALUESET_SEX);
|
||||
$row['mathSignOptions'] = $this->getValuesetOptions(self::VALUESET_MATH_SIGN);
|
||||
|
||||
return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $row], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/tests
|
||||
* POST /v1/tests/site
|
||||
* Create new test definition
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (!$this->validateData($input, $this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Insert into Main Table (testdefsite)
|
||||
$testSiteData = [
|
||||
'SiteID' => $input['SiteID'],
|
||||
'TestSiteCode' => $input['TestSiteCode'],
|
||||
'TestSiteName' => $input['TestSiteName'],
|
||||
'TestType' => $input['TestType'],
|
||||
'Description' => $input['Description'] ?? null,
|
||||
'SeqScr' => $input['SeqScr'] ?? 0,
|
||||
'SeqRpt' => $input['SeqRpt'] ?? 0,
|
||||
'IndentLeft' => $input['IndentLeft'] ?? 0,
|
||||
'FontStyle' => $input['FontStyle'] ?? null,
|
||||
'VisibleScr' => $input['VisibleScr'] ?? 1,
|
||||
'VisibleRpt' => $input['VisibleRpt'] ?? 1,
|
||||
'CountStat' => $input['CountStat'] ?? 1,
|
||||
'StartDate' => $input['StartDate'] ?? date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$id = $this->model->insert($testSiteData);
|
||||
if (!$id) {
|
||||
throw new \Exception("Failed to insert main test definition");
|
||||
}
|
||||
|
||||
// 2. Handle Details based on TestType
|
||||
$this->handleDetails($id, $input, 'insert');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'created',
|
||||
'message' => "Test created successfully",
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT/PATCH /v1/tests/{id}
|
||||
* PUT/PATCH /v1/tests/site/{id}
|
||||
* Update existing test definition
|
||||
*/
|
||||
public function update($id = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) {
|
||||
$id = $input["TestSiteID"];
|
||||
}
|
||||
if (!$id) {
|
||||
return $this->failValidationErrors('TestSiteID is required.');
|
||||
}
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Update Main Table
|
||||
$testSiteData = [];
|
||||
$allowedUpdateFields = [
|
||||
'TestSiteCode',
|
||||
'TestSiteName',
|
||||
'TestType',
|
||||
'Description',
|
||||
'SeqScr',
|
||||
'SeqRpt',
|
||||
'IndentLeft',
|
||||
'FontStyle',
|
||||
'VisibleScr',
|
||||
'VisibleRpt',
|
||||
'CountStat',
|
||||
'StartDate'
|
||||
];
|
||||
|
||||
foreach ($allowedUpdateFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$testSiteData[$field] = $input[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($testSiteData)) {
|
||||
$this->model->update($id, $testSiteData);
|
||||
}
|
||||
|
||||
// 2. Handle Details
|
||||
$this->handleDetails($id, $input, 'update');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test updated successfully",
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /v1/tests/{id}
|
||||
* DELETE /v1/tests/site/{id}
|
||||
* Soft delete test by setting EndDate
|
||||
*/
|
||||
public function delete($id = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) {
|
||||
$id = $input["TestSiteID"];
|
||||
}
|
||||
if (!$id) {
|
||||
return $this->failValidationErrors('TestSiteID is required.');
|
||||
}
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
// Check if already disabled
|
||||
if (!empty($existing['EndDate'])) {
|
||||
return $this->failValidationErrors('Test is already disabled');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// 1. Soft delete main record
|
||||
$this->model->update($id, ['EndDate' => $now]);
|
||||
|
||||
// 2. Get TestType to handle related records
|
||||
$testType = $existing['TestType'];
|
||||
$vs = $this->modelValueSet->find($testType);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// 3. Soft delete related records based on TestType
|
||||
if ($typeCode === 'CALC') {
|
||||
$this->db->table('testdefcal')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
$this->db->table('testdeftech')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
|
||||
// Soft delete refnum and reftxt records
|
||||
$this->modelRefNum->where('TestSiteID', $id)->set('EndDate', $now)->update();
|
||||
$this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update();
|
||||
}
|
||||
|
||||
// 4. Soft delete test mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test disabled successfully",
|
||||
'data' => ['TestSiteId' => $id, 'EndDate' => $now]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get valueset options
|
||||
*/
|
||||
private function getValuesetOptions($vsetID)
|
||||
{
|
||||
return $this->db->table('valueset')
|
||||
->select('VID as vid, VValue as vvalue, VDesc as vdesc')
|
||||
->where('VSetID', $vsetID)
|
||||
->orderBy('VOrder', 'ASC')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get VValue from VID for display
|
||||
*/
|
||||
private function getVValue($vsetID, $vid)
|
||||
{
|
||||
if ($vid === null || $vid === '')
|
||||
return null;
|
||||
$row = $this->db->table('valueset')
|
||||
->select('VValue as vvalue')
|
||||
->where('VSetID', $vsetID)
|
||||
->where('VID', (int) $vid)
|
||||
->get()->getRowArray();
|
||||
return $row ? $row['vvalue'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to handle inserting/updating sub-tables based on TestType
|
||||
*/
|
||||
private function handleDetails($testSiteID, $input, $action)
|
||||
{
|
||||
$testTypeID = $input['TestType'] ?? null;
|
||||
|
||||
// If update and TestType not in payload, fetch from DB
|
||||
if (!$testTypeID && $action === 'update') {
|
||||
$existing = $this->model->find($testSiteID);
|
||||
$testTypeID = $existing['TestType'] ?? null;
|
||||
}
|
||||
|
||||
if (!$testTypeID)
|
||||
return;
|
||||
|
||||
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
||||
$vs = $this->modelValueSet->find($testTypeID);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// Get details data from input
|
||||
$details = $input['details'] ?? $input;
|
||||
$details['TestSiteID'] = $testSiteID;
|
||||
$details['SiteID'] = $input['SiteID'] ?? 1;
|
||||
|
||||
switch ($typeCode) {
|
||||
case 'CALC':
|
||||
$this->saveCalcDetails($testSiteID, $details, $action);
|
||||
break;
|
||||
|
||||
case 'GROUP':
|
||||
$this->saveGroupDetails($testSiteID, $details, $input, $action);
|
||||
break;
|
||||
|
||||
case 'TITLE':
|
||||
// TITLE type only has testdefsite, no additional details needed
|
||||
// But we should save test mappings if provided
|
||||
if (isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TEST':
|
||||
case 'PARAM':
|
||||
default:
|
||||
$this->saveTechDetails($testSiteID, $details, $action, $typeCode);
|
||||
|
||||
// Save refnum/reftxt for TEST/PARAM types
|
||||
if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) {
|
||||
$refType = (int) $details['RefType'];
|
||||
|
||||
// Save refnum for NMRC type (RefType = 1)
|
||||
if ($refType === 1 && isset($input['refnum']) && is_array($input['refnum'])) {
|
||||
$this->saveRefNumRanges($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1);
|
||||
}
|
||||
|
||||
// Save reftxt for TEXT type (RefType = 2)
|
||||
if ($refType === 2 && isset($input['reftxt']) && is_array($input['reftxt'])) {
|
||||
$this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, $input['SiteID'] ?? 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Save test mappings for TEST and CALC types as well
|
||||
if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save technical details for TEST and PARAM types
|
||||
*/
|
||||
private function saveTechDetails($testSiteID, $data, $action, $typeCode)
|
||||
{
|
||||
$techData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'ResultType' => $data['ResultType'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? null,
|
||||
'VSet' => $data['VSet'] ?? null,
|
||||
'ReqQty' => $data['ReqQty'] ?? null,
|
||||
'ReqQtyUnit' => $data['ReqQtyUnit'] ?? null,
|
||||
'Unit1' => $data['Unit1'] ?? null,
|
||||
'Factor' => $data['Factor'] ?? null,
|
||||
'Unit2' => $data['Unit2'] ?? null,
|
||||
'Decimal' => $data['Decimal'] ?? 2,
|
||||
'CollReq' => $data['CollReq'] ?? null,
|
||||
'Method' => $data['Method'] ?? null,
|
||||
'ExpectedTAT' => $data['ExpectedTAT'] ?? null
|
||||
];
|
||||
|
||||
if ($action === 'update') {
|
||||
$exists = $this->db->table('testdeftech')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
$this->modelTech->update($exists['TestTechID'], $techData);
|
||||
} else {
|
||||
$this->modelTech->insert($techData);
|
||||
}
|
||||
} else {
|
||||
$this->modelTech->insert($techData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save refnum ranges for NMRC type
|
||||
*/
|
||||
private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
$this->modelRefNum->where('TestSiteID', $testSiteID)
|
||||
->set('EndDate', date('Y-m-d H:i:s'))
|
||||
->update();
|
||||
}
|
||||
|
||||
foreach ($ranges as $index => $range) {
|
||||
$this->modelRefNum->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'NumRefType' => (int) $range['NumRefType'],
|
||||
'RangeType' => (int) $range['RangeType'],
|
||||
'Sex' => (int) $range['Sex'],
|
||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
||||
'LowSign' => !empty($range['LowSign']) ? (int) $range['LowSign'] : null,
|
||||
'Low' => !empty($range['Low']) ? (int) $range['Low'] : null,
|
||||
'HighSign' => !empty($range['HighSign']) ? (int) $range['HighSign'] : null,
|
||||
'High' => !empty($range['High']) ? (int) $range['High'] : null,
|
||||
'Flag' => $range['Flag'] ?? null,
|
||||
'Display' => $index,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save reftxt ranges for TEXT type
|
||||
*/
|
||||
private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
$this->modelRefTxt->where('TestSiteID', $testSiteID)
|
||||
->set('EndDate', date('Y-m-d H:i:s'))
|
||||
->update();
|
||||
}
|
||||
|
||||
foreach ($ranges as $range) {
|
||||
$this->modelRefTxt->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'TxtRefType' => (int) $range['TxtRefType'],
|
||||
'Sex' => (int) $range['Sex'],
|
||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
||||
'RefTxt' => $range['RefTxt'] ?? '',
|
||||
'Flag' => $range['Flag'] ?? null,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save calculation details for CALC type
|
||||
*/
|
||||
private function saveCalcDetails($testSiteID, $data, $action)
|
||||
{
|
||||
$calcData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'FormulaInput' => $data['FormulaInput'] ?? null,
|
||||
'FormulaCode' => $data['FormulaCode'] ?? $data['Formula'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? 'NMRC',
|
||||
'Unit1' => $data['Unit1'] ?? $data['ResultUnit'] ?? null,
|
||||
'Factor' => $data['Factor'] ?? null,
|
||||
'Unit2' => $data['Unit2'] ?? null,
|
||||
'Decimal' => $data['Decimal'] ?? 2,
|
||||
'Method' => $data['Method'] ?? null
|
||||
];
|
||||
|
||||
if ($action === 'update') {
|
||||
$exists = $this->db->table('testdefcal')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
$this->modelCal->update($exists['TestCalID'], $calcData);
|
||||
} else {
|
||||
$this->modelCal->insert($calcData);
|
||||
}
|
||||
} else {
|
||||
$this->modelCal->insert($calcData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save group details for GROUP type
|
||||
*/
|
||||
private function saveGroupDetails($testSiteID, $data, $input, $action)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing members
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
// Get members from details or input
|
||||
$members = $data['members'] ?? ($input['Members'] ?? []);
|
||||
|
||||
if (is_array($members)) {
|
||||
foreach ($members as $m) {
|
||||
$memberID = is_array($m) ? ($m['Member'] ?? ($m['TestSiteID'] ?? null)) : $m;
|
||||
if ($memberID) {
|
||||
$this->modelGrp->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'Member' => $memberID
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save test mappings
|
||||
*/
|
||||
private function saveTestMap($testSiteID, $mappings, $action)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
if (is_array($mappings)) {
|
||||
foreach ($mappings as $map) {
|
||||
$mapData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'HostType' => $map['HostType'] ?? null,
|
||||
'HostID' => $map['HostID'] ?? null,
|
||||
'HostDataSource' => $map['HostDataSource'] ?? null,
|
||||
'HostTestCode' => $map['HostTestCode'] ?? null,
|
||||
'HostTestName' => $map['HostTestName'] ?? null,
|
||||
'ClientType' => $map['ClientType'] ?? null,
|
||||
'ClientID' => $map['ClientID'] ?? null,
|
||||
'ClientDataSource' => $map['ClientDataSource'] ?? null,
|
||||
'ConDefID' => $map['ConDefID'] ?? null,
|
||||
'ClientTestCode' => $map['ClientTestCode'] ?? null,
|
||||
'ClientTestName' => $map['ClientTestName'] ?? null
|
||||
];
|
||||
$this->modelMap->insert($mapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ValueSet\ValueSetModel;
|
||||
|
||||
class ValueSet extends BaseController {
|
||||
class ValueSetController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -23,17 +23,31 @@ class ValueSet extends BaseController {
|
||||
|
||||
public function index() {
|
||||
$param = $this->request->getVar('param');
|
||||
$rows = $this->model->getValueSets($param);
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
$VSetID = $this->request->getVar('VSetID');
|
||||
$page = $this->request->getVar('page') ?? 1;
|
||||
$limit = $this->request->getVar('limit') ?? 20;
|
||||
|
||||
$result = $this->model->getValueSets($param, $page, $limit, $VSetID);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message'=> "Data fetched successfully",
|
||||
'data' => $result['data'],
|
||||
'pagination' => [
|
||||
'currentPage' => (int)$page,
|
||||
'totalPages' => $result['pager']->getPageCount(),
|
||||
'totalItems' => $result['pager']->getTotal(),
|
||||
'limit' => (int)$limit
|
||||
]
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function show($VID = null) {
|
||||
$rows = $this->model->getValueSet($VID);
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "ValueSet with ID $VID not found.", 'data' => [] ], 200);
|
||||
$row = $this->model->getValueSet($VID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "ValueSet with ID $VID not found.", 'data' => null ], 200);
|
||||
}
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows], 200);
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row], 200);
|
||||
}
|
||||
|
||||
public function showByValueSetDef($VSetID = null) {
|
||||
@ -80,4 +94,4 @@ class ValueSet extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ValueSet\ValueSetDefModel;
|
||||
|
||||
class ValueSetDef extends BaseController {
|
||||
class ValueSetDefController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -29,9 +29,9 @@ class ValueSetDef extends BaseController {
|
||||
}
|
||||
|
||||
public function show($VSetID = null) {
|
||||
$rows = $this->model->find($VSetID);
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
$row = $this->model->find($VSetID);
|
||||
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
@ -70,4 +70,4 @@ class ValueSetDef extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SyncCRM\ZonesModel;
|
||||
|
||||
class Zones extends BaseController {
|
||||
class ZonesController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -92,4 +92,4 @@ class Zones extends BaseController {
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
*/
|
||||
@ -24,12 +24,14 @@ class CreateLocationTable extends Migration {
|
||||
$this->forge->addField([
|
||||
'LocationID' => ['type' => 'INT', 'unsigned' => true],
|
||||
'Street1' => ['type' => 'Varchar', 'constraint' => 255, 'null' => true],
|
||||
'Street2' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
|
||||
'Street2' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'City' => ['type' => 'int', 'null' => true],
|
||||
'Province' => ['type' => 'int', 'null' => true],
|
||||
'PostCode' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
|
||||
'GeoLocationSystem' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
|
||||
'GeoLocationData' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
|
||||
'Phone' => ['type' => 'varchar', 'constraint' => 100, 'null' => true],
|
||||
'Email' => ['type' => 'varchar', 'constraint' => 150, 'null' => true],
|
||||
'CreateDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'EndDate' => ['type' => 'DATETIME', 'null' => true]
|
||||
]);
|
||||
|
||||
@ -6,97 +6,107 @@ use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateTestsTable extends Migration {
|
||||
public function up() {
|
||||
// testdefsite - Main test definition table per site
|
||||
$this->forge->addField([
|
||||
'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteCode' => ['type' => 'varchar', 'constraint'=> 6, 'null' => false],
|
||||
'TestSiteName' => ['type' => 'varchar', 'constraint'=> 50, 'null' => false],
|
||||
'TestSiteName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => false],
|
||||
'TestType' => ['type' => 'int', 'null' => false],
|
||||
'Description' => ['type' => 'varchar', 'constraint'=> 150, 'null' => true],
|
||||
'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||
'SeqScr' => ['type' => 'int', 'null' => true],
|
||||
'SeqRpt' => ['type' => 'int', 'null' => true],
|
||||
'IndentLeft' => ['type' => 'int', 'null' => true],
|
||||
'VisibleScr' => ['type' => 'int', 'null' => true],
|
||||
'VisibleRpt' => ['type' => 'int', 'null' => true],
|
||||
'CountStat' => ['type' => 'int', 'null' => true],
|
||||
'IndentLeft' => ['type' => 'int', 'null' => true, 'default' => 0],
|
||||
'FontStyle' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'VisibleScr' => ['type' => 'int', 'null' => true, 'default' => 1],
|
||||
'VisibleRpt' => ['type' => 'int', 'null' => true, 'default' => 1],
|
||||
'CountStat' => ['type' => 'int', 'null' => true, 'default' => 1],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'StartDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('TestSiteID', true);
|
||||
$this->forge->createTable('testdefsite');
|
||||
|
||||
// testdeftech - Technical definition for TEST and PARAM types
|
||||
$this->forge->addField([
|
||||
'TestTechID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'DisciplineID' => ['type' => 'int', 'null' => true],
|
||||
'DepartmentID' => ['type' => 'int', 'null' => true],
|
||||
'ResultType' => ['type' => 'int', 'null' => true],
|
||||
'RefType' => ['type' => 'int', 'null' => true],
|
||||
'ResultType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'RefType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||
'VSet' => ['type' => 'int', 'null' => true],
|
||||
'SpcType' => ['type' => 'int', 'null' => true],
|
||||
'ReqQty' => ['type' => 'int', 'null' => true],
|
||||
'ReqQtyUnit' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
||||
'Unit1' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
||||
'Factor' => ['type' => 'int', 'null' => true],
|
||||
'Unit2' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
||||
'Decimal' => ['type' => 'int', 'null' => true],
|
||||
'CollReq' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
||||
'Method' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
||||
'ReqQty' => ['type' => 'DECIMAL', 'constraint'=> '10,2', 'null' => true],
|
||||
'ReqQtyUnit' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Unit1' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Factor' => ['type' => 'DECIMAL', 'constraint'=> '10,4', 'null' => true],
|
||||
'Unit2' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Decimal' => ['type' => 'int', 'null' => true, 'default' => 2],
|
||||
'CollReq' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||
'Method' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'ExpectedTAT' => ['type' => 'INT', 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||
]);
|
||||
$this->forge->addKey('TestTechID', true);
|
||||
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||
$this->forge->createTable('testdeftech');
|
||||
|
||||
// testdefcal - Calculation definition for CALC type
|
||||
$this->forge->addField([
|
||||
'TestCalID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'DisciplineID' => ['type' => 'INT', 'null' => true],
|
||||
'DepartmentID' => ['type' => 'INT', 'null' => true],
|
||||
'FormulaInput' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
||||
'FormulaCode' => ['type' => 'varchar', 'constraint'=>150, 'null' => true],
|
||||
'Unit1' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
||||
'Factor' => ['type' => 'int', 'null' => true],
|
||||
'Unit2' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
||||
'Decimal' => ['type' => 'int', 'null' => true],
|
||||
'FormulaInput' => ['type' => 'text', 'null' => true],
|
||||
'FormulaCode' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||
'RefType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true, 'default' => 'NMRC'],
|
||||
'Unit1' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Factor' => ['type' => 'DECIMAL', 'constraint'=> '10,4', 'null' => true],
|
||||
'Unit2' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Decimal' => ['type' => 'int', 'null' => true, 'default' => 2],
|
||||
'Method' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||
]);
|
||||
$this->forge->addKey('TestCalID', true);
|
||||
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||
$this->forge->createTable('testdefcal');
|
||||
|
||||
// testdefgrp - Group definition for GROUP type
|
||||
$this->forge->addField([
|
||||
'TestGrpID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'Member' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'Member' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||
]);
|
||||
$this->forge->addKey('TestGrpID', true);
|
||||
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||
$this->forge->addForeignKey('Member', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||
$this->forge->createTable('testdefgrp');
|
||||
|
||||
// testmap - Test mapping for all types
|
||||
$this->forge->addField([
|
||||
'TestMapID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'HostType' => ['type' => 'int', 'null' => true],
|
||||
'HostID' => ['type' => 'int', 'null' => true],
|
||||
'HostDataSource' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
||||
'HostTestCode' => ['type' => 'varchar', 'constraint'=>10, 'null' => true],
|
||||
'HostTestName' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
||||
'ClientType' => ['type' => 'int', 'null' => true],
|
||||
'ClientID' => ['type' => 'int', 'null' => true],
|
||||
'ClientDataSource' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
||||
'ConDefID' => ['type' => 'int', 'null' => true],
|
||||
'ClientTestCode' => ['type' => 'varchar', 'constraint'=>10, 'null' => true],
|
||||
'ClientTestName' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'HostType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'HostID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'HostDataSource' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'HostTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||
'HostTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true],
|
||||
'ClientType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'ClientID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'ClientDataSource' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'ConDefID' => ['type' => 'INT', 'null' => true],
|
||||
'ClientTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||
'ClientTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||
]);
|
||||
$this->forge->addKey('TestMapID', true);
|
||||
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||
$this->forge->createTable('testmap');
|
||||
}
|
||||
|
||||
@ -107,4 +117,4 @@ class CreateTestsTable extends Migration {
|
||||
$this->forge->dropTable('testdefgrp');
|
||||
$this->forge->dropTable('testmap');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,67 +11,43 @@ class CreateRefRangesTable extends Migration {
|
||||
'RefNumID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'SpcType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => false],
|
||||
'SpcType' => ['type' => 'INT', 'null' => true],
|
||||
'Sex' => ['type' => 'INT', 'null' => true],
|
||||
'Criteria' => ['type' => 'varchar', 'constraint'=>100, 'null' => true],
|
||||
'AgeStart' => ['type' => 'INT', 'null' => true],
|
||||
'AgeEnd' => ['type' => 'int', 'null' => true],
|
||||
'CriticalLow' => ['type' => 'int', 'null' => true],
|
||||
'Low' => ['type' => 'int', 'null' => true],
|
||||
'High' => ['type' => 'int', 'null' => true],
|
||||
'CriticalHigh' => ['type' => 'int', 'null' => true],
|
||||
'NumRefType' => ['type' => 'INT', 'null' => true],
|
||||
'RangeType' => ['type' => 'INT', 'null' => true],
|
||||
'LowSign' => ['type' => 'INT', 'null' => true],
|
||||
'Low' => ['type' => 'INT', 'null' => true],
|
||||
'HighSign' => ['type' => 'INT', 'null' => true],
|
||||
'High' => ['type' => 'INT', 'null' => true],
|
||||
'Display' => ['type' => 'INT', 'null' => true],
|
||||
'Flag' => ['type' => 'varchar', 'constraint'=>10, 'null' => true],
|
||||
'Interpretation' => ['type' => 'varchar', 'constraint'=>255, 'null' => true],
|
||||
'Notes' => ['type' => 'varchar', 'constraint'=>255, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'StartDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('RefNumID', true);
|
||||
$this->forge->createTable('refnum');
|
||||
|
||||
$this->forge->addField([
|
||||
'RefTHoldID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'SpcType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => false],
|
||||
'Sex' => ['type' => 'INT', 'null' => true],
|
||||
'AgeStart' => ['type' => 'INT', 'null' => true],
|
||||
'AgeEnd' => ['type' => 'int', 'null' => true],
|
||||
'TholdSign' => ['type' => 'int', 'null' => true],
|
||||
'TholdValue' => ['type' => 'int', 'null' => true],
|
||||
'BelowTxt' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'AboveTxt' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'GrayZoneLow' => ['type' => 'int', 'null' => true],
|
||||
'GrayZoneHigh' => ['type' => 'int', 'null' => true],
|
||||
'GrayZoneTxt' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('RefTHoldID', true);
|
||||
$this->forge->createTable('refthold');
|
||||
|
||||
$this->forge->addField([
|
||||
'RefVSetID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'SpcType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => false],
|
||||
'Sex' => ['type' => 'INT', 'null' => true],
|
||||
'AgeStart' => ['type' => 'INT', 'null' => true],
|
||||
'AgeEnd' => ['type' => 'int', 'null' => true],
|
||||
'RefTxt' => ['type' => 'varchar', 'constraint'=>255, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('RefVSetID', true);
|
||||
$this->forge->createTable('refvset');
|
||||
|
||||
$this->forge->addField([
|
||||
'RefTxtID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'SpcType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => false],
|
||||
'Sex' => ['type' => 'INT', 'null' => true],
|
||||
'Criteria' => ['type' => 'varchar', 'constraint'=>100, 'null' => true],
|
||||
'AgeStart' => ['type' => 'INT', 'null' => true],
|
||||
'AgeEnd' => ['type' => 'int', 'null' => true],
|
||||
'TxtRefType' => ['type' => 'INT', 'null' => true],
|
||||
'RefTxt' => ['type' => 'varchar', 'constraint'=>255, 'null' => true],
|
||||
'Flag' => ['type' => 'varchar', 'constraint'=>10, 'null' => true],
|
||||
'Notes' => ['type' => 'varchar', 'constraint'=>255, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'StartDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('RefTxtID', true);
|
||||
@ -80,8 +56,6 @@ class CreateRefRangesTable extends Migration {
|
||||
|
||||
public function down() {
|
||||
$this->forge->dropTable('refnum');
|
||||
$this->forge->dropTable('refthold');
|
||||
$this->forge->dropTable('refvset');
|
||||
$this->forge->dropTable('reftxt');
|
||||
}
|
||||
}
|
||||
58
app/Database/Migrations/2025-12-29-150000_EdgeRes.php
Normal file
58
app/Database/Migrations/2025-12-29-150000_EdgeRes.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateEdgeResTables extends Migration {
|
||||
public function up() {
|
||||
// Main edgeres table - staging for instrument results
|
||||
$this->forge->addField([
|
||||
'EdgeResID' => ['type' => 'INT', 'auto_increment' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => true],
|
||||
'InstrumentID' => ['type' => 'varchar', 'constraint' => 100, 'null' => true],
|
||||
'SampleID' => ['type' => 'varchar', 'constraint' => 30, 'null' => true],
|
||||
'PatientID' => ['type' => 'varchar', 'constraint' => 50, 'null' => true],
|
||||
'Payload' => ['type' => 'TEXT', 'null' => true],
|
||||
'Status' => ['type' => 'varchar', 'constraint' => 20, 'default' => 'pending'],
|
||||
'AutoProcess' => ['type' => 'TINYINT', 'default' => 0, 'null' => true],
|
||||
'ProcessedAt' => ['type' => 'DATETIME', 'null' => true],
|
||||
'ErrorMessage' => ['type' => 'TEXT', 'null' => true],
|
||||
'CreateDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'EndDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'ArchiveDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'DelDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('EdgeResID');
|
||||
$this->forge->createTable('edgeres');
|
||||
|
||||
// Edge status log - for instrument status tracking
|
||||
$this->forge->addField([
|
||||
'EdgeStatusID' => ['type' => 'INT', 'auto_increment' => true],
|
||||
'InstrumentID' => ['type' => 'varchar', 'constraint' => 100, 'null' => true],
|
||||
'Status' => ['type' => 'varchar', 'constraint' => 50, 'null' => true],
|
||||
'LastActivity' => ['type' => 'DATETIME', 'null' => true],
|
||||
'Timestamp' => ['type' => 'DATETIME', 'null' => true],
|
||||
'CreateDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('EdgeStatusID');
|
||||
$this->forge->createTable('edgestatus');
|
||||
|
||||
// Edge order acknowledgment log
|
||||
$this->forge->addField([
|
||||
'EdgeAckID' => ['type' => 'INT', 'auto_increment' => true],
|
||||
'OrderID' => ['type' => 'INT', 'null' => true],
|
||||
'InstrumentID' => ['type' => 'varchar', 'constraint' => 100, 'null' => true],
|
||||
'AckDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
'CreateDate' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('EdgeAckID');
|
||||
$this->forge->createTable('edgeack');
|
||||
}
|
||||
|
||||
public function down() {
|
||||
$this->forge->dropTable('edgeack', true);
|
||||
$this->forge->dropTable('edgestatus', true);
|
||||
$this->forge->dropTable('edgeres', true);
|
||||
}
|
||||
}
|
||||
@ -9,10 +9,12 @@ class DummySeeder extends Seeder {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// users
|
||||
// Password: 'password' for all users (bcrypt hash)
|
||||
$passwordHash = password_hash('password', PASSWORD_BCRYPT);
|
||||
$data = [
|
||||
['id' => 1, 'role_id' => 1, 'username' => 'zaka', 'password' => '$2y$12$vSB7PpKOUKEyFKbeExiGkuujRfQbR.yl6YVudDpfy24FemZopBG0m'],
|
||||
['id' => 2, 'role_id' => 1, 'username' => 'tes' , 'password' => '$2y$12$KwPedIPb7K/0IR/8/FcwdOMG4eBNNAXSjXnbkB26SwjH4Nf7PaYBe'],
|
||||
['id' => 3, 'role_id' => 1, 'username' => 'tes2', 'password' => '$2y$12$vSB7PpKOUKEyFKbeExiGkuujRfQbR.yl6YVudDpfy24FemZopBG0m'],
|
||||
['id' => 1, 'role_id' => 1, 'username' => 'zaka', 'password' => $passwordHash],
|
||||
['id' => 2, 'role_id' => 1, 'username' => 'tes' , 'password' => $passwordHash],
|
||||
['id' => 3, 'role_id' => 1, 'username' => 'tes2', 'password' => $passwordHash],
|
||||
];
|
||||
$this->db->table('users')->insertBatch($data);
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\Database\Seeder;
|
||||
use App\Models\ValueSet\ValueSetModel;
|
||||
|
||||
class TestSeeder extends Seeder {
|
||||
|
||||
|
||||
public function run() {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$vsModel = new ValueSetModel();
|
||||
@ -25,104 +25,104 @@ class TestSeeder extends Seeder {
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'HB', 'TestSiteName' => 'Hemoglobin', 'TestType' => $vs[27]['TEST'], 'Description' => '', 'SeqScr' => '2', 'SeqRpt' => '2', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HB'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['HB'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HB'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'HCT', 'TestSiteName' => 'Hematocrit', 'TestType' => $vs[27]['TEST'], 'Description' => '', 'SeqScr' => '3', 'SeqRpt' => '3', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HCT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['HCT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => '%', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HCT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => '%', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'RBC', 'TestSiteName' => 'Red Blood Cell', 'TestType' => $vs[27]['TEST'], 'Description' => 'Eritrosit', 'SeqScr' => '4', 'SeqRpt' => '4', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['RBC'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['RBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^6/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['RBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^6/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'WBC', 'TestSiteName' => 'White Blood Cell', 'TestType' => $vs[27]['TEST'], 'Description' => 'Leukosit', 'SeqScr' => '5', 'SeqRpt' => '5', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['WBC'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['WBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['WBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'PLT', 'TestSiteName' => 'Platelet', 'TestType' => $vs[27]['TEST'], 'Description' => 'Trombosit', 'SeqScr' => '6', 'SeqRpt' => '6', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['PLT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['PLT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['PLT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'MCV', 'TestSiteName' => 'MCV', 'TestType' => $vs[27]['TEST'], 'Description' => 'Mean Corpuscular Volume', 'SeqScr' => '7', 'SeqRpt' => '7', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['MCV'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['MCV'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'fL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['MCV'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'fL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'MCH', 'TestSiteName' => 'MCH', 'TestType' => $vs[27]['TEST'], 'Description' => 'Mean Corpuscular Hemoglobin', 'SeqScr' => '8', 'SeqRpt' => '8', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['MCH'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['MCH'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'pg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['MCH'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'pg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'MCHC', 'TestSiteName' => 'MCHC', 'TestType' => $vs[27]['TEST'], 'Description' => 'Mean Corpuscular Hemoglobin Concentration', 'SeqScr' => '9', 'SeqRpt' => '9', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['MCHC'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['MCHC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['MCHC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
// Chemistry Tests
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'GLU', 'TestSiteName' => 'Glucose', 'TestType' => $vs[27]['TEST'], 'Description' => 'Glukosa Sewaktu', 'SeqScr' => '11', 'SeqRpt' => '11', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['GLU'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['GLU'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '0.0555', 'Unit2' => 'mmol/L', 'Decimal' => '0', 'Method' => 'Hexokinase', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['GLU'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '0.0555', 'Unit2' => 'mmol/L', 'Decimal' => '0', 'Method' => 'Hexokinase', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'CREA', 'TestSiteName' => 'Creatinine', 'TestType' => $vs[27]['TEST'], 'Description' => 'Kreatinin', 'SeqScr' => '12', 'SeqRpt' => '12', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['CREA'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['CREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '88.4', 'Unit2' => 'umol/L', 'Decimal' => '2', 'Method' => 'Enzymatic', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['CREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '88.4', 'Unit2' => 'umol/L', 'Decimal' => '2', 'Method' => 'Enzymatic', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'UREA', 'TestSiteName' => 'Blood Urea Nitrogen', 'TestType' => $vs[27]['TEST'], 'Description' => 'BUN', 'SeqScr' => '13', 'SeqRpt' => '13', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UREA'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['UREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Urease-GLDH', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Urease-GLDH', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'SGOT', 'TestSiteName' => 'AST (SGOT)', 'TestType' => $vs[27]['TEST'], 'Description' => 'Aspartate Aminotransferase', 'SeqScr' => '14', 'SeqRpt' => '14', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SGOT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['SGOT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['SGOT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'SGPT', 'TestSiteName' => 'ALT (SGPT)', 'TestType' => $vs[27]['TEST'], 'Description' => 'Alanine Aminotransferase', 'SeqScr' => '15', 'SeqRpt' => '15', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SGPT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['SGPT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['SGPT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'CHOL', 'TestSiteName' => 'Total Cholesterol', 'TestType' => $vs[27]['TEST'], 'Description' => 'Kolesterol Total', 'SeqScr' => '16', 'SeqRpt' => '16', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['CHOL'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['CHOL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['THOLD'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Enzymatic', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['CHOL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Enzymatic', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'TG', 'TestSiteName' => 'Triglycerides', 'TestType' => $vs[27]['TEST'], 'Description' => 'Trigliserida', 'SeqScr' => '17', 'SeqRpt' => '17', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['TG'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['TG'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['THOLD'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'GPO-PAP', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['TG'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'GPO-PAP', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'HDL', 'TestSiteName' => 'HDL Cholesterol', 'TestType' => $vs[27]['TEST'], 'Description' => 'Kolesterol HDL', 'SeqScr' => '18', 'SeqRpt' => '18', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HDL'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['HDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['THOLD'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'LDL', 'TestSiteName' => 'LDL Cholesterol', 'TestType' => $vs[27]['TEST'], 'Description' => 'Kolesterol LDL', 'SeqScr' => '19', 'SeqRpt' => '19', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['LDL'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['LDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['THOLD'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['LDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
// ========================================
|
||||
@ -131,31 +131,31 @@ class TestSeeder extends Seeder {
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'HEIGHT', 'TestSiteName' => 'Height', 'TestType' => $vs[27]['PARAM'], 'Description' => 'Tinggi Badan', 'SeqScr' => '40', 'SeqRpt' => '40', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HEIGHT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['HEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'cm', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'cm', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'WEIGHT', 'TestSiteName' => 'Weight', 'TestType' => $vs[27]['PARAM'], 'Description' => 'Berat Badan', 'SeqScr' => '41', 'SeqRpt' => '41', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['WEIGHT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['WEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'kg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['WEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'kg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'AGE', 'TestSiteName' => 'Age', 'TestType' => $vs[27]['PARAM'], 'Description' => 'Usia', 'SeqScr' => '42', 'SeqRpt' => '42', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['AGE'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['AGE'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['AGE'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'SYSTL', 'TestSiteName' => 'Systolic BP', 'TestType' => $vs[27]['PARAM'], 'Description' => 'Tekanan Darah Sistolik', 'SeqScr' => '43', 'SeqRpt' => '43', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SYSTL'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['SYSTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['SYSTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'DIASTL', 'TestSiteName' => 'Diastolic BP', 'TestType' => $vs[27]['PARAM'], 'Description' => 'Tekanan Darah Diastolik', 'SeqScr' => '44', 'SeqRpt' => '44', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['DIASTL'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['DIASTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['DIASTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
// ========================================
|
||||
@ -164,19 +164,19 @@ class TestSeeder extends Seeder {
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'BMI', 'TestSiteName' => 'Body Mass Index', 'TestType' => $vs[27]['CALC'], 'Description' => 'Indeks Massa Tubuh - weight/(height^2)', 'SeqScr' => '45', 'SeqRpt' => '45', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['BMI'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['BMI'], 'DisciplineID' => '10', 'DepartmentID' => '', 'FormulaInput' => 'WEIGHT,HEIGHT', 'FormulaCode' => 'WEIGHT / ((HEIGHT/100) * (HEIGHT/100))', 'Unit1' => 'kg/m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['BMI'], 'DisciplineID' => '10', 'DepartmentID' => '', 'FormulaInput' => 'WEIGHT,HEIGHT', 'FormulaCode' => 'WEIGHT / ((HEIGHT/100) * (HEIGHT/100))', 'Unit1' => 'kg/m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'EGFR', 'TestSiteName' => 'eGFR (CKD-EPI)', 'TestType' => $vs[27]['CALC'], 'Description' => 'Estimated Glomerular Filtration Rate', 'SeqScr' => '20', 'SeqRpt' => '20', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['EGFR'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['EGFR'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaInput' => 'CREA,AGE,GENDER', 'FormulaCode' => 'CKD_EPI(CREA,AGE,GENDER)', 'Unit1' => 'mL/min/1.73m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['EGFR'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaInput' => 'CREA,AGE,GENDER', 'FormulaCode' => 'CKD_EPI(CREA,AGE,GENDER)', 'Unit1' => 'mL/min/1.73m2', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'LDLCALC', 'TestSiteName' => 'LDL Cholesterol (Calculated)', 'TestType' => $vs[27]['CALC'], 'Description' => 'Friedewald formula: TC - HDL - (TG/5)', 'SeqScr' => '21', 'SeqRpt' => '21', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['LDLCALC'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['LDLCALC'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaInput' => 'CHOL,HDL,TG', 'FormulaCode' => 'CHOL - HDL - (TG/5)', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['LDLCALC'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'FormulaInput' => 'CHOL,HDL,TG', 'FormulaCode' => 'CHOL - HDL - (TG/5)', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefcal')->insert($data);
|
||||
|
||||
// ========================================
|
||||
@ -186,68 +186,68 @@ class TestSeeder extends Seeder {
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['CBC'] = $this->db->insertID();
|
||||
$this->db->table('testdefgrp')->insertBatch([
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['HB'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['HCT'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['RBC'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['WBC'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['PLT'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['MCV'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['MCH'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['MCHC'], 'CreateDate' => "$now"]
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['HB'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['HCT'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['RBC'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['WBC'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['PLT'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['MCV'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['MCH'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['CBC'], 'Member' => $tIDs['MCHC'], 'CreateDate' => "$now"]
|
||||
]);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'LIPID', 'TestSiteName' => 'Lipid Profile', 'TestType' => $vs[27]['GROUP'], 'Description' => 'Profil Lipid', 'SeqScr' => '51', 'SeqRpt' => '51', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['LIPID'] = $this->db->insertID();
|
||||
$this->db->table('testdefgrp')->insertBatch([
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['CHOL'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['TG'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['HDL'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['LDL'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['LDLCALC'], 'CreateDate' => "$now"]
|
||||
['TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['CHOL'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['TG'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['HDL'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['LDL'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['LIPID'], 'Member' => $tIDs['LDLCALC'], 'CreateDate' => "$now"]
|
||||
]);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'LFT', 'TestSiteName' => 'Liver Function Test', 'TestType' => $vs[27]['GROUP'], 'Description' => 'Fungsi Hati', 'SeqScr' => '52', 'SeqRpt' => '52', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['LFT'] = $this->db->insertID();
|
||||
$this->db->table('testdefgrp')->insertBatch([
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LFT'], 'Member' => $tIDs['SGOT'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['LFT'], 'Member' => $tIDs['SGPT'], 'CreateDate' => "$now"]
|
||||
['TestSiteID' => $tIDs['LFT'], 'Member' => $tIDs['SGOT'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['LFT'], 'Member' => $tIDs['SGPT'], 'CreateDate' => "$now"]
|
||||
]);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'RFT', 'TestSiteName' => 'Renal Function Test', 'TestType' => $vs[27]['GROUP'], 'Description' => 'Fungsi Ginjal', 'SeqScr' => '53', 'SeqRpt' => '53', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['RFT'] = $this->db->insertID();
|
||||
$this->db->table('testdefgrp')->insertBatch([
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['RFT'], 'Member' => $tIDs['UREA'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['RFT'], 'Member' => $tIDs['CREA'], 'CreateDate' => "$now"],
|
||||
['SiteID' => '1', 'TestSiteID' => $tIDs['RFT'], 'Member' => $tIDs['EGFR'], 'CreateDate' => "$now"]
|
||||
['TestSiteID' => $tIDs['RFT'], 'Member' => $tIDs['UREA'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['RFT'], 'Member' => $tIDs['CREA'], 'CreateDate' => "$now"],
|
||||
['TestSiteID' => $tIDs['RFT'], 'Member' => $tIDs['EGFR'], 'CreateDate' => "$now"]
|
||||
]);
|
||||
|
||||
// Urinalysis Tests (with valueset result type)
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'UCOLOR', 'TestSiteName' => 'Urine Color', 'TestType' => $vs[27]['TEST'], 'Description' => 'Warna Urine', 'SeqScr' => '31', 'SeqRpt' => '31', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UCOLOR'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['UCOLOR'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['VSET'], 'VSet' => '1001', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Visual', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UCOLOR'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['TEXT'], 'VSet' => '1001', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Visual', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'UGLUC', 'TestSiteName' => 'Urine Glucose', 'TestType' => $vs[27]['TEST'], 'Description' => 'Glukosa Urine', 'SeqScr' => '32', 'SeqRpt' => '32', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UGLUC'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['UGLUC'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['VSET'], 'VSet' => '1002', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UGLUC'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['TEXT'], 'VSet' => '1002', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'UPROT', 'TestSiteName' => 'Urine Protein', 'TestType' => $vs[27]['TEST'], 'Description' => 'Protein Urine', 'SeqScr' => '33', 'SeqRpt' => '33', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UPROT'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['UPROT'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['VSET'], 'VSet' => '1003', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UPROT'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['TEXT'], 'VSet' => '1003', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'PH', 'TestSiteName' => 'Urine pH', 'TestType' => $vs[27]['TEST'], 'Description' => 'pH Urine', 'SeqScr' => '34', 'SeqRpt' => '34', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['PH'] = $this->db->insertID();
|
||||
$data = ['SiteID' => '1', 'TestSiteID' => $tIDs['PH'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['RANGE'], 'VSet' => '', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['PH'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ class ValueSetSeeder extends Seeder {
|
||||
['VSetID' => 15,'VOrder' => 2, 'VValue' =>'BLDA', 'VDesc' => "Blood arterial", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 3, 'VValue' =>'BLDCO', 'VDesc' => "Cord blood", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 4, 'VValue' =>'FBLOOD', 'VDesc' => "Blood, Fetal", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 5, 'VValue' =>'FBLOOD', 'VDesc' => "Blood, Fetal", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 5, 'VValue' =>'CSF', 'VDesc' => "Cerebral spinal fluid", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 6, 'VValue' =>'WB', 'VDesc' => "Blood, Whole", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 7, 'VValue' =>'BBL', 'VDesc' => "Blood bag", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 15,'VOrder' => 8, 'VValue' =>'SER', 'VDesc' => "Serum", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
@ -306,33 +306,46 @@ class ValueSetSeeder extends Seeder {
|
||||
['VSetID' => 43,'VOrder' => 2, 'VValue' =>'RANGE', 'VDesc' => "Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 43,'VOrder' => 3, 'VValue' =>'TEXT', 'VDesc' => "Text", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 43,'VOrder' => 4, 'VValue' =>'VSET', 'VDesc' => "Value set", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 44,'VOrder' => 1, 'VValue' =>'RANGE', 'VDesc' => "Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 44,'VOrder' => 2, 'VValue' =>'THOLD', 'VDesc' => "Threshold", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 44,'VOrder' => 3, 'VValue' =>'VSET', 'VDesc' => "Value Set", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 44,'VOrder' => 4, 'VValue' =>'TEXT', 'VDesc' => "Text.", 'VCategory' => 1, 'CreateDate' => "$now"]
|
||||
['VSetID' => 44,'VOrder' => 1, 'VValue' =>'NMRC', 'VDesc' => "Numeric", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 44,'VOrder' => 2, 'VValue' =>'TEXT', 'VDesc' => "Text", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 45,'VOrder' => 1, 'VValue' =>'REF', 'VDesc' => "Reference Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 45,'VOrder' => 2, 'VValue' =>'CRTC', 'VDesc' => "Critical Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 45,'VOrder' => 3, 'VValue' =>'VAL', 'VDesc' => "Validation Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 45,'VOrder' => 4, 'VValue' =>'RERUN', 'VDesc' => "Rerun Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 46,'VOrder' => 1, 'VValue' =>'RANGE', 'VDesc' => "Range", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 46,'VOrder' => 2, 'VValue' =>'THOLD', 'VDesc' => "Threshold", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 47,'VOrder' => 1, 'VValue' =>'VSET', 'VDesc' => "Value Set", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 47,'VOrder' => 2, 'VValue' =>'TEXT', 'VDesc' => "Text.", 'VCategory' => 1, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1001,'VOrder' => 1, 'VValue' =>'NEG', 'VDesc' => "Negative", 'VCategory' => 2, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1001,'VOrder' => 2, 'VValue' =>'POS', 'VDesc' => "Positive", 'VCategory' => 2, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1001,'VOrder' => 3, 'VValue' =>'GZ', 'VDesc' => "Grayzone", 'VCategory' => 2, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1002,'VOrder' => 1, 'VValue' =>'KNG', 'VDesc' => "Kuning", 'VCategory' => 2, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1002,'VOrder' => 2, 'VValue' =>'JNG', 'VDesc' => "Jingga", 'VCategory' => 2, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1002,'VOrder' => 3, 'VValue' =>'MRH', 'VDesc' => "Merah", 'VCategory' => 2, 'CreateDate' => "$now"],
|
||||
['VSetID' => 1002,'VOrder' => 4, 'VValue' =>'CKLT', 'VDesc' => "Coklat tua", 'VCategory' => 2, 'CreateDate' => "$now"]
|
||||
];
|
||||
$this->db->table('valueset')->insertBatch($data);
|
||||
|
||||
$data = [
|
||||
['VSName' => 'WSType','VSDesc' =>'workstation.Type', 'VSetID' => '1', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Enable/Disable','VSDesc' =>'workstation.Enable equipmentlist.Enable testdef.CountStat testdefsite.CountStat testdefsite.VisibleScr testdefsite.VisibleRpt', 'VSetID' => '2', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Gender','VSDesc' =>'patient.Gender', 'VSetID' => '3', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Enable/Disable','VSDesc' =>'workstation.Enable, equipmentlist.Enable, testdef.CountStat, testdefsite.CountStat, testdefsite.VisibleScr, testdefsite.VisibleRpt', 'VSetID' => '2', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Gender','VSDesc' =>'patient.Gender, refnum.Sex', 'VSetID' => '3', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Marital Status','VSDesc' =>'patient.MaritalStatus', 'VSetID' => '4', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Deceased','VSDesc' =>'patient.Deceased', 'VSetID' => '5', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Death Indicator','VSDesc' =>'patient.DeathIndicator', 'VSetID' => '5', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Identifier Type','VSDesc' =>'patidt.IdentifierType', 'VSetID' => '6', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Operation','VSDesc' =>'patreglog.Operation patvisitlog.Operation orderlog.Operation', 'VSetID' => '7', 'CreateDate' => "$now"],
|
||||
['VSName' => 'DID Type','VSDesc' =>'patreglog.DIDType patvisitlog.DIDType', 'VSetID' => '8', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Operation','VSDesc' =>'patreglog.Operation, patvisitlog.Operation, orderlog.Operation', 'VSetID' => '7', 'CreateDate' => "$now"],
|
||||
['VSName' => 'DID Type','VSDesc' =>'patreglog.DIDType, patvisitlog.DIDType', 'VSetID' => '8', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Requested Entity','VSDesc' =>'order.ReqEntity', 'VSetID' => '9', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Order Priority','VSDesc' =>'order.Priority', 'VSetID' => '10', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Order Status','VSDesc' =>'orderststatus.OrderStatus', 'VSetID' => '11', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Location Type','VSDesc' =>'location.LocationType', 'VSetID' => '12', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Additive','VSDesc' =>'containertype.Additive specimenprep.Additive', 'VSetID' => '13', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Location TypeTable 34 location','VSDesc' =>'location.LocationType', 'VSetID' => '12', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Additive','VSDesc' =>'containertype.Additive, specimenprep.Additive', 'VSetID' => '13', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Container Class','VSDesc' =>'containertype.ConClass', 'VSetID' => '14', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Specimen Type','VSDesc' =>'testdeftech.SpcType refnum. SpcType refthold.SpcType', 'VSetID' => '15', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Unit','VSDesc' =>'spcdef.Unit specimens.Unit specimenstatus.Unit specimenprep.AddUnit', 'VSetID' => '16', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Specimen Type','VSDesc' =>'testdeftech.SpcType, refnum.SpcType, reftxt.SpcType', 'VSetID' => '15', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Unit','VSDesc' =>'spcdef.Unit, specimens.Unit, specimenstatus.Unit, specimenprep.AddUnit', 'VSetID' => '16', 'CreateDate' => "$now"],
|
||||
['VSName' => 'GenerateBy','VSDesc' =>'specimens. GenerateBy', 'VSetID' => '17', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Specimen Activity','VSDesc' =>'specimenstatus.SpcAct', 'VSetID' => '18', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Activity Result','VSDesc' =>'specimenstatus.ActRes patrestatus.ActRes', 'VSetID' => '19', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Activity Result','VSDesc' =>'specimenstatus.ActRes, patrestatus.ActRes', 'VSetID' => '19', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Specimen Status','VSDesc' =>'specimenstatus.SpcStatus', 'VSetID' => '20', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Specimen Condition','VSDesc' =>'specimenstatus.SpcCon', 'VSetID' => '21', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Specimen Role','VSDesc' =>'specimencollection.SpcRole', 'VSetID' => '22', 'CreateDate' => "$now"],
|
||||
@ -340,8 +353,8 @@ class ValueSetSeeder extends Seeder {
|
||||
['VSName' => 'Body Site','VSDesc' =>'specimencollection.BodySite', 'VSetID' => '24', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Container Size','VSDesc' =>'specimencollection.CntSize', 'VSetID' => '25', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Fasting Status','VSDesc' =>'specimencollection.Fasting', 'VSetID' => '26', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Test Type','VSDesc' =>'testdefsite.Type', 'VSetID' => '27', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Result Unit','VSDesc' =>'testdefsite.Unit1 testdefsite.Unit2', 'VSetID' => '28', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Test Type','VSDesc' =>'testdefsite.TestType', 'VSetID' => '27', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Result Unit','VSDesc' =>'testdefsite.Unit1, testdefsite.Unit2', 'VSetID' => '28', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Formula Languange','VSDesc' =>'testdefcal.FormulaLang', 'VSetID' => '29', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Race','VSDesc' =>'patient.Race', 'VSetID' => '30', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Religion','VSDesc' =>'patient.Religion', 'VSetID' => '31', 'CreateDate' => "$now"],
|
||||
@ -352,12 +365,17 @@ class ValueSetSeeder extends Seeder {
|
||||
['VSName' => 'ADT Event','VSDesc' =>'patvisitadt.Code', 'VSetID' => '36', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Site Type','VSDesc' =>'Site.SiteType', 'VSetID' => '37', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Site Class','VSDesc' =>'Site.SiteClass', 'VSetID' => '38', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Entity Type','VSDesc' =>'testmap.HostType testmap.ClientType', 'VSetID' => '39', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Entity Type','VSDesc' =>'testmap.HostType, testmap.ClientType', 'VSetID' => '39', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Area Class','VSDesc' =>'AreaGeo', 'VSetID' => '40', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Math Sign','VSDesc' =>'refthold.TholdSign', 'VSetID' => '41', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Math Sign','VSDesc' =>'refnum.LowSign, refnum.HighSign', 'VSetID' => '41', 'CreateDate' => "$now"],
|
||||
['VSName' => 'VCategory','VSDesc' =>'valueset. VCategory', 'VSetID' => '42', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Result Type','VSDesc' =>'testdeftech.ResultType', 'VSetID' => '43', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Reference Type','VSDesc' =>'testdeftech.RefType', 'VSetID' => '44', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Range Type','VSDesc' =>'refnum.RangeType', 'VSetID' => '45', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Numeric Reference Type','VSDesc' =>'refnum.NumRefType', 'VSetID' => '46', 'CreateDate' => "$now"],
|
||||
['VSName' => 'Text Reference Type','VSDesc' =>'reftxt. TxtRefType', 'VSetID' => '47', 'CreateDate' => "$now"],
|
||||
['VSName' => 'HIV','VSDesc' =>'Value set untuk hasil HIV', 'VSetID' => '1001', 'CreateDate' => "$now"],
|
||||
|
||||
];
|
||||
$this->db->table('valuesetdef')->insertBatch($data);
|
||||
}
|
||||
|
||||
@ -16,14 +16,22 @@ class AuthFilter implements FilterInterface
|
||||
$key = getenv('JWT_SECRET');
|
||||
$token = $request->getCookie('token'); // ambil dari cookie
|
||||
|
||||
// Check if this is an API request or a page request
|
||||
$isApiRequest = strpos($request->getUri()->getPath(), '/api/') !== false
|
||||
|| $request->isAJAX();
|
||||
|
||||
// Kalau tidak ada token
|
||||
if (!$token) {
|
||||
return Services::response()
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'status' => 'failed',
|
||||
'message' => 'Unauthorized: Token not found'
|
||||
]);
|
||||
if ($isApiRequest) {
|
||||
return Services::response()
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'status' => 'failed',
|
||||
'message' => 'Unauthorized: Token not found'
|
||||
]);
|
||||
}
|
||||
// Redirect to login for page requests
|
||||
return redirect()->to('/v2/login');
|
||||
}
|
||||
|
||||
try {
|
||||
@ -36,12 +44,16 @@ class AuthFilter implements FilterInterface
|
||||
// $request->userData = $decoded;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return Services::response()
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'status' => 'failed',
|
||||
'message' => 'Unauthorized: ' . $e->getMessage()
|
||||
]);
|
||||
if ($isApiRequest) {
|
||||
return Services::response()
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'status' => 'failed',
|
||||
'message' => 'Unauthorized: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
// Redirect to login for page requests
|
||||
return redirect()->to('/v2/login');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ class Cors implements FilterInterface
|
||||
{
|
||||
protected $allowedOrigins = [
|
||||
'http://localhost:5173',
|
||||
'http://localhost',
|
||||
'https://clqms01.services-summit.my.id',
|
||||
];
|
||||
|
||||
@ -19,6 +20,11 @@ class Cors implements FilterInterface
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
$response = service('response');
|
||||
|
||||
// Allow same-origin requests (when no Origin header is present)
|
||||
if (empty($origin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($origin, $this->allowedOrigins)) {
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origin);
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
|
||||
63
app/Models/EdgeResModel.php
Normal file
63
app/Models/EdgeResModel.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EdgeResModel extends Model {
|
||||
protected $table = 'edgeres';
|
||||
protected $primaryKey = 'EdgeResID';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'InstrumentID',
|
||||
'SampleID',
|
||||
'PatientID',
|
||||
'Payload',
|
||||
'Status',
|
||||
'AutoProcess',
|
||||
'ProcessedAt',
|
||||
'ErrorMessage',
|
||||
'CreateDate',
|
||||
'EndDate',
|
||||
'ArchiveDate',
|
||||
'DelDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = 'EndDate';
|
||||
|
||||
/**
|
||||
* Get pending results for processing
|
||||
*/
|
||||
public function getPending($limit = 100) {
|
||||
return $this->where('Status', 'pending')
|
||||
->whereNull('DelDate')
|
||||
->orderBy('CreateDate', 'ASC')
|
||||
->findAll($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as processed
|
||||
*/
|
||||
public function markProcessed($id) {
|
||||
return $this->update($id, [
|
||||
'Status' => 'processed',
|
||||
'ProcessedAt' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as error
|
||||
*/
|
||||
public function markError($id, $errorMessage) {
|
||||
return $this->update($id, [
|
||||
'Status' => 'error',
|
||||
'ErrorMessage' => $errorMessage,
|
||||
'ProcessedAt' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,8 @@ use App\Models\BaseModel;
|
||||
class LocationAddressModel extends BaseModel {
|
||||
protected $table = 'locationaddress';
|
||||
protected $primaryKey = 'LocationID';
|
||||
protected $allowedFields = ['LocationID', 'Street1', 'Street2', 'City', 'Province', 'PostCode', 'GeoLocationSystem', 'GeoLocationData', 'CreateDate', 'EndDate'];
|
||||
protected $allowedFields = ['LocationID', 'Street1', 'Street2', 'City', 'Province', 'PostCode',
|
||||
'GeoLocationSystem', 'GeoLocationData', 'Phone', 'Email', 'CreateDate', 'EndDate'];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
|
||||
@ -24,15 +24,15 @@ class LocationModel extends BaseModel {
|
||||
|
||||
public function getLocation($LocationID) {
|
||||
//'Street1', 'Street2', 'City', 'Province', 'PostCode', 'GeoLocationSystem', 'GeoLocationData',
|
||||
$rows = $this->select("location.*, la.Street1, la.Street2, la.PostCode, la.GeoLocationSystem, la.GeoLocationData,
|
||||
$row = $this->select("location.*, la.Street1, la.Street2, la.PostCode, la.GeoLocationSystem, la.GeoLocationData,
|
||||
v.VDesc as LocTypeText, prop.AreaGeoID as ProvinceID, prop.AreaName as Province, city.AreaGeoID as CityID, city.AreaName as City, site.SiteID, site.SiteName")
|
||||
->join("locationaddress la", "location.LocationID=la.LocationID", "left")
|
||||
->join("valueset v", "v.VID=location.loctype", "left")
|
||||
->join("areageo prop", "la.Province=prop.AreaGeoID", "left")
|
||||
->join("areageo city", "la.City=city.AreaGeoID", "left")
|
||||
->join("site", "site.SiteID=location.SiteID", "left")
|
||||
->where('location.LocationID', (int) $LocationID)->findAll();
|
||||
return $rows;
|
||||
->where('location.LocationID', (int) $LocationID)->first();
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function saveLocation(array $data): array {
|
||||
@ -50,14 +50,15 @@ class LocationModel extends BaseModel {
|
||||
$modelAddress->insert($data);
|
||||
}
|
||||
if ($db->transStatus() === false) {
|
||||
$error = $db->error();
|
||||
$db->transRollback();
|
||||
throw new \Exception('Transaction failed');
|
||||
throw new \Exception($error['message'] ?? 'Transaction failed');
|
||||
}
|
||||
$db->transCommit();
|
||||
return [ 'status' => 'success', 'LocationID' => $LocationID ];
|
||||
} catch (\Throwable $e) {
|
||||
$db->transRollback();
|
||||
return [ 'status' => 'error', 'message' => $e->getMessage() ];
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,14 +71,15 @@ class LocationModel extends BaseModel {
|
||||
$this->delete($LocationID);
|
||||
$modelAddress->delete($LocationID);
|
||||
if ($db->transStatus() === false) {
|
||||
$error = $db->error();
|
||||
$db->transRollback();
|
||||
throw new \Exception('Transaction failed');
|
||||
throw new \Exception($error['message'] ?? 'Transaction failed');
|
||||
}
|
||||
$db->transCommit();
|
||||
return [ 'status' => 'success', 'LocationID' => $LocationID ];
|
||||
} catch (\Throwable $e) {
|
||||
$db->transRollback();
|
||||
return [ 'status' => 'error', 'message' => $e->getMessage() ];
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ class AccountModel extends BaseModel {
|
||||
}
|
||||
|
||||
public function getAccount($AccountID) {
|
||||
$rows = $this->select('account.*, pa.AccountName as ParentName, areageo.AreaName, areageo.AreaGeoID,
|
||||
$row = $this->select('account.*, pa.AccountName as ParentName, areageo.AreaName, areageo.AreaGeoID,
|
||||
city.AreaName as CityName, city.AreaGeoID as City, prov.AreaName as ProvName, prov.AreaGeoID as Prov,
|
||||
country.VValue as CountryName, country.VID as country')
|
||||
->join('account pa', 'pa.AccountID=account.Parent', 'left')
|
||||
@ -40,7 +40,7 @@ class AccountModel extends BaseModel {
|
||||
->join('areageo prov', 'prov.AreaGeoID=account.Province', 'left')
|
||||
->join('valueset country', 'country.VID=account.Country', 'left')
|
||||
->where('account.AccountID', $AccountID)
|
||||
->findAll();
|
||||
return $rows;
|
||||
->first();
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,11 +30,11 @@ class DepartmentModel extends BaseModel {
|
||||
}
|
||||
|
||||
public function getDepartment($DepartmentID) {
|
||||
$rows = $this->select('department.*, discipline.DisciplineCode, discipline.DisciplineName, site.SiteCode, site.SiteName')
|
||||
$row = $this->select('department.*, discipline.DisciplineCode, discipline.DisciplineName, site.SiteCode, site.SiteName')
|
||||
->join('discipline', 'discipline.DisciplineID=department.DisciplineID', 'left')
|
||||
->join('site', 'site.SiteID=department.SiteID', 'left')
|
||||
->where('department.DepartmentID', $DepartmentID)
|
||||
->findAll();
|
||||
return $rows;
|
||||
->first();
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,13 +32,13 @@ class SiteModel extends BaseModel {
|
||||
}
|
||||
|
||||
public function getSite($SiteID) {
|
||||
$rows = $this->select('site.*, account.AccountName, s1.SiteName as ParentName, sitetype.VValue as SiteType, siteclass.VValue as SiteClass')
|
||||
$row = $this->select('site.*, account.AccountName, s1.SiteName as ParentName, sitetype.VValue as SiteType, siteclass.VValue as SiteClass')
|
||||
->join('account', 'account.AccountID=site.AccountID', 'left')
|
||||
->join('site s1', 's1.SiteID=site.Parent', 'left')
|
||||
->join('valueset sitetype', 'site.SiteTypeID=sitetype.VID', 'left')
|
||||
->join('valueset siteclass', 'site.SiteClassID=siteclass.VID', 'left')
|
||||
->where('site.SiteID', $SiteID)
|
||||
->findAll();
|
||||
return $rows;
|
||||
->first();
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,13 +30,13 @@ class WorkstationModel extends BaseModel {
|
||||
}
|
||||
|
||||
public function getWorkstation($WorkstationID) {
|
||||
$rows = $this->select("workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName,enable.VDesc as EnableName, wstype.VDesc as TypeName")
|
||||
$row = $this->select("workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName,enable.VDesc as EnableName, wstype.VDesc as TypeName")
|
||||
->join('workstation wst1', 'workstation.LinkTo=wst1.WorkstationID', 'left')
|
||||
->join('department', 'department.DepartmentID=workstation.DepartmentID', 'left')
|
||||
->join('valueset wstype', 'wstype.VID=workstation.Type', 'left')
|
||||
->join('valueset enable', 'enable.VID=workstation.Enable', 'left')
|
||||
->where('workstation.WorkstationID', $WorkstationID)
|
||||
->findAll();
|
||||
return $rows;
|
||||
->first();
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,11 +20,11 @@ class PatVisitModel extends BaseModel {
|
||||
protected $visnum_prefix = "DV";
|
||||
|
||||
public function show($PVID) {
|
||||
$rows = $this->select("*, patvisit.InternalPID, patvisit.CreateDate as PVCreateDate, patdiag.CreateDate as PDCreateDate, patvisitadt.CreateDate as PVACreateDate")
|
||||
$row = $this->select("*, patvisit.InternalPID, patvisit.CreateDate as PVCreateDate, patdiag.CreateDate as PDCreateDate, patvisitadt.CreateDate as PVACreateDate")
|
||||
->join('patdiag', 'patdiag.InternalPVID=patvisit.InternalPVID and patdiag.DelDate is null', 'left')
|
||||
->join('patvisitadt', 'patvisitadt.InternalPVID=patvisit.InternalPVID', 'left')
|
||||
->where('patvisit.PVID',$PVID)->findAll();
|
||||
return $rows;
|
||||
->where('patvisit.PVID',$PVID)->first();
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function showByPatient($InternalPID) {
|
||||
|
||||
@ -4,17 +4,36 @@ namespace App\Models\RefRange;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class RefNumModel extends BaseModel {
|
||||
class RefNumModel extends BaseModel
|
||||
{
|
||||
protected $table = 'refnum';
|
||||
protected $primaryKey = 'RefNumID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'SpcType', 'Sex', 'AgeStart', 'AgeEnd',
|
||||
'CriticalLow', 'Low', 'High', 'CriticalHigh',
|
||||
'CreateDate', 'EndDate'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteID',
|
||||
'SpcType',
|
||||
'Sex',
|
||||
'Criteria',
|
||||
'AgeStart',
|
||||
'AgeEnd',
|
||||
'NumRefType',
|
||||
'RangeType',
|
||||
'LowSign',
|
||||
'Low',
|
||||
'HighSign',
|
||||
'High',
|
||||
'Display',
|
||||
'Flag',
|
||||
'Interpretation',
|
||||
'Notes',
|
||||
'CreateDate',
|
||||
'StartDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
33
app/Models/RefRange/RefTxtModel.php
Normal file
33
app/Models/RefRange/RefTxtModel.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\RefRange;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class RefTxtModel extends BaseModel
|
||||
{
|
||||
protected $table = 'reftxt';
|
||||
protected $primaryKey = 'RefTxtID';
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteID',
|
||||
'SpcType',
|
||||
'Sex',
|
||||
'Criteria',
|
||||
'AgeStart',
|
||||
'AgeEnd',
|
||||
'TxtRefType',
|
||||
'RefTxt',
|
||||
'Flag',
|
||||
'Notes',
|
||||
'CreateDate',
|
||||
'StartDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
}
|
||||
@ -33,11 +33,11 @@ class ContainerDefModel extends BaseModel {
|
||||
}
|
||||
|
||||
public function getContainer($ConDefID) {
|
||||
$rows = $this->select('containerdef.*, vscol.VValue as ColorTxt, vscla.VValue as ConClassTxt, vsadd.VValue as AdditiveTxt')
|
||||
$row = $this->select('containerdef.*, vscol.VValue as ColorTxt, vscla.VValue as ConClassTxt, vsadd.VValue as AdditiveTxt')
|
||||
->join('valueset vscol', 'vscol.VID=containerdef.Color', 'left')
|
||||
->join('valueset vscla', 'vscla.VID=containerdef.ConClass', 'left')
|
||||
->join('valueset vsadd', 'vsadd.VID=containerdef.Additive', 'left')
|
||||
->where('ConDefID', $ConDefID)->findAll();
|
||||
return $rows;
|
||||
->where('ConDefID', $ConDefID)->first();
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
@ -7,13 +7,72 @@ use App\Models\BaseModel;
|
||||
class TestDefCalModel extends BaseModel {
|
||||
protected $table = 'testdefcal';
|
||||
protected $primaryKey = 'TestCalID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'DisciplineID', 'DepartmentID','FormulaCode', 'FormulaInput',
|
||||
'Unit1', 'Factor', 'Unit2', 'Decimal' ,'CreateDate', 'EndDate'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'TestSiteID',
|
||||
'DisciplineID',
|
||||
'DepartmentID',
|
||||
'FormulaInput',
|
||||
'FormulaCode',
|
||||
'RefType',
|
||||
'Unit1',
|
||||
'Factor',
|
||||
'Unit2',
|
||||
'Decimal',
|
||||
'Method',
|
||||
'CreateDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
}
|
||||
/**
|
||||
* Get calculation details for a test
|
||||
*/
|
||||
public function getCalcDetails($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left')
|
||||
->where('testdefcal.TestSiteID', $testSiteID)
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculated tests by discipline
|
||||
*/
|
||||
public function getCalcsByDiscipline($disciplineID, $siteID = null) {
|
||||
$builder = $this->select('testdefcal.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdefcal.TestSiteID', 'left')
|
||||
->where('testdefcal.DisciplineID', $disciplineID)
|
||||
->where('testdefcal.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculated tests by department
|
||||
*/
|
||||
public function getCalcsByDepartment($departmentID, $siteID = null) {
|
||||
$builder = $this->select('testdefcal.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdefcal.TestSiteID', 'left')
|
||||
->where('testdefcal.DepartmentID', $departmentID)
|
||||
->where('testdefcal.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,12 +7,43 @@ use App\Models\BaseModel;
|
||||
class TestDefGrpModel extends BaseModel {
|
||||
protected $table = 'testdefgrp';
|
||||
protected $primaryKey = 'TestGrpID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'Member', 'CreateDate', 'EndDate'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'TestSiteID',
|
||||
'Member',
|
||||
'CreateDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
}
|
||||
/**
|
||||
* Get group members for a test group
|
||||
*/
|
||||
public function getGroupMembers($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $testSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups that contain a specific test
|
||||
*/
|
||||
public function getGroupsContainingTest($memberTestSiteID) {
|
||||
return $this->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.TestSiteID', 'left')
|
||||
->where('testdefgrp.Member', $memberTestSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,23 +7,70 @@ use App\Models\BaseModel;
|
||||
class TestDefSiteModel extends BaseModel {
|
||||
protected $table = 'testdefsite';
|
||||
protected $primaryKey = 'TestSiteID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteCode', 'TestSiteName', 'TestType', 'Description', 'SeqScr', 'SeqRpt', 'IndentLeft',
|
||||
'VisibleScr', 'VisibleRpt', 'CountStat', 'CreateDate', 'EndDate'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteCode',
|
||||
'TestSiteName',
|
||||
'TestType',
|
||||
'Description',
|
||||
'SeqScr',
|
||||
'SeqRpt',
|
||||
'IndentLeft',
|
||||
'FontStyle',
|
||||
'VisibleScr',
|
||||
'VisibleRpt',
|
||||
'CountStat',
|
||||
'CreateDate',
|
||||
'StartDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $updatedField = 'StartDate';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
public function getTests() {
|
||||
$rows = $this->select("TestSiteID, TestSiteCode, TestSiteName, TestType, valueset.VValue as TypeCode, valueset.VDesc as TypeName ")
|
||||
/**
|
||||
* Get all tests with type information
|
||||
*/
|
||||
public function getTests($siteId = null, $testType = null, $visibleScr = null, $visibleRpt = null, $keyword = null) {
|
||||
$builder = $this->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->findAll();
|
||||
return $rows;
|
||||
->where('testdefsite.EndDate IS NULL');
|
||||
|
||||
if ($siteId) {
|
||||
$builder->where('testdefsite.SiteID', $siteId);
|
||||
}
|
||||
|
||||
if ($testType) {
|
||||
$builder->where('testdefsite.TestType', $testType);
|
||||
}
|
||||
|
||||
if ($visibleScr !== null) {
|
||||
$builder->where('testdefsite.VisibleScr', $visibleScr);
|
||||
}
|
||||
|
||||
if ($visibleRpt !== null) {
|
||||
$builder->where('testdefsite.VisibleRpt', $visibleRpt);
|
||||
}
|
||||
|
||||
if ($keyword) {
|
||||
$builder->like('testdefsite.TestSiteName', $keyword);
|
||||
}
|
||||
|
||||
return $builder->orderBy('testdefsite.SeqScr', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single test with all related details based on TestType
|
||||
*/
|
||||
public function getTest($TestSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$row = $this->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where("testdefsite.TestSiteID", $TestSiteID)
|
||||
@ -31,14 +78,58 @@ class TestDefSiteModel extends BaseModel {
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
if ($row['TypeCode'] == 'Calculated') {
|
||||
$row['testdefcal'] = $this->db->query("select * from testdefcal where TestSiteID='$TestSiteID'")->getResultArray();
|
||||
} elseif ($row['TypeCode'] == 'GROUP') {
|
||||
$row['testdefgrp'] = $this->db->query("select testdefgrp.*, t.TestSiteCode, t.TestSiteName from testdefgrp left join testdefsite t on t.TestSiteID=testdefgrp.Member where testdefgrp.TestSiteID='$TestSiteID'")->getResultArray();
|
||||
} else {
|
||||
$row['testdeftech'] = $this->db->query("select * from testdeftech where TestSiteID='$TestSiteID'")->getResultArray();
|
||||
$typeCode = $row['TypeCode'] ?? '';
|
||||
|
||||
// Load related details based on TestType
|
||||
if ($typeCode === 'CALC') {
|
||||
// Load calculation details with joined discipline and department
|
||||
$row['testdefcal'] = $db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left')
|
||||
->where('testdefcal.TestSiteID', $TestSiteID)
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
// Load group members with test details
|
||||
$row['testdefgrp'] = $db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $TestSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'TITLE') {
|
||||
// Load test mappings only for TITLE type
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
// TEST or PARAM - load technical details with joined tables
|
||||
$row['testdeftech'] = $db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $TestSiteID)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
}
|
||||
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,76 @@ use App\Models\BaseModel;
|
||||
class TestDefTechModel extends BaseModel {
|
||||
protected $table = 'testdeftech';
|
||||
protected $primaryKey = 'TestTechID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'DisciplineID', 'DepartmentID', 'WorkstationID', 'EquipmentID', 'VSet', 'SpcType',
|
||||
'ReqQty', 'ReqQtyUnit', 'Unit1', 'Factor', 'Unit2', 'Decimal', 'CollReq', 'Method', 'ExpectedTAT', 'CreateDate', 'EndDate'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'TestSiteID',
|
||||
'DisciplineID',
|
||||
'DepartmentID',
|
||||
'ResultType',
|
||||
'RefType',
|
||||
'VSet',
|
||||
'ReqQty',
|
||||
'ReqQtyUnit',
|
||||
'Unit1',
|
||||
'Factor',
|
||||
'Unit2',
|
||||
'Decimal',
|
||||
'CollReq',
|
||||
'Method',
|
||||
'ExpectedTAT',
|
||||
'CreateDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
}
|
||||
/**
|
||||
* Get technical details for a test
|
||||
*/
|
||||
public function getTechDetails($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $testSiteID)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tests by discipline
|
||||
*/
|
||||
public function getTestsByDiscipline($disciplineID, $siteID = null) {
|
||||
$builder = $this->select('testdeftech.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdeftech.TestSiteID', 'left')
|
||||
->where('testdeftech.DisciplineID', $disciplineID)
|
||||
->where('testdeftech.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tests by department
|
||||
*/
|
||||
public function getTestsByDepartment($departmentID, $siteID = null) {
|
||||
$builder = $this->select('testdeftech.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdeftech.TestSiteID', 'left')
|
||||
->where('testdeftech.DepartmentID', $departmentID)
|
||||
->where('testdeftech.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,22 @@ use App\Models\BaseModel;
|
||||
class TestMapModel extends BaseModel {
|
||||
protected $table = 'testmap';
|
||||
protected $primaryKey = 'TestMapID';
|
||||
protected $allowedFields = ['HostType', 'HostID', 'HostDataSource', 'HostTestCode', 'HostTestName',
|
||||
'ClientType', 'ClientID', 'ClientTestCode', 'ClientTestName', 'CreateDate', 'EndDate' ];
|
||||
protected $allowedFields = [
|
||||
'TestSiteID',
|
||||
'HostType',
|
||||
'HostID',
|
||||
'HostDataSource',
|
||||
'HostTestCode',
|
||||
'HostTestName',
|
||||
'ClientType',
|
||||
'ClientID',
|
||||
'ClientDataSource',
|
||||
'ConDefID',
|
||||
'ClientTestCode',
|
||||
'ClientTestName',
|
||||
'CreateDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
@ -16,4 +30,67 @@ class TestMapModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
}
|
||||
/**
|
||||
* Get test mappings by test site
|
||||
*/
|
||||
public function getMappingsByTestSite($testSiteID) {
|
||||
return $this->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mappings by client (equipment/workstation)
|
||||
*/
|
||||
public function getMappingsByClient($clientType, $clientID) {
|
||||
return $this->where('ClientType', $clientType)
|
||||
->where('ClientID', $clientID)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mappings by host (site/HIS)
|
||||
*/
|
||||
public function getMappingsByHost($hostType, $hostID) {
|
||||
return $this->where('HostType', $hostType)
|
||||
->where('HostID', $hostID)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mapping by client test code and container
|
||||
*/
|
||||
public function getMappingByClientCode($clientTestCode, $conDefID = null) {
|
||||
$builder = $this->where('ClientTestCode', $clientTestCode)
|
||||
->where('EndDate IS NULL');
|
||||
|
||||
if ($conDefID) {
|
||||
$builder->where('ConDefID', $conDefID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mapping by host test code
|
||||
*/
|
||||
public function getMappingByHostCode($hostTestCode) {
|
||||
return $this->where('HostTestCode', $hostTestCode)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mapping exists for client and host
|
||||
*/
|
||||
public function mappingExists($testSiteID, $clientType, $clientID, $clientTestCode) {
|
||||
return $this->where('TestSiteID', $testSiteID)
|
||||
->where('ClientType', $clientType)
|
||||
->where('ClientID', $clientID)
|
||||
->where('ClientTestCode', $clientTestCode)
|
||||
->where('EndDate IS NULL')
|
||||
->countAllResults() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,13 +16,25 @@ class ValueSetDefModel extends BaseModel {
|
||||
protected $deletedField = 'EndDate';
|
||||
|
||||
public function getValueSetDefs($param = null) {
|
||||
if ($param !== null) {
|
||||
$rows = $this->like('VSName', $param, 'both')
|
||||
->orlike('VSDesc', $param, 'both')
|
||||
->findAll();
|
||||
} else {
|
||||
$rows = $this->findAll();
|
||||
// Get item counts subquery
|
||||
$itemCounts = $this->db->table('valueset')
|
||||
->select('VSetID, COUNT(*) as ItemCount')
|
||||
->where('EndDate IS NULL')
|
||||
->groupBy('VSetID');
|
||||
|
||||
$builder = $this->db->table('valuesetdef vd');
|
||||
$builder->select('vd.*, COALESCE(ic.ItemCount, 0) as ItemCount');
|
||||
$builder->join("({$itemCounts->getCompiledSelect()}) ic", 'vd.VSetID = ic.VSetID', 'LEFT');
|
||||
$builder->where('vd.EndDate IS NULL');
|
||||
|
||||
if ($param !== null) {
|
||||
$builder->groupStart()
|
||||
->like('vd.VSName', $param, 'both')
|
||||
->orLike('vd.VSDesc', $param, 'both')
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
@ -15,18 +15,30 @@ class ValueSetModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = 'EndDate';
|
||||
|
||||
public function getValueSets($param = null) {
|
||||
$this->select("valueset.*, v1.VDesc as VCategoryName")
|
||||
->join('valueset v1', 'valueset.VCategory = v1.VID', 'LEFT');
|
||||
public function getValueSets($param = null, $page = null, $limit = 50, $VSetID = null) {
|
||||
$this->select("valueset.*, valuesetdef.VSName as VCategoryName")
|
||||
->join('valuesetdef', 'valueset.VSetID = valuesetdef.VSetID', 'LEFT');
|
||||
|
||||
if ($VSetID !== null) {
|
||||
$this->where('valueset.VSetID', $VSetID);
|
||||
}
|
||||
|
||||
if ($param !== null) {
|
||||
$this
|
||||
->groupStart()
|
||||
->like('VValue', $param, 'both')
|
||||
->orlike('VDesc', $param, 'both')
|
||||
->groupEnd();
|
||||
$this->groupStart()
|
||||
->like('valueset.VValue', $param, 'both')
|
||||
->orLike('valueset.VDesc', $param, 'both')
|
||||
->orLike('valuesetdef.VSName', $param, 'both')
|
||||
->groupEnd();
|
||||
}
|
||||
$rows = $this->findAll();
|
||||
return $rows;
|
||||
|
||||
if ($page !== null) {
|
||||
return [
|
||||
'data' => $this->paginate($limit, 'default', $page),
|
||||
'pager' => $this->pager
|
||||
];
|
||||
}
|
||||
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
public function getValueSet($VID) {
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
|
||||
<!-- SEO Meta -->
|
||||
<title><?= $title ?? 'CLQMS' ?> - Clinical Laboratory QMS</title>
|
||||
<meta name="description" content="<?= $description ?? 'CLQMS - Modern Clinical Laboratory Quality Management System' ?>">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
||||
<!-- Google Fonts - Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Lucide Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
|
||||
<!-- App Styles -->
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
|
||||
<!-- Page-specific styles -->
|
||||
<?= $this->renderSection('styles') ?>
|
||||
</head>
|
||||
<body class="bg-pattern" x-data>
|
||||
|
||||
<!-- Floating Decorative Shapes -->
|
||||
<div class="floating-shapes">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
<div class="shape shape-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<?= $this->renderSection('content') ?>
|
||||
|
||||
<!-- Toast Notifications Container -->
|
||||
<div
|
||||
x-data
|
||||
class="toast-container"
|
||||
style="position: fixed; top: 1rem; right: 1rem; z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem;"
|
||||
>
|
||||
<template x-for="toast in $store.toast.messages" :key="toast.id">
|
||||
<div
|
||||
x-show="true"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform translate-x-8"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
:class="{
|
||||
'alert': true,
|
||||
'alert-success': toast.type === 'success',
|
||||
'alert-error': toast.type === 'error',
|
||||
'alert-info': toast.type === 'info'
|
||||
}"
|
||||
style="min-width: 280px; cursor: pointer;"
|
||||
@click="$store.toast.dismiss(toast.id)"
|
||||
>
|
||||
<span x-text="toast.message"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Alpine.js 3.x -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- App Scripts (loaded before Alpine) -->
|
||||
<script src="/assets/js/app.js"></script>
|
||||
|
||||
<!-- Initialize Lucide Icons -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Page-specific scripts -->
|
||||
<?= $this->renderSection('scripts') ?>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,40 +0,0 @@
|
||||
<?= $this->extend('layouts/main') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div style="min-height: 100vh; padding: 2rem;">
|
||||
<div class="card card-glass fade-in" style="max-width: 600px; margin: 2rem auto; text-align: center;">
|
||||
|
||||
<div class="login-logo" style="margin-bottom: 1.5rem;">
|
||||
<i data-lucide="layout-dashboard"></i>
|
||||
</div>
|
||||
|
||||
<h1 style="margin-bottom: 0.5rem;">🎉 Welcome to Dashboard!</h1>
|
||||
<p class="text-muted" style="margin-bottom: 2rem;">
|
||||
You're successfully logged in. This is a placeholder page.
|
||||
</p>
|
||||
|
||||
<?php if (isset($user)): ?>
|
||||
<div class="alert alert-success" style="text-align: left;">
|
||||
<i data-lucide="check-circle" style="width: 18px; height: 18px;"></i>
|
||||
<span>Logged in as: <strong><?= esc($user->username ?? 'User') ?></strong></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
|
||||
<a href="/login" class="btn btn-secondary">
|
||||
<i data-lucide="arrow-left" style="width: 18px; height: 18px;"></i>
|
||||
Back to Login
|
||||
</a>
|
||||
<form action="/logout" method="get" style="margin: 0;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i data-lucide="log-out" style="width: 18px; height: 18px;"></i>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@ -1,130 +0,0 @@
|
||||
<?= $this->extend('layouts/main') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-card card card-glass fade-in" x-data="loginForm" x-ref="loginCard">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="login-header">
|
||||
<div class="login-logo">
|
||||
<i data-lucide="flask-conical"></i>
|
||||
</div>
|
||||
<h1 class="login-title">Welcome Back!</h1>
|
||||
<p class="login-subtitle">Sign in to your CLQMS account</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<template x-if="error">
|
||||
<div class="alert alert-error" x-transition>
|
||||
<i data-lucide="alert-circle" style="width: 18px; height: 18px;"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="submitLogin">
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Username</label>
|
||||
<div class="form-input-icon">
|
||||
<i data-lucide="user" class="icon" style="width: 18px; height: 18px;"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
class="form-input"
|
||||
placeholder="Enter your username"
|
||||
x-model="username"
|
||||
:disabled="isLoading"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<div class="form-input-icon">
|
||||
<i data-lucide="lock" class="icon" style="width: 18px; height: 18px;"></i>
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
id="password"
|
||||
class="form-input"
|
||||
placeholder="Enter your password"
|
||||
x-model="password"
|
||||
:disabled="isLoading"
|
||||
autocomplete="current-password"
|
||||
style="padding-right: 3rem;"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="password-toggle"
|
||||
@click="togglePassword"
|
||||
:title="showPassword ? 'Hide password' : 'Show password'"
|
||||
>
|
||||
<i :data-lucide="showPassword ? 'eye-off' : 'eye'" style="width: 18px; height: 18px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me & Forgot Password -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox-input"
|
||||
x-model="rememberMe"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<span class="checkbox-label">Remember me</span>
|
||||
</label>
|
||||
<a href="#" class="text-sm text-primary">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<template x-if="isLoading">
|
||||
<div class="spinner"></div>
|
||||
</template>
|
||||
<template x-if="!isLoading">
|
||||
<i data-lucide="log-in" style="width: 20px; height: 20px;"></i>
|
||||
</template>
|
||||
<span x-text="isLoading ? 'Signing in...' : 'Sign In'"></span>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="login-footer">
|
||||
<p class="text-muted">
|
||||
© <?= date('Y') ?> CLQMS • Clinical Laboratory QMS
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
// Re-initialize Lucide icons after Alpine updates the DOM
|
||||
document.addEventListener('alpine:initialized', () => {
|
||||
// Watch for DOM changes and re-create icons
|
||||
const observer = new MutationObserver(() => {
|
||||
lucide.createIcons();
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
214
app/Views/v2/auth/login.php
Normal file
214
app/Views/v2/auth/login.php
Normal file
@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - CLQMS</title>
|
||||
|
||||
<!-- Google Fonts - Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- TailwindCSS 4 CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="<?= base_url('css/v2/styles.css') ?>">
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
||||
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* Animated gradient background */
|
||||
.gradient-bg {
|
||||
background: linear-gradient(-45deg, #1e3a8a, #1e40af, #2563eb, #3b82f6);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Floating animation for logo */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.float-animation {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex items-center justify-center gradient-bg" x-data="loginApp()">
|
||||
|
||||
<!-- Login Card -->
|
||||
<div class="w-full max-w-md p-4">
|
||||
<div class="card-glass p-8 animate-fadeIn">
|
||||
|
||||
<!-- Logo & Title -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="w-20 h-20 mx-auto mb-4 rounded-3xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-xl float-animation">
|
||||
<i class="fa-solid fa-flask text-white text-4xl"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-2 text-gradient">CLQMS</h1>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Clinical Laboratory Quality Management System</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div x-show="errorMessage" x-cloak class="alert alert-error mb-4 animate-slideInUp">
|
||||
<i class="fa-solid fa-exclamation-circle"></i>
|
||||
<span x-text="errorMessage"></span>
|
||||
</div>
|
||||
|
||||
<div x-show="successMessage" x-cloak class="alert alert-success mb-4 animate-slideInUp">
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
<span x-text="successMessage"></span>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="login" class="space-y-4">
|
||||
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Username</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
class="input !pl-10"
|
||||
x-model="form.username"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Password</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="Enter your password"
|
||||
class="input !pl-10 !pr-10"
|
||||
x-model="form.password"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 z-10"
|
||||
style="color: rgb(var(--color-text-muted));"
|
||||
tabindex="-1"
|
||||
>
|
||||
<i :class="showPassword ? 'fa-solid fa-eye-slash' : 'fa-solid fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.remember" id="remember" />
|
||||
<label for="remember" class="label-text cursor-pointer">Remember me</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-full !rounded-full"
|
||||
:disabled="loading"
|
||||
>
|
||||
<span x-show="loading" class="spinner spinner-sm"></span>
|
||||
<span x-show="!loading">
|
||||
<i class="fa-solid fa-sign-in-alt mr-2"></i>
|
||||
Login
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div class="text-center mt-6 text-white/90">
|
||||
<p class="text-sm drop-shadow-lg">© 2025 5Panda. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
window.BASEURL = "<?= base_url() ?>";
|
||||
|
||||
function loginApp() {
|
||||
return {
|
||||
loading: false,
|
||||
showPassword: false,
|
||||
errorMessage: '',
|
||||
successMessage: '',
|
||||
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false
|
||||
},
|
||||
|
||||
async login() {
|
||||
this.errorMessage = '';
|
||||
this.successMessage = '';
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const formData = new URLSearchParams({
|
||||
username: this.form.username,
|
||||
password: this.form.password,
|
||||
remember: this.form.remember ? '1' : '0'
|
||||
});
|
||||
|
||||
const res = await fetch(`${BASEURL}v2/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.status === 'success') {
|
||||
this.successMessage = 'Login successful! Redirecting...';
|
||||
setTimeout(() => {
|
||||
window.location.href = `${BASEURL}v2/`;
|
||||
}, 1000);
|
||||
} else {
|
||||
this.errorMessage = data.message || 'Login failed. Please try again.';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.errorMessage = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
153
app/Views/v2/dashboard/dashboard_index.php
Normal file
153
app/Views/v2/dashboard/dashboard_index.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div class="w-full space-y-6">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<div class="card-glass p-8 animate-fadeIn">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-chart-line text-3xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold mb-2" style="color: rgb(var(--color-text));">Welcome to CLQMS</h2>
|
||||
<p class="text-lg" style="color: rgb(var(--color-text-muted));">Clinical Laboratory Quality Management System</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<!-- Total Patients -->
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Total Patients</p>
|
||||
<p class="text-3xl font-bold" style="color: rgb(var(--color-text));">1,247</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-blue-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-users text-blue-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's Visits -->
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Today's Visits</p>
|
||||
<p class="text-3xl font-bold text-emerald-500">89</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-emerald-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-calendar-check text-emerald-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Tests -->
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Pending Tests</p>
|
||||
<p class="text-3xl font-bold text-amber-500">34</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-amber-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-flask text-amber-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Today -->
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Completed</p>
|
||||
<p class="text-3xl font-bold text-sky-500">156</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-sky-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-check-circle text-sky-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Recent Activity -->
|
||||
<div class="card">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-bold mb-4 flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-clock-rotate-left" style="color: rgb(var(--color-primary));"></i>
|
||||
Recent Activity
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg hover:bg-opacity-50 transition-colors" style="background: rgb(var(--color-bg) / 0.5);">
|
||||
<div class="w-10 h-10 rounded-full bg-emerald-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-user-plus text-emerald-500"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm" style="color: rgb(var(--color-text));">New patient registered</p>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">John Doe - 5 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg hover:bg-opacity-50 transition-colors" style="background: rgb(var(--color-bg) / 0.5);">
|
||||
<div class="w-10 h-10 rounded-full bg-sky-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-vial text-sky-500"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm" style="color: rgb(var(--color-text));">Test completed</p>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Sample #12345 - 12 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg hover:bg-opacity-50 transition-colors" style="background: rgb(var(--color-bg) / 0.5);">
|
||||
<div class="w-10 h-10 rounded-full bg-amber-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-exclamation-triangle text-amber-500"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm" style="color: rgb(var(--color-text));">Pending approval</p>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Request #789 - 25 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="card">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-bold mb-4 flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-bolt" style="color: rgb(var(--color-primary));"></i>
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<a href="<?= base_url('/v2/patients') ?>" class="btn btn-outline group">
|
||||
<i class="fa-solid fa-users mr-2 group-hover:scale-110 transition-transform"></i>
|
||||
Patients
|
||||
</a>
|
||||
<a href="<?= base_url('/v2/requests') ?>" class="btn btn-outline-secondary group">
|
||||
<i class="fa-solid fa-flask mr-2 group-hover:scale-110 transition-transform"></i>
|
||||
Lab Requests
|
||||
</a>
|
||||
<button class="btn btn-outline-accent group">
|
||||
<i class="fa-solid fa-vial mr-2 group-hover:scale-110 transition-transform"></i>
|
||||
Specimens
|
||||
</button>
|
||||
<button class="btn btn-outline-info group">
|
||||
<i class="fa-solid fa-chart-bar mr-2 group-hover:scale-110 transition-transform"></i>
|
||||
Reports
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
389
app/Views/v2/layout/main_layout.php
Normal file
389
app/Views/v2/layout/main_layout.php
Normal file
@ -0,0 +1,389 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= esc($pageTitle ?? 'CLQMS') ?> - CLQMS</title>
|
||||
|
||||
<!-- Google Fonts - Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- TailwindCSS 4 CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="<?= base_url('css/v2/styles.css') ?>">
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
||||
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body class="min-h-screen flex" style="background: rgb(var(--color-bg));" x-data="layout()">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="sidebar sticky top-0 z-40 h-screen flex flex-col shadow-2xl"
|
||||
:class="sidebarOpen ? 'w-64' : 'w-0 lg:w-20'"
|
||||
style="transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1);"
|
||||
>
|
||||
<!-- Sidebar Header -->
|
||||
<div class="h-16 flex items-center justify-between px-4 border-b border-white/10" x-show="sidebarOpen" x-cloak>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-flask text-white text-lg"></i>
|
||||
</div>
|
||||
<span class="text-xl font-bold text-white">CLQMS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsed Logo -->
|
||||
<div class="h-16 flex items-center justify-center border-b border-white/10" x-show="!sidebarOpen" x-cloak>
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-flask text-white text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 py-6 overflow-y-auto" :class="sidebarOpen ? 'px-4' : 'px-2'">
|
||||
<ul class="menu">
|
||||
<!-- Dashboard -->
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/') ?>"
|
||||
:class="isActive('v2') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-th-large w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen" x-cloak>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Patients -->
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/patients') ?>"
|
||||
:class="isActive('patients') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-users w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen" x-cloak>Patients</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Lab Requests -->
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/requests') ?>"
|
||||
:class="isActive('requests') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-flask w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen" x-cloak>Lab Requests</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Master Data Sections -->
|
||||
<template x-if="sidebarOpen">
|
||||
<li class="px-3 py-2 mt-4 mb-1">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider opacity-60">Master Data</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- Organization (Nested Group) -->
|
||||
<li>
|
||||
<div x-data="{
|
||||
isOpen: orgOpen,
|
||||
toggle() { this.isOpen = !this.isOpen; $root.layout().orgOpen = this.isOpen }
|
||||
}" x-init="$watch('orgOpen', v => isOpen = v)">
|
||||
<button @click="isOpen = !isOpen"
|
||||
class="group w-full flex items-center justify-between"
|
||||
:class="isParentActive('organization') ? 'text-primary font-medium' : ''">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-building w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen">Organization</span>
|
||||
</div>
|
||||
<i x-show="sidebarOpen" class="fa-solid fa-chevron-down text-xs transition-transform" :class="isOpen && 'rotate-180'"></i>
|
||||
</button>
|
||||
<ul x-show="isOpen && sidebarOpen" x-collapse class="ml-8 mt-2 space-y-1">
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/organization/accounts') ?>"
|
||||
:class="isActive('organization/accounts') ? 'active' : ''"
|
||||
class="text-sm">Accounts</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/organization/sites') ?>"
|
||||
:class="isActive('organization/sites') ? 'active' : ''"
|
||||
class="text-sm">Sites</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/organization/disciplines') ?>"
|
||||
:class="isActive('organization/disciplines') ? 'active' : ''"
|
||||
class="text-sm">Disciplines</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/organization/departments') ?>"
|
||||
:class="isActive('organization/departments') ? 'active' : ''"
|
||||
class="text-sm">Departments</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/organization/workstations') ?>"
|
||||
:class="isActive('organization/workstations') ? 'active' : ''"
|
||||
class="text-sm">Workstations</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Specimen (Nested Group) -->
|
||||
<li>
|
||||
<div x-data="{
|
||||
isOpen: specimenOpen,
|
||||
toggle() { this.isOpen = !this.isOpen; $root.layout().specimenOpen = this.isOpen }
|
||||
}" x-init="$watch('specimenOpen', v => isOpen = v)">
|
||||
<button @click="isOpen = !isOpen"
|
||||
class="group w-full flex items-center justify-between"
|
||||
:class="isParentActive('specimen') ? 'text-primary font-medium' : ''">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-vial w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen">Specimen</span>
|
||||
</div>
|
||||
<i x-show="sidebarOpen" class="fa-solid fa-chevron-down text-xs transition-transform" :class="isOpen && 'rotate-180'"></i>
|
||||
</button>
|
||||
<ul x-show="isOpen && sidebarOpen" x-collapse class="ml-8 mt-2 space-y-1">
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/specimen/containers') ?>"
|
||||
:class="isActive('specimen/containers') ? 'active' : ''"
|
||||
class="text-sm">Container Defs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/specimen/preparations') ?>"
|
||||
:class="isActive('specimen/preparations') ? 'active' : ''"
|
||||
class="text-sm">Preparations</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Lab Tests -->
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/tests') ?>"
|
||||
:class="isActive('master/tests') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-microscope w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen">Lab Tests</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Value Sets -->
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/valuesets') ?>"
|
||||
:class="isActive('master/valuesets') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-list-check w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen">Value Sets</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Settings -->
|
||||
<li class="mt-4">
|
||||
<a href="<?= base_url('/v2/settings') ?>"
|
||||
:class="isActive('settings') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-cog w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
<span x-show="sidebarOpen" x-cloak>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Overlay for mobile -->
|
||||
<div
|
||||
x-show="sidebarOpen"
|
||||
@click="sidebarOpen = false"
|
||||
class="fixed inset-0 bg-black/50 z-30 lg:hidden backdrop-blur-sm"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
></div>
|
||||
|
||||
<!-- Main Content Wrapper -->
|
||||
<div class="flex-1 flex flex-col min-h-screen">
|
||||
|
||||
<!-- Top Navbar -->
|
||||
<nav class="h-16 flex items-center justify-between px-4 lg:px-6 sticky top-0 z-20 glass shadow-sm">
|
||||
<!-- Left: Burger Menu & Title -->
|
||||
<div class="flex items-center gap-4">
|
||||
<button @click="sidebarOpen = !sidebarOpen" class="btn btn-ghost btn-square">
|
||||
<i class="fa-solid fa-bars text-lg"></i>
|
||||
</button>
|
||||
<h1 class="text-lg font-semibold" style="color: rgb(var(--color-text));"><?= esc($pageTitle ?? 'Dashboard') ?></h1>
|
||||
</div>
|
||||
|
||||
<!-- Right: Theme Toggle & User -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Theme Toggle -->
|
||||
<button @click="toggleTheme()" class="btn btn-ghost btn-square group" title="Toggle theme">
|
||||
<i x-show="lightMode" class="fa-solid fa-moon text-lg transition-transform group-hover:rotate-12"></i>
|
||||
<i x-show="!lightMode" class="fa-solid fa-sun text-lg transition-transform group-hover:rotate-45"></i>
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div class="dropdown dropdown-end" x-data="{ open: false }">
|
||||
<button @click="open = !open" class="btn btn-ghost gap-2 px-3">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-md">
|
||||
<span class="text-xs font-semibold text-white">U</span>
|
||||
</div>
|
||||
<span class="hidden sm:inline text-sm font-medium">User</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs opacity-60 transition-transform" :class="open && 'rotate-180'"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Content -->
|
||||
<div
|
||||
x-show="open"
|
||||
@click.away="open = false"
|
||||
x-cloak
|
||||
class="dropdown-content mt-2 w-72 shadow-2xl"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- User Info Header -->
|
||||
<div class="px-4 py-4" style="border-bottom: 1px solid rgb(var(--color-border));">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
|
||||
<span class="text-sm font-semibold text-white">U</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-sm" style="color: rgb(var(--color-text));">User Name</p>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">user@example.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<ul class="menu menu-sm p-2">
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 py-2">
|
||||
<i class="fa-solid fa-user w-4 text-center"></i>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 py-2">
|
||||
<i class="fa-solid fa-cog w-4 text-center"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Logout -->
|
||||
<div style="border-top: 1px solid rgb(var(--color-border));" class="p-2">
|
||||
<button @click="logout()" class="btn btn-ghost btn-sm w-full justify-start gap-3 hover:bg-red-50" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-sign-out-alt w-4 text-center"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 p-4 lg:p-6 overflow-auto">
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="glass border-t py-4 px-6" style="border-color: rgb(var(--color-border));">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm" style="color: rgb(var(--color-text-muted));">
|
||||
<span>© 2025 5Panda. All rights reserved.</span>
|
||||
<span>CLQMS v1.0.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Global Scripts -->
|
||||
<script>
|
||||
window.BASEURL = "<?= base_url() ?>".replace(/\/$/, "") + "/";
|
||||
|
||||
function layout() {
|
||||
return {
|
||||
sidebarOpen: localStorage.getItem('sidebarOpen') !== 'false',
|
||||
lightMode: localStorage.getItem('theme') !== 'dark',
|
||||
orgOpen: false,
|
||||
specimenOpen: false,
|
||||
currentPath: window.location.pathname,
|
||||
|
||||
init() {
|
||||
// Apply saved theme (default to light theme)
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
this.lightMode = savedTheme === 'light';
|
||||
|
||||
// Detect sidebar open/closed for mobile
|
||||
if (window.innerWidth < 1024) this.sidebarOpen = false;
|
||||
|
||||
// Auto-expand menus based on active path
|
||||
this.orgOpen = this.currentPath.includes('organization');
|
||||
this.specimenOpen = this.currentPath.includes('specimen');
|
||||
|
||||
// Watch sidebar state to persist
|
||||
this.$watch('sidebarOpen', val => localStorage.setItem('sidebarOpen', val));
|
||||
},
|
||||
|
||||
isActive(path) {
|
||||
// Get the current path without query strings or hash
|
||||
const current = window.location.pathname;
|
||||
|
||||
// Handle dashboard as root - exact match only
|
||||
if (path === 'v2') {
|
||||
return current === '/v2' || current === '/v2/' || current === '/clqms-be/v2' || current === '/clqms-be/v2/';
|
||||
}
|
||||
// For other paths, check if current path contains the expected path segment
|
||||
// Use exact match with /v2/ prefix
|
||||
const checkPath = '/v2/' + path;
|
||||
return current.includes(checkPath);
|
||||
},
|
||||
|
||||
isParentActive(parent) {
|
||||
return this.currentPath.includes(parent);
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.lightMode = !this.lightMode;
|
||||
const theme = this.lightMode ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}v2/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = `${BASEURL}v2/login`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
window.location.href = `${BASEURL}v2/login`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<?= $this->renderSection('script') ?>
|
||||
</body>
|
||||
</html>
|
||||
189
app/Views/v2/master/organization/account_dialog.php
Normal file
189
app/Views/v2/master/organization/account_dialog.php
Normal file
@ -0,0 +1,189 @@
|
||||
<!-- Account Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-building-circle-plus" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Account' : 'New Account'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Basic Information</h4>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Account Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.AccountName && 'input-error'"
|
||||
x-model="form.AccountName"
|
||||
placeholder="Main Laboratory Inc."
|
||||
/>
|
||||
<label class="label" x-show="errors.AccountName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.AccountName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Initial / Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input text-center font-mono"
|
||||
x-model="form.Initial"
|
||||
placeholder="MLAB"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Parent Account</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Parent">
|
||||
<option value="">None (Top Level)</option>
|
||||
<template x-for="acc in list" :key="acc.AccountID">
|
||||
<option :value="acc.AccountID" x-text="acc.AccountName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">Contact Info</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Address</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input"
|
||||
x-model="form.EmailAddress1"
|
||||
placeholder="contact@lab.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Phone Number</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
class="input"
|
||||
x-model="form.Phone"
|
||||
placeholder="+62 21..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Location & Address</h4>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Street Address</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-24 pt-2"
|
||||
x-model="form.Street_1"
|
||||
placeholder="Jalan Sudirman No. 123..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.City"
|
||||
placeholder="Jakarta"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Province</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Province"
|
||||
placeholder="DKI Jakarta"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ZIP Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ZIP"
|
||||
placeholder="12345"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Country</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Country"
|
||||
placeholder="Indonesia"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Account'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
329
app/Views/v2/master/organization/accounts_index.php
Normal file
329
app/Views/v2/master/organization/accounts_index.php
Normal file
@ -0,0 +1,329 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="accounts()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-600 to-purple-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-building text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Organization Accounts</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage organization accounts and entities</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search accounts..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading accounts...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account Name</th>
|
||||
<th>Code</th>
|
||||
<th>Parent</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No accounts found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Account
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Account Rows -->
|
||||
<template x-for="account in list" :key="account.AccountID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="account.AccountID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="account.AccountName || '-'"></div>
|
||||
</td>
|
||||
<td x-text="account.Initial || '-'"></td>
|
||||
<td>
|
||||
<span class="text-xs" x-text="account.ParentName || '-'"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editAccount(account.AccountID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(account)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));" x-show="list && list.length > 0">
|
||||
<span class="text-sm" style="color: rgb(var(--color-text-muted));" x-text="'Showing ' + list.length + ' accounts'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/organization/account_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete account <strong x-text="deleteTarget?.AccountName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteAccount()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function accounts() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
keyword: "",
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
AccountID: null,
|
||||
Parent: "",
|
||||
AccountName: "",
|
||||
Initial: "",
|
||||
Street_1: "",
|
||||
City: "",
|
||||
Province: "",
|
||||
ZIP: "",
|
||||
Country: "",
|
||||
EmailAddress1: "",
|
||||
Phone: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
},
|
||||
|
||||
// Fetch account list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('AccountName', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/organization/account?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new account
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
AccountID: null,
|
||||
Parent: "",
|
||||
AccountName: "",
|
||||
Initial: "",
|
||||
Street_1: "",
|
||||
City: "",
|
||||
Province: "",
|
||||
ZIP: "",
|
||||
Country: "",
|
||||
EmailAddress1: "",
|
||||
Phone: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit account
|
||||
async editAccount(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/account/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load account data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.AccountName?.trim()) e.AccountName = "Account name is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save account
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
let res;
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
|
||||
res = await fetch(`${BASEURL}api/organization/account`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save account");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(account) {
|
||||
this.deleteTarget = account;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete account
|
||||
async deleteAccount() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/account`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ AccountID: this.deleteTarget.AccountID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete account");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
107
app/Views/v2/master/organization/department_dialog.php
Normal file
107
app/Views/v2/master/organization/department_dialog.php
Normal file
@ -0,0 +1,107 @@
|
||||
<!-- Department Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-sitemap" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Department' : 'New Department'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Department Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.DepartmentName && 'input-error'"
|
||||
x-model="form.DepartmentName"
|
||||
placeholder="Clinical Chemistry"
|
||||
/>
|
||||
<label class="label" x-show="errors.DepartmentName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.DepartmentName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Department Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.DepartmentCode && 'input-error'"
|
||||
x-model="form.DepartmentCode"
|
||||
placeholder="CHEM"
|
||||
/>
|
||||
<label class="label" x-show="errors.DepartmentCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.DepartmentCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<option value="">Select Site</option>
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Discipline</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<template x-for="d in disciplinesList" :key="d.DisciplineID">
|
||||
<option :value="d.DisciplineID" x-text="d.DisciplineName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Department'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
351
app/Views/v2/master/organization/departments_index.php
Normal file
351
app/Views/v2/master/organization/departments_index.php
Normal file
@ -0,0 +1,351 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="departments()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-teal-600 to-teal-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-sitemap text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Organization Departments</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage lab departments and functional units</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search departments..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Department
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading departments...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Department Name</th>
|
||||
<th>Code</th>
|
||||
<th>Discipline</th>
|
||||
<th>Site</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No departments found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Department
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Department Rows -->
|
||||
<template x-for="dept in list" :key="dept.DepartmentID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="dept.DepartmentID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="dept.DepartmentName || '-'"></div>
|
||||
</td>
|
||||
<td class="font-mono text-sm" x-text="dept.DepartmentCode || '-'"></td>
|
||||
<td x-text="dept.DisciplineName || '-'"></td>
|
||||
<td x-text="dept.SiteName || '-'"></td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editDepartment(dept.DepartmentID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(dept)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/organization/department_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete department <strong x-text="deleteTarget?.DepartmentName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteDepartment()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function departments() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
sitesList: [],
|
||||
disciplinesList: [],
|
||||
keyword: "",
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
DepartmentID: null,
|
||||
DepartmentCode: "",
|
||||
DepartmentName: "",
|
||||
SiteID: "",
|
||||
DisciplineID: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
await this.fetchSites();
|
||||
await this.fetchDisciplines();
|
||||
},
|
||||
|
||||
// Fetch department list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('DepartmentName', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/organization/department?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch site list for dropdown
|
||||
async fetchSites() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/site`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
this.sitesList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch sites:', err);
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch discipline list for dropdown
|
||||
async fetchDisciplines() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/discipline`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
// Since discipline API returns nested structure, we need to flatten it for the dropdown
|
||||
const flat = [];
|
||||
if (data.data) {
|
||||
data.data.forEach(p => {
|
||||
flat.push({ DisciplineID: p.DisciplineID, DisciplineName: p.DisciplineName });
|
||||
if (p.children) {
|
||||
p.children.forEach(c => {
|
||||
flat.push({ DisciplineID: c.DisciplineID, DisciplineName: `— ${c.DisciplineName}` });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.disciplinesList = flat;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch disciplines:', err);
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new department
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
DepartmentID: null,
|
||||
DepartmentCode: "",
|
||||
DepartmentName: "",
|
||||
SiteID: "",
|
||||
DisciplineID: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit department
|
||||
async editDepartment(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/department/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load department data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.DepartmentName?.trim()) e.DepartmentName = "Department name is required";
|
||||
if (!this.form.DepartmentCode?.trim()) e.DepartmentCode = "Department code is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save department
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const res = await fetch(`${BASEURL}api/organization/department`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save department");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(dept) {
|
||||
this.deleteTarget = dept;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete department
|
||||
async deleteDepartment() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/department`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ DepartmentID: this.deleteTarget.DepartmentID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete department");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
107
app/Views/v2/master/organization/discipline_dialog.php
Normal file
107
app/Views/v2/master/organization/discipline_dialog.php
Normal file
@ -0,0 +1,107 @@
|
||||
<!-- Discipline Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-layer-group" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Discipline' : 'New Discipline'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Discipline Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.DisciplineName && 'input-error'"
|
||||
x-model="form.DisciplineName"
|
||||
placeholder="Hematology"
|
||||
/>
|
||||
<label class="label" x-show="errors.DisciplineName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.DisciplineName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Discipline Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.DisciplineCode && 'input-error'"
|
||||
x-model="form.DisciplineCode"
|
||||
placeholder="HEM"
|
||||
/>
|
||||
<label class="label" x-show="errors.DisciplineCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.DisciplineCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<option value="">Select Site</option>
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Parent Discipline</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Parent">
|
||||
<option value="">None</option>
|
||||
<template x-for="d in flatList" :key="d.DisciplineID">
|
||||
<option :value="d.DisciplineID" x-text="d.DisciplineName" :disabled="d.DisciplineID == form.DisciplineID"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Discipline'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
352
app/Views/v2/master/organization/disciplines_index.php
Normal file
352
app/Views/v2/master/organization/disciplines_index.php
Normal file
@ -0,0 +1,352 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="disciplines()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-600 to-indigo-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-layer-group text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Lab Disciplines</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage laboratory disciplines and specialties</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search disciplines..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Discipline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading disciplines...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table table-compact w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="80">ID</th>
|
||||
<th>Discipline Name</th>
|
||||
<th>Code</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4">
|
||||
<div class="flex flex-col items-center gap-2 py-2" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-3xl opacity-40"></i>
|
||||
<p class="text-sm">No disciplines found</p>
|
||||
<button class="btn btn-primary btn-xs mt-1" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Discipline
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Discipline Rows -->
|
||||
<template x-for="item in flatListWithLevels" :key="item.DisciplineID">
|
||||
<tr :class="item.level === 0 ? 'bg-slate-50/50' : 'hover:bg-opacity-50'">
|
||||
<td :class="item.level === 1 ? 'pl-8' : ''">
|
||||
<span class="badge badge-ghost font-mono text-xs" :class="{'opacity-60': item.level === 1}" x-text="item.DisciplineID"></span>
|
||||
</td>
|
||||
<td :class="item.level === 1 ? 'pl-12' : ''">
|
||||
<div class="flex items-center gap-2" :class="item.level === 0 ? 'font-bold' : ''" style="color: rgb(var(--color-text));">
|
||||
<i :class="item.level === 0 ? 'fa-solid fa-folder-open text-amber-500' : 'fa-solid fa-chevron-right text-xs opacity-30'"></i>
|
||||
<span x-text="item.DisciplineName"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="font-mono text-sm" :class="{'opacity-70': item.level === 1}" x-text="item.DisciplineCode"></td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editDiscipline(item.DisciplineID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(item)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/organization/discipline_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete discipline <strong x-text="deleteTarget?.DisciplineName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteDiscipline()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function disciplines() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
flatList: [],
|
||||
sitesList: [],
|
||||
keyword: "",
|
||||
|
||||
// Get flattened list with level indicators for table rendering
|
||||
get flatListWithLevels() {
|
||||
const flat = [];
|
||||
this.list.forEach(parent => {
|
||||
flat.push({ ...parent, level: 0 });
|
||||
if (parent.children && parent.children.length > 0) {
|
||||
parent.children.forEach(child => {
|
||||
flat.push({ ...child, level: 1, ParentID: parent.DisciplineID });
|
||||
});
|
||||
}
|
||||
});
|
||||
return flat;
|
||||
},
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
DisciplineID: null,
|
||||
DisciplineCode: "",
|
||||
DisciplineName: "",
|
||||
SiteID: "",
|
||||
Parent: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
await this.fetchSites();
|
||||
},
|
||||
|
||||
// Fetch discipline list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('DisciplineName', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/organization/discipline?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
|
||||
// Build flat list for parent selection dropdown
|
||||
const flat = [];
|
||||
this.list.forEach(p => {
|
||||
flat.push({ DisciplineID: p.DisciplineID, DisciplineName: p.DisciplineName });
|
||||
if (p.children && p.children.length > 0) {
|
||||
p.children.forEach(c => {
|
||||
flat.push({ DisciplineID: c.DisciplineID, DisciplineName: `— ${c.DisciplineName}` });
|
||||
});
|
||||
}
|
||||
});
|
||||
this.flatList = flat;
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch site list for dropdown
|
||||
async fetchSites() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/site`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
this.sitesList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch sites:', err);
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new discipline
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
DisciplineID: null,
|
||||
DisciplineCode: "",
|
||||
DisciplineName: "",
|
||||
SiteID: "",
|
||||
Parent: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit discipline
|
||||
async editDiscipline(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/discipline/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load discipline data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.DisciplineName?.trim()) e.DisciplineName = "Discipline name is required";
|
||||
if (!this.form.DisciplineCode?.trim()) e.DisciplineCode = "Discipline code is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save discipline
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const res = await fetch(`${BASEURL}api/organization/discipline`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save discipline");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(discipline) {
|
||||
this.deleteTarget = discipline;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete discipline
|
||||
async deleteDiscipline() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/discipline`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ DisciplineID: this.deleteTarget.DisciplineID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete discipline");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
119
app/Views/v2/master/organization/site_dialog.php
Normal file
119
app/Views/v2/master/organization/site_dialog.php
Normal file
@ -0,0 +1,119 @@
|
||||
<!-- Site Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-hospital-user" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Site' : 'New Site'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.SiteName && 'input-error'"
|
||||
x-model="form.SiteName"
|
||||
placeholder="Main Hospital Site"
|
||||
/>
|
||||
<label class="label" x-show="errors.SiteName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.SiteName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.SiteCode && 'input-error'"
|
||||
x-model="form.SiteCode"
|
||||
placeholder="SITE-01"
|
||||
/>
|
||||
<label class="label" x-show="errors.SiteCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.SiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Account</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.AccountID">
|
||||
<option value="">Select Account</option>
|
||||
<template x-for="acc in accountsList" :key="acc.AccountID">
|
||||
<option :value="acc.AccountID" x-text="acc.AccountName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Parent Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Parent">
|
||||
<option value="">None</option>
|
||||
<template x-for="s in list" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName" :disabled="s.SiteID == form.SiteID"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ME (Medical Examiner?)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ME"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Site'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
332
app/Views/v2/master/organization/sites_index.php
Normal file
332
app/Views/v2/master/organization/sites_index.php
Normal file
@ -0,0 +1,332 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="sites()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-hospital text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Organization Sites</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage physical sites and locations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search sites..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Site
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading sites...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Site Name</th>
|
||||
<th>Code</th>
|
||||
<th>Account</th>
|
||||
<th>Parent Site</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No sites found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Site
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Site Rows -->
|
||||
<template x-for="site in list" :key="site.SiteID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="site.SiteID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="site.SiteName || '-'"></div>
|
||||
</td>
|
||||
<td x-text="site.SiteCode || '-'"></td>
|
||||
<td x-text="site.AccountName || '-'"></td>
|
||||
<td x-text="site.ParentName || '-'"></td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editSite(site.SiteID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(site)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));" x-show="list && list.length > 0">
|
||||
<span class="text-sm" style="color: rgb(var(--color-text-muted));" x-text="'Showing ' + list.length + ' sites'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/organization/site_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete site <strong x-text="deleteTarget?.SiteName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteSite()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function sites() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
accountsList: [],
|
||||
keyword: "",
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
SiteID: null,
|
||||
SiteCode: "",
|
||||
SiteName: "",
|
||||
AccountID: "",
|
||||
Parent: "",
|
||||
ME: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
await this.fetchAccounts();
|
||||
},
|
||||
|
||||
// Fetch site list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('SiteName', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/organization/site?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch account list for dropdown
|
||||
async fetchAccounts() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/account`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
this.accountsList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch accounts:', err);
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new site
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
SiteID: null,
|
||||
SiteCode: "",
|
||||
SiteName: "",
|
||||
AccountID: "",
|
||||
Parent: "",
|
||||
ME: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit site
|
||||
async editSite(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/site/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load site data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.SiteName?.trim()) e.SiteName = "Site name is required";
|
||||
if (!this.form.SiteCode?.trim()) e.SiteCode = "Site code is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save site
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const res = await fetch(`${BASEURL}api/organization/site`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save site");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(site) {
|
||||
this.deleteTarget = site;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete site
|
||||
async deleteSite() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/site`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ SiteID: this.deleteTarget.SiteID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete site");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
129
app/Views/v2/master/organization/workstation_dialog.php
Normal file
129
app/Views/v2/master/organization/workstation_dialog.php
Normal file
@ -0,0 +1,129 @@
|
||||
<!-- Workstation Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-desktop" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Workstation' : 'New Workstation'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Workstation Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.WorkstationName && 'input-error'"
|
||||
x-model="form.WorkstationName"
|
||||
placeholder="Chemistry Analyzer 1"
|
||||
/>
|
||||
<label class="label" x-show="errors.WorkstationName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.WorkstationName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Workstation Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.WorkstationCode && 'input-error'"
|
||||
x-model="form.WorkstationCode"
|
||||
placeholder="WS-CH-01"
|
||||
/>
|
||||
<label class="label" x-show="errors.WorkstationCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.WorkstationCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Department</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<template x-for="d in departmentsList" :key="d.DepartmentID">
|
||||
<option :value="d.DepartmentID" x-text="d.DepartmentName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Type</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Type">
|
||||
<option value="">Select Type</option>
|
||||
<option value="1">Manual</option>
|
||||
<option value="2">Automated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Status</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Enable">
|
||||
<option value="1">Enabled</option>
|
||||
<option value="0">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Link To Workstation</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.LinkTo">
|
||||
<option value="">None</option>
|
||||
<template x-for="w in list" :key="w.WorkstationID">
|
||||
<option :value="w.WorkstationID" x-text="w.WorkstationName" :disabled="w.WorkstationID == form.WorkstationID"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Workstation'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
330
app/Views/v2/master/organization/workstations_index.php
Normal file
330
app/Views/v2/master/organization/workstations_index.php
Normal file
@ -0,0 +1,330 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="workstations()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-600 to-purple-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-desktop text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Organization Workstations</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage lab workstations and equipment units</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search workstations..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Workstation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading workstations...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Workstation Name</th>
|
||||
<th>Code</th>
|
||||
<th>Department</th>
|
||||
<th>Status</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No workstations found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Workstation
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Workstation Rows -->
|
||||
<template x-for="ws in list" :key="ws.WorkstationID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="ws.WorkstationID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="ws.WorkstationName || '-'"></div>
|
||||
</td>
|
||||
<td class="font-mono text-sm" x-text="ws.WorkstationCode || '-'"></td>
|
||||
<td x-text="ws.DepartmentName || '-'"></td>
|
||||
<td>
|
||||
<span class="badge badge-sm" :class="ws.Enable == 1 ? 'badge-success' : 'badge-ghost'" x-text="ws.Enable == 1 ? 'Active' : 'Disabled'"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editWorkstation(ws.WorkstationID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(ws)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/organization/workstation_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete workstation <strong x-text="deleteTarget?.WorkstationName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteWorkstation()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function workstations() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
departmentsList: [],
|
||||
keyword: "",
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
WorkstationID: null,
|
||||
WorkstationCode: "",
|
||||
WorkstationName: "",
|
||||
DepartmentID: "",
|
||||
Type: "",
|
||||
Enable: 1,
|
||||
LinkTo: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
await this.fetchDepartments();
|
||||
},
|
||||
|
||||
// Fetch workstation list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('WorkstationName', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/organization/workstation?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch department list for dropdown
|
||||
async fetchDepartments() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/department`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
this.departmentsList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch departments:', err);
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new workstation
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
WorkstationID: null,
|
||||
WorkstationCode: "",
|
||||
WorkstationName: "",
|
||||
DepartmentID: "",
|
||||
Type: "",
|
||||
Enable: 1,
|
||||
LinkTo: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit workstation
|
||||
async editWorkstation(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/workstation/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load workstation data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.WorkstationName?.trim()) e.WorkstationName = "Workstation name is required";
|
||||
if (!this.form.WorkstationCode?.trim()) e.WorkstationCode = "Workstation code is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save workstation
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const res = await fetch(`${BASEURL}api/organization/workstation`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save workstation");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(ws) {
|
||||
this.deleteTarget = ws;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete workstation
|
||||
async deleteWorkstation() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/workstation`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ WorkstationID: this.deleteTarget.WorkstationID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete workstation");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
143
app/Views/v2/master/specimen/container_dialog.php
Normal file
143
app/Views/v2/master/specimen/container_dialog.php
Normal file
@ -0,0 +1,143 @@
|
||||
<!-- Container Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-flask-vial" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Container' : 'New Container'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Container Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.ConName && 'input-error'"
|
||||
x-model="form.ConName"
|
||||
placeholder="Gold Top Tube"
|
||||
/>
|
||||
<label class="label" x-show="errors.ConName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.ConName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Container Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.ConCode && 'input-error'"
|
||||
x-model="form.ConCode"
|
||||
placeholder="GTT-10"
|
||||
/>
|
||||
<label class="label" x-show="errors.ConCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.ConCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Cap Color</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Color"
|
||||
placeholder="e.g. Gold, Red, Lavender"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-20 pt-2"
|
||||
x-model="form.ConDesc"
|
||||
placeholder="Tube description and usage notes..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Additive</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Additive"
|
||||
placeholder="SST / EDTA / Heparin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Class</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ConClass"
|
||||
placeholder="Tube / Swab"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<option value="">Select Site</option>
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Container'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
335
app/Views/v2/master/specimen/containers_index.php
Normal file
335
app/Views/v2/master/specimen/containers_index.php
Normal file
@ -0,0 +1,335 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="containers()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-pink-600 to-pink-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-flask-vial text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Container Definitions</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage specimen collection containers and tubes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search containers..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Container
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading containers...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Container Name</th>
|
||||
<th>Code</th>
|
||||
<th>Color</th>
|
||||
<th>Additive</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No containers found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Container
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Container Rows -->
|
||||
<template x-for="con in list" :key="con.ConDefID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="con.ConDefID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="con.ConName || '-'"></div>
|
||||
</td>
|
||||
<td class="font-mono text-sm" x-text="con.ConCode || '-'"></td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-3 h-3 rounded-full border border-slate-300" :style="`background-color: ${con.Color || 'transparent'}`"></div>
|
||||
<span x-text="con.ColorTxt || con.Color || '-'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td x-text="con.AdditiveTxt || con.Additive || '-'"></td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editContainer(con.ConDefID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(con)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/specimen/container_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete container <strong x-text="deleteTarget?.ConName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteContainer()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function containers() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
sitesList: [],
|
||||
keyword: "",
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
ConDefID: null,
|
||||
ConCode: "",
|
||||
ConName: "",
|
||||
ConDesc: "",
|
||||
Additive: "",
|
||||
ConClass: "",
|
||||
Color: "",
|
||||
SiteID: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
await this.fetchSites();
|
||||
},
|
||||
|
||||
// Fetch container list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('ConName', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/specimen/container?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch site list for dropdown
|
||||
async fetchSites() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/organization/site`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
this.sitesList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch sites:', err);
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new container
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
ConDefID: null,
|
||||
ConCode: "",
|
||||
ConName: "",
|
||||
ConDesc: "",
|
||||
Additive: "",
|
||||
ConClass: "",
|
||||
Color: "",
|
||||
SiteID: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit container
|
||||
async editContainer(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/specimen/container/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load container data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.ConName?.trim()) e.ConName = "Container name is required";
|
||||
if (!this.form.ConCode?.trim()) e.ConCode = "Container code is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save container
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const res = await fetch(`${BASEURL}api/specimen/container`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save container");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(con) {
|
||||
this.deleteTarget = con;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete container
|
||||
async deleteContainer() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/specimen/container`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ConDefID: this.deleteTarget.ConDefID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete container");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
138
app/Views/v2/master/specimen/preparation_dialog.php
Normal file
138
app/Views/v2/master/specimen/preparation_dialog.php
Normal file
@ -0,0 +1,138 @@
|
||||
<!-- Specimen Prep Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-mortar-pestle" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Preparation' : 'New Preparation'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.Description && 'input-error'"
|
||||
x-model="form.Description"
|
||||
placeholder="Centrifugation 3000rpm"
|
||||
/>
|
||||
<label class="label" x-show="errors.Description">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.Description"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Method</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Method"
|
||||
placeholder="Centrifuge / Aliqout / Heat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Additive</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Additive"
|
||||
placeholder="None"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Qty</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input text-center"
|
||||
x-model="form.AddQty"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Unit</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input text-center"
|
||||
x-model="form.AddUnit"
|
||||
placeholder="ml"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Preparation Start</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="input"
|
||||
x-model="form.PrepStart"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Preparation End</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="input"
|
||||
x-model="form.PrepEnd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Preparation'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
317
app/Views/v2/master/specimen/preparations_index.php
Normal file
317
app/Views/v2/master/specimen/preparations_index.php
Normal file
@ -0,0 +1,317 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="preparations()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-orange-600 to-orange-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-mortar-pestle text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Specimen Preparations</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage specimen processing and preparation methods</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search preparations..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Preparation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading preparations...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Description</th>
|
||||
<th>Method</th>
|
||||
<th>Additive</th>
|
||||
<th>Qty/Unit</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No preparations found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Preparation
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Prep Rows -->
|
||||
<template x-for="prep in list" :key="prep.SpcPrpID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="prep.SpcPrpID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="prep.Description || '-'"></div>
|
||||
</td>
|
||||
<td x-text="prep.Method || '-'"></td>
|
||||
<td x-text="prep.Additive || '-'"></td>
|
||||
<td>
|
||||
<span x-text="prep.AddQty || '-'"></span>
|
||||
<span class="text-xs opacity-60" x-text="prep.AddUnit"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editPrep(prep.SpcPrpID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(prep)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/master/specimen/preparation_dialog') ?>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete preparation <strong x-text="deleteTarget?.Description"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deletePrep()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function preparations() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
keyword: "",
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
SpcPrpID: null,
|
||||
Description: "",
|
||||
Method: "",
|
||||
Additive: "",
|
||||
AddQty: "",
|
||||
AddUnit: "",
|
||||
PrepStart: "",
|
||||
PrepEnd: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
},
|
||||
|
||||
// Fetch prep list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('Description', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/specimen/preparation?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new prep
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
SpcPrpID: null,
|
||||
Description: "",
|
||||
Method: "",
|
||||
Additive: "",
|
||||
AddQty: "",
|
||||
AddUnit: "",
|
||||
PrepStart: "",
|
||||
PrepEnd: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit prep
|
||||
async editPrep(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/specimen/preparation/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load prep data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.Description?.trim()) e.Description = "Description is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save prep
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const res = await fetch(`${BASEURL}api/specimen/preparation`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save preparation");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(prep) {
|
||||
this.deleteTarget = prep;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete prep
|
||||
async deletePrep() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/specimen/preparation`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ SpcPrpID: this.deleteTarget.SpcPrpID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete preparation");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
364
app/Views/v2/master/tests/calc_dialog.php
Normal file
364
app/Views/v2/master/tests/calc_dialog.php
Normal file
@ -0,0 +1,364 @@
|
||||
<!-- Calculation Dialog (for CALC type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'CALC' || form.TypeCode === 'CALC')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center">
|
||||
<i class="fa-solid fa-calculator text-amber-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Calculated Test' : 'New Calculated Test'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Derived/Calculated Test Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Calculation Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-warning gap-1">
|
||||
<i class="fa-solid fa-calculator"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'formula' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'formula'">
|
||||
<i class="fa-solid fa-calculator mr-1"></i> Formula
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-cog mr-1"></i> Config
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Tab: Basic Information (includes Org and Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-amber-500"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Code <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode"
|
||||
placeholder="e.g., BMI, eGFR, LDL_C" maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Body Mass Index, eGFR" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Test name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="e.g., Calculated based on weight and height..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-building text-amber-500"></i> Organization
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Discipline</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<option value="1">Hematology</option>
|
||||
<option value="2">Chemistry</option>
|
||||
<option value="3">Microbiology</option>
|
||||
<option value="4">Urinalysis</option>
|
||||
<option value="10">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Department</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<option value="1">Lab Hematology</option>
|
||||
<option value="2">Lab Chemistry</option>
|
||||
<option value="3">Lab Microbiology</option>
|
||||
<option value="4">Lab Urinalysis</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-amber-500"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Options</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Count Stat</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Formula Configuration -->
|
||||
<div x-show="activeTab === 'formula'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Formula Input Variables <span
|
||||
class="text-error">*</span></span>
|
||||
<span class="label-text-alt text-xs">Comma-separated test codes (these become variable names)</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono w-full" x-model="form.FormulaInput"
|
||||
placeholder="e.g., WEIGHT,HEIGHT,AGE,SCR" />
|
||||
<p class="text-xs mt-1" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i>
|
||||
Enter test codes that will be used as input variables. Use comma to separate multiple variables.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Formula Expression <span class="text-error">*</span></span>
|
||||
<span class="label-text-alt text-xs">JavaScript expression using variable names</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered font-mono text-sm w-full" x-model="form.FormulaCode"
|
||||
placeholder="e.g., WEIGHT / ((HEIGHT/100) * (HEIGHT/100))" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Available Functions Help -->
|
||||
<div class="p-3 rounded bg-opacity-20" style="background: rgb(var(--color-bg-tertiary));">
|
||||
<h5 class="font-semibold text-sm mb-2">Available Functions:</h5>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs font-mono" style="color: rgb(var(--color-text-muted));">
|
||||
<div><code>ABS(x)</code> - Absolute value</div>
|
||||
<div><code>ROUND(x, d)</code> - Round to d decimals</div>
|
||||
<div><code>MIN(a, b, ...)</code> - Minimum value</div>
|
||||
<div><code>MAX(a, b, ...)</code> - Maximum value</div>
|
||||
<div><code>IF(cond, t, f)</code> - Conditional</div>
|
||||
<div><code>MEAN(a, b, ...)</code> - Average</div>
|
||||
<div><code>SQRT(x)</code> - Square root</div>
|
||||
<div><code>POW(x, y)</code> - Power (x^y)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formula Preview -->
|
||||
<div class="p-3 rounded border"
|
||||
style="background: rgb(var(--color-bg-secondary)); border-color: rgb(var(--color-border));">
|
||||
<h5 class="font-semibold text-sm mb-2 flex items-center gap-2">
|
||||
<i class="fa-solid fa-eye text-amber-500"></i>
|
||||
Formula Preview
|
||||
</h5>
|
||||
<template x-if="form.FormulaInput || form.FormulaCode">
|
||||
<div class="font-mono text-sm space-y-1">
|
||||
<div class="flex gap-2">
|
||||
<span class="opacity-60">Inputs:</span>
|
||||
<span x-text="form.FormulaInput || '(none)'"></span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="opacity-60">Formula:</span>
|
||||
<code x-text="form.FormulaCode || '(none)'"></code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!form.FormulaInput && !form.FormulaCode">
|
||||
<span class="text-sm opacity-50 italic">Enter formula inputs and expression above</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Result Configuration (includes Sample) -->
|
||||
<div x-show="activeTab === 'results'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Result Configuration -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask text-amber-500"></i> Result Configuration
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Result Unit</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit1"
|
||||
placeholder="e.g., kg/m2, mg/dL, %" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Decimal Places</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.Decimal" placeholder="2"
|
||||
min="0" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Expected TAT (min)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.ExpectedTAT"
|
||||
placeholder="5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Range Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-ruler-combined text-amber-500"></i> Reference Range
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Numeric Reference Fields -->
|
||||
<div class="grid grid-cols-4 gap-3 mt-3" x-show="form.RefType === 'NMRC' || form.RefType === 'AGE' || form.RefType === 'GENDER'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Reference Field -->
|
||||
<div class="mt-3" x-show="form.RefType === 'TEXT'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText" placeholder="e.g., Negative, Positive, etc." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-warning flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Calculated Test' : 'Create Calculated Test')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
305
app/Views/v2/master/tests/grp_dialog.php
Normal file
305
app/Views/v2/master/tests/grp_dialog.php
Normal file
@ -0,0 +1,305 @@
|
||||
<!-- Group Dialog (for GROUP type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'GROUP' || form.TypeCode === 'GROUP')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary bg-opacity-20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-layer-group text-primary text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Test Group' : 'New Test Group'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Group/Panel Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Group Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-primary gap-1">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-primary text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'members' ? 'bg-primary text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'members'">
|
||||
<i class="fa-solid fa-users mr-1"></i> Members
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Tab: Basic Information (includes Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-primary"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Group Code <span class="text-error">*</span></span>
|
||||
<span class="label-text-alt text-xs">Auto-generated from name</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode" placeholder="Auto-generated"
|
||||
maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Group Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Lipid Profile, CBC Panel" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Group name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="e.g., Comprehensive lipid analysis panel..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-primary"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Group Members -->
|
||||
<div x-show="activeTab === 'members'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-primary bg-opacity-10"
|
||||
style="border: 1px solid rgb(var(--color-primary));">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-users text-primary"></i>
|
||||
<span class="font-medium text-sm">Group Members (<span
|
||||
x-text="form.groupMembers?.length || 0"></span>)</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" @click="showMemberSelector = true">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Member List -->
|
||||
<template x-if="!form.groupMembers || form.groupMembers.length === 0">
|
||||
<div class="text-center py-8 rounded-lg border border-dashed"
|
||||
style="border-color: rgb(var(--color-border));">
|
||||
<i class="fa-solid fa-inbox text-3xl opacity-40 mb-2"></i>
|
||||
<p class="opacity-60">No members added yet</p>
|
||||
<p class="text-xs opacity-50">Click "Add Member" to add tests to this group</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="form.groupMembers && form.groupMembers.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Seq</th>
|
||||
<th class="w-10">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(member, index) in form.groupMembers" :key="index">
|
||||
<tr class="hover">
|
||||
<td><code class="text-xs" x-text="member.TestSiteCode"></code></td>
|
||||
<td x-text="member.TestSiteName"></td>
|
||||
<td>
|
||||
<span class="badge badge-xs" :class="{
|
||||
'badge-info': member.MemberTypeCode === 'TEST',
|
||||
'badge-success': member.MemberTypeCode === 'PARAM'
|
||||
}" x-text="member.MemberTypeCode || 'TEST'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="input input-xs w-16" x-model.number="member.SeqScr"
|
||||
placeholder="0" />
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error" @click="removeMember(index)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Quick Add Common Tests -->
|
||||
<div class="p-3 rounded-lg border border-dashed" style="border-color: rgb(var(--color-border));">
|
||||
<h4 class="font-medium text-sm mb-2 flex items-center gap-2">
|
||||
<i class="fa-solid fa-bolt text-amber-500"></i>
|
||||
Quick Add Common Tests
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HBA1C', 'TEST')">HbA1c</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('GLU_R', 'TEST')">Glucose (Random)</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('GLU_F', 'TEST')">Glucose
|
||||
(Fasting)</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('CHOL', 'TEST')">Cholesterol</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('TG', 'TEST')">Triglycerides</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HDL', 'TEST')">HDL</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('LDL', 'TEST')">LDL</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('VLDL', 'TEST')">VLDL</button>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('RBC', 'PARAM')">RBC</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('WBC', 'PARAM')">WBC</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HGB', 'PARAM')">HGB</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HCT', 'PARAM')">HCT</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('PLT', 'PARAM')">PLT</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('MCV', 'PARAM')">MCV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Member Selector Modal (outside tabs) -->
|
||||
<div x-show="showMemberSelector" x-cloak class="modal-overlay" x-transition>
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[80vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="font-bold text-lg">Select Test Members</h4>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="showMemberSelector = false">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<input type="text" class="input input-bordered w-full" placeholder="Search tests..." x-model="memberSearch" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<template
|
||||
x-for="test in availableTests.filter(t => t.TestSiteName?.toLowerCase().includes(memberSearch?.toLowerCase() || ''))"
|
||||
:key="test.TestSiteID">
|
||||
<label
|
||||
class="flex items-center gap-3 p-3 rounded-lg border cursor-pointer hover:bg-opacity-50 transition-colors"
|
||||
style="background: rgb(var(--color-bg-secondary)); border-color: rgb(var(--color-border));">
|
||||
<input type="checkbox" class="checkbox checkbox-sm"
|
||||
:checked="form.groupMembers?.some(m => m.TestSiteID === test.TestSiteID)"
|
||||
@change="toggleMember(test)" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium" x-text="test.TestSiteName"></div>
|
||||
<div class="text-xs flex items-center gap-2">
|
||||
<code x-text="test.TestSiteCode"></code>
|
||||
<span class="badge badge-xs" :class="{
|
||||
'badge-info': test.TypeCode === 'TEST',
|
||||
'badge-success': test.TypeCode === 'PARAM'
|
||||
}" x-text="test.TypeCode"></span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost" @click="showMemberSelector = false">Cancel</button>
|
||||
<button class="btn btn-primary" @click="showMemberSelector = false; $forceUpdate()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Group' : 'Create Group')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
386
app/Views/v2/master/tests/param_dialog.php
Normal file
386
app/Views/v2/master/tests/param_dialog.php
Normal file
@ -0,0 +1,386 @@
|
||||
<!-- Parameter Dialog (for PARAM type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'PARAM' || form.TypeCode === 'PARAM')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<i class="fa-solid fa-sliders text-emerald-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Parameter' : 'New Parameter'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Parameter/Component Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Parameter Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-success gap-1">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-cog mr-1"></i> Config
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'valueset' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'valueset'">
|
||||
<i class="fa-solid fa-list-ul mr-1"></i> VSet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Tab: Basic Information (includes Org, Sample, and Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-emerald-500"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Parameter Code <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode"
|
||||
placeholder="e.g., RBC, WBC, HGB" maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Parameter Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Red Blood Cell Count" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Parameter name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="Optional description..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-building text-emerald-500"></i> Organization
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Discipline</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<option value="1">Hematology</option>
|
||||
<option value="2">Chemistry</option>
|
||||
<option value="3">Microbiology</option>
|
||||
<option value="4">Urinalysis</option>
|
||||
<option value="5">Immunology</option>
|
||||
<option value="6">Serology</option>
|
||||
<option value="10">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Department</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<option value="1">Lab Hematology</option>
|
||||
<option value="2">Lab Chemistry</option>
|
||||
<option value="3">Lab Microbiology</option>
|
||||
<option value="4">Lab Urinalysis</option>
|
||||
<option value="5">Lab Immunology</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-emerald-500"></i> Sample
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., Automated Cell Counter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-emerald-500"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Options</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Count Stat</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Result Configuration (includes Sample & Method) -->
|
||||
<div x-show="activeTab === 'results'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Result Configuration -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask text-emerald-500"></i> Result Configuration
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Result Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.ResultType">
|
||||
<option value="">Select Result Type</option>
|
||||
<option value="NMRIC">Numeric</option>
|
||||
<option value="TEXT">Text</option>
|
||||
<option value="VSET">Value Set (Select)</option>
|
||||
<option value="RANGE">Range with Reference</option>
|
||||
<option value="DTTM">Date/Time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 1</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit1"
|
||||
placeholder="e.g., 10^6/µL, g/dL" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 2 (SI)</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit2"
|
||||
placeholder="e.g., 10^12/L, g/L" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Decimal Places</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.Decimal" placeholder="2"
|
||||
min="0" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Expected TAT (min)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.ExpectedTAT"
|
||||
placeholder="30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Range Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-ruler-combined text-emerald-500"></i> Reference Range
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Numeric Reference Fields -->
|
||||
<div class="grid grid-cols-4 gap-3 mt-3" x-show="form.RefType === 'NMRC' || form.RefType === 'AGE' || form.RefType === 'GENDER'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Reference Field -->
|
||||
<div class="mt-3" x-show="form.RefType === 'TEXT'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText" placeholder="e.g., Negative, Positive, etc." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Tab: Value Set Selection -->
|
||||
<div x-show="activeTab === 'valueset'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<template x-if="form.ResultType === 'VSET'">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Value Set</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.ValueSetID">
|
||||
<option value="">Select Value Set</option>
|
||||
<option value="1">Positive/Negative</option>
|
||||
<option value="2">+1 to +4</option>
|
||||
<option value="3">Absent/Present</option>
|
||||
<option value="4">Normal/Abnormal</option>
|
||||
<option value="5">Trace/+/++/+++</option>
|
||||
<option value="6">Yes/No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Default Value</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.DefaultValue"
|
||||
placeholder="Default selection" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="form.ResultType !== 'VSET'">
|
||||
<div class="p-8 text-center rounded-lg border bg-opacity-30"
|
||||
style="background: rgb(var(--color-bg-secondary)); border-color: rgb(var(--color-border));">
|
||||
<i class="fa-solid fa-list-ul text-4xl opacity-40 mb-2"></i>
|
||||
<p class="opacity-60">Value Set configuration is only available when Result Type is "Value Set (Select)"</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-success flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Parameter' : 'Create Parameter')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
344
app/Views/v2/master/tests/test_dialog.php
Normal file
344
app/Views/v2/master/tests/test_dialog.php
Normal file
@ -0,0 +1,344 @@
|
||||
<!-- Test Dialog (Base - for TEST type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'TEST' || form.TypeCode === 'TEST')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center">
|
||||
<i class="fa-solid fa-flask text-indigo-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Test' : 'New Test'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Laboratory Test Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Test Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-info gap-1">
|
||||
<i class="fa-solid fa-flask"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-cog mr-1"></i> Config
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Tab: Basic Information (includes Org, Sample, and Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-indigo-500"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Code <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode"
|
||||
placeholder="e.g., CBC, GLU, HB" maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Complete Blood Count" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Test name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="Optional description..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-building text-indigo-500"></i> Organization
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Discipline</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<option value="1">Hematology</option>
|
||||
<option value="2">Chemistry</option>
|
||||
<option value="3">Microbiology</option>
|
||||
<option value="4">Urinalysis</option>
|
||||
<option value="5">Immunology</option>
|
||||
<option value="6">Serology</option>
|
||||
<option value="10">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Department</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<option value="1">Lab Hematology</option>
|
||||
<option value="2">Lab Chemistry</option>
|
||||
<option value="3">Lab Microbiology</option>
|
||||
<option value="4">Lab Urinalysis</option>
|
||||
<option value="5">Lab Immunology</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample & Method Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-indigo-500"></i> Sample & Method
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., CBC Analyzer, Hexokinase" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-indigo-500"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Options</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Count Stat</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Result Configuration (includes Sample & Method) -->
|
||||
<div x-show="activeTab === 'results'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Result Configuration -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask text-indigo-500"></i> Result Configuration
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Result Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.ResultType">
|
||||
<option value="">Select Result Type</option>
|
||||
<option value="NMRIC">Numeric</option>
|
||||
<option value="TEXT">Text</option>
|
||||
<option value="VSET">Value Set (Select)</option>
|
||||
<option value="RANGE">Range with Reference</option>
|
||||
<option value="CALC">Calculated</option>
|
||||
<option value="DTTM">Date/Time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 1</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit1"
|
||||
placeholder="e.g., mg/dL, U/L, %" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 2</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit2"
|
||||
placeholder="e.g., mmol/L (optional)" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Decimal Places</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.Decimal" placeholder="2"
|
||||
min="0" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Expected TAT (min)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.ExpectedTAT"
|
||||
placeholder="60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Range Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-ruler-combined text-indigo-500"></i> Reference Range
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Numeric Reference Fields -->
|
||||
<div class="grid grid-cols-4 gap-3 mt-3" x-show="form.RefType === 'NMRC' || form.RefType === 'AGE' || form.RefType === 'GENDER'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Reference Field -->
|
||||
<div class="mt-3" x-show="form.RefType === 'TEXT'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText" placeholder="e.g., Negative, Positive, etc." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Test' : 'Create Test')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
780
app/Views/v2/master/tests/tests_index.php
Normal file
780
app/Views/v2/master/tests/tests_index.php
Normal file
@ -0,0 +1,780 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="tests()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-600 to-indigo-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-microscope text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Laboratory Tests</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage test definitions, parameters, and groups
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input type="text" placeholder="Search tests..." class="input flex-1 sm:w-80" x-model="keyword"
|
||||
@keyup.enter="fetchList()" />
|
||||
<select class="select w-40" x-model="filterType" @change="fetchList()">
|
||||
<option value="">All Types</option>
|
||||
<template x-for="(type, index) in (testTypes || [])" :key="(type?.VID ?? index)">
|
||||
<option :value="type?.VID" x-text="(type?.VValue || '') + ' - ' + (type?.VDesc || '')"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
Add Test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading tests...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Code</th>
|
||||
<th>Test Name</th>
|
||||
<th>Type</th>
|
||||
<th>Seq</th>
|
||||
<th>Visible</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No tests found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Test
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Test Rows -->
|
||||
<template x-for="test in list" :key="test.TestSiteID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="test.TestSiteID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-sm font-mono px-2 py-1 rounded"
|
||||
style="background: rgb(var(--color-bg-secondary)); color: rgb(var(--color-primary));"
|
||||
x-text="test.TestSiteCode || '-'"></code>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="test.TestSiteName || '-'"></div>
|
||||
<div class="text-xs mt-1" style="color: rgb(var(--color-text-muted));" x-text="test.Description || ''">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'badge-info': test.TypeCode === 'TEST',
|
||||
'badge-success': test.TypeCode === 'PARAM',
|
||||
'badge-warning': test.TypeCode === 'CALC',
|
||||
'badge-primary': test.TypeCode === 'GROUP',
|
||||
'badge-secondary': test.TypeCode === 'TITLE'
|
||||
}" x-text="test.TypeName || test.TypeCode || '-'"></span>
|
||||
</td>
|
||||
<td x-text="test.SeqScr || '-'"></td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<span class="badge badge-ghost badge-xs" title="Screen" x-show="test.VisibleScr == 1">
|
||||
<i class="fa-solid fa-desktop text-green-500"></i>
|
||||
</span>
|
||||
<span class="badge badge-ghost badge-xs" title="Report" x-show="test.VisibleRpt == 1">
|
||||
<i class="fa-solid fa-file-alt text-blue-500"></i>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="viewTest(test.TestSiteID)"
|
||||
title="View Details">
|
||||
<i class="fa-solid fa-eye text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editTest(test.TestSiteID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-indigo-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(test)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));"
|
||||
x-show="list && list.length > 0">
|
||||
<span class="text-sm" style="color: rgb(var(--color-text-muted));"
|
||||
x-text="'Showing ' + list.length + ' tests'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialogs -->
|
||||
<?= $this->include('v2/master/tests/test_dialog') ?>
|
||||
<?= $this->include('v2/master/tests/param_dialog') ?>
|
||||
<?= $this->include('v2/master/tests/calc_dialog') ?>
|
||||
<?= $this->include('v2/master/tests/grp_dialog') ?>
|
||||
|
||||
<!-- View Details Modal -->
|
||||
<div x-show="showViewModal" x-cloak class="modal-overlay" @click.self="showViewModal = false">
|
||||
<div class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-microscope" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="viewData?.TestSiteName || 'Test Details'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="showViewModal = false">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Details Content -->
|
||||
<template x-if="viewData">
|
||||
<div class="space-y-6">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Code</div>
|
||||
<div class="font-mono font-semibold" x-text="viewData.TestSiteCode || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Type</div>
|
||||
<span class="badge" :class="{
|
||||
'badge-info': viewData.TypeCode === 'TEST',
|
||||
'badge-success': viewData.TypeCode === 'PARAM',
|
||||
'badge-warning': viewData.TypeCode === 'CALC',
|
||||
'badge-primary': viewData.TypeCode === 'GROUP',
|
||||
'badge-secondary': viewData.TypeCode === 'TITLE'
|
||||
}" x-text="viewData.TypeName || viewData.TypeCode || '-'"></span>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Sequence (Screen)</div>
|
||||
<div class="font-semibold" x-text="viewData.SeqScr || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Sequence (Report)</div>
|
||||
<div class="font-semibold" x-text="viewData.SeqRpt || '-'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type-specific Details -->
|
||||
<template x-if="viewData.TypeCode === 'TEST' || viewData.TypeCode === 'PARAM'">
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask"></i> Technical Details
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Result Type</div>
|
||||
<div x-text="viewData.testdeftech?.[0]?.ResultType || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Unit</div>
|
||||
<div x-text="viewData.testdeftech?.[0]?.Unit1 || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Method</div>
|
||||
<div x-text="viewData.testdeftech?.[0]?.Method || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Expected TAT</div>
|
||||
<div x-text="(viewData.testdeftech?.[0]?.ExpectedTAT || '-') + ' min'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="viewData.TypeCode === 'CALC'">
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-calculator"></i> Calculation Details
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Formula Input</div>
|
||||
<div class="font-mono text-sm" x-text="viewData.testdefcal?.[0]?.FormulaInput || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Formula Code</div>
|
||||
<div class="font-mono text-sm" x-text="viewData.testdefcal?.[0]?.FormulaCode || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Result Unit</div>
|
||||
<div x-text="viewData.testdefcal?.[0]?.Unit1 || '-'"></div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Decimal</div>
|
||||
<div x-text="viewData.testdefcal?.[0]?.Decimal || '-'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="viewData.TypeCode === 'GROUP'">
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-layer-group"></i> Group Members
|
||||
</h4>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="member in (viewData.testdefgrp || [])" :key="member.TestGrpID">
|
||||
<tr>
|
||||
<td><code x-text="member.TestSiteCode"></code></td>
|
||||
<td x-text="member.TestSiteName"></td>
|
||||
<td>
|
||||
<span class="badge badge-xs" :class="{
|
||||
'badge-info': member.MemberTypeCode === 'TEST',
|
||||
'badge-success': member.MemberTypeCode === 'PARAM'
|
||||
}" x-text="member.MemberTypeCode"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="!viewData.testdefgrp || viewData.testdefgrp.length === 0">
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">No members</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Test Mappings -->
|
||||
<template x-if="viewData.testmap && viewData.testmap.length > 0">
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-link"></i> Test Mappings
|
||||
</h4>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host Type</th>
|
||||
<th>Host Code</th>
|
||||
<th>Client Type</th>
|
||||
<th>Client Code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="map in viewData.testmap" :key="map.TestMapID">
|
||||
<tr>
|
||||
<td x-text="map.HostType || '-'"></td>
|
||||
<td x-text="map.HostTestCode || '-'"></td>
|
||||
<td x-text="map.ClientType || '-'"></td>
|
||||
<td><code x-text="map.ClientTestCode || '-'"></code></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="showViewModal = false">Close</button>
|
||||
<button class="btn btn-primary flex-1" @click="editTest(viewData?.TestSiteID)">
|
||||
<i class="fa-solid fa-edit mr-2"></i> Edit Test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div x-show="showDeleteModal" x-cloak class="modal-overlay" @click.self="showDeleteModal = false">
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete test <strong x-text="deleteTarget?.TestSiteName"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteTest()"
|
||||
:disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function tests() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
testTypes: [],
|
||||
keyword: "",
|
||||
filterType: "",
|
||||
|
||||
// View Modal
|
||||
showViewModal: false,
|
||||
viewData: null,
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
activeTab: 'basic',
|
||||
|
||||
// Member Selector
|
||||
showMemberSelector: false,
|
||||
memberSearch: '',
|
||||
availableTests: [],
|
||||
|
||||
form: {
|
||||
TestSiteID: null,
|
||||
SiteID: 1,
|
||||
TestSiteCode: "",
|
||||
TestSiteName: "",
|
||||
TestType: "",
|
||||
Description: "",
|
||||
SeqScr: 0,
|
||||
SeqRpt: 0,
|
||||
IndentLeft: 0,
|
||||
VisibleScr: 1,
|
||||
VisibleRpt: 1,
|
||||
CountStat: 1,
|
||||
// Technical fields
|
||||
DisciplineID: "",
|
||||
DepartmentID: "",
|
||||
ResultType: "",
|
||||
RefType: "",
|
||||
Unit1: "",
|
||||
Unit2: "",
|
||||
Decimal: 2,
|
||||
Method: "",
|
||||
SampleType: "",
|
||||
ExpectedTAT: 60,
|
||||
RefMin: null,
|
||||
RefMax: null,
|
||||
RefText: "",
|
||||
CriticalLow: null,
|
||||
CriticalHigh: null,
|
||||
// Calculation fields
|
||||
FormulaInput: "",
|
||||
FormulaCode: "",
|
||||
// Value Set fields
|
||||
ValueSetID: "",
|
||||
DefaultValue: "",
|
||||
// Group members
|
||||
groupMembers: []
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchTestTypes();
|
||||
await this.fetchAvailableTests();
|
||||
// Small delay to ensure Alpine has processed testTypes
|
||||
setTimeout(() => {
|
||||
this.fetchList();
|
||||
}, 50);
|
||||
},
|
||||
|
||||
// Fetch test types from valueset
|
||||
async fetchTestTypes() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef/27`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.testTypes = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch test types:', err);
|
||||
// Fallback to hardcoded types
|
||||
this.testTypes = [
|
||||
{ VID: 1, VValue: 'TEST', VDesc: 'Test' },
|
||||
{ VID: 2, VValue: 'PARAM', VDesc: 'Parameter' },
|
||||
{ VID: 3, VValue: 'CALC', VDesc: 'Calculated' },
|
||||
{ VID: 4, VValue: 'GROUP', VDesc: 'Group' },
|
||||
{ VID: 5, VValue: 'TITLE', VDesc: 'Title' }
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch available tests for group members
|
||||
async fetchAvailableTests() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/tests`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.availableTests = (data.data || []).filter(t =>
|
||||
t.TypeCode === 'TEST' || t.TypeCode === 'PARAM'
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch available tests:', err);
|
||||
this.availableTests = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Get type display name
|
||||
getTypeName(vid) {
|
||||
const code = this.getTypeCode(vid);
|
||||
const typeMap = {
|
||||
'TEST': 'Test',
|
||||
'PARAM': 'Parameter',
|
||||
'CALC': 'Calculated Test',
|
||||
'GROUP': 'Group',
|
||||
'TITLE': 'Title'
|
||||
};
|
||||
return typeMap[code] || 'Test';
|
||||
},
|
||||
|
||||
// Fetch test list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('TestSiteName', this.keyword);
|
||||
if (this.filterType) params.append('TestType', this.filterType);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/tests?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// View test details
|
||||
async viewTest(id) {
|
||||
this.viewData = null;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/tests/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.viewData = data.data;
|
||||
this.showViewModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load test details');
|
||||
}
|
||||
},
|
||||
|
||||
// Show form for new test
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.activeTab = 'basic';
|
||||
this.form = {
|
||||
TestSiteID: null,
|
||||
SiteID: 1,
|
||||
TestSiteCode: "",
|
||||
TestSiteName: "",
|
||||
TestType: "",
|
||||
Description: "",
|
||||
SeqScr: 0,
|
||||
SeqRpt: 0,
|
||||
IndentLeft: 0,
|
||||
VisibleScr: 1,
|
||||
VisibleRpt: 1,
|
||||
CountStat: 1,
|
||||
DisciplineID: "",
|
||||
DepartmentID: "",
|
||||
ResultType: "",
|
||||
RefType: "",
|
||||
Unit1: "",
|
||||
Unit2: "",
|
||||
Decimal: 2,
|
||||
Method: "",
|
||||
SampleType: "",
|
||||
ExpectedTAT: 60,
|
||||
RefMin: null,
|
||||
RefMax: null,
|
||||
RefText: "",
|
||||
CriticalLow: null,
|
||||
CriticalHigh: null,
|
||||
FormulaInput: "",
|
||||
FormulaCode: "",
|
||||
ValueSetID: "",
|
||||
DefaultValue: "",
|
||||
groupMembers: []
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit test
|
||||
async editTest(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
this.activeTab = 'basic';
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/tests/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
const testData = data.data;
|
||||
// Map TypeCode to TestType (VID) for proper dialog display
|
||||
const typeCode = testData.TypeCode || '';
|
||||
let testTypeVid = testData.TestType || null;
|
||||
|
||||
// Convert TypeCode string to VID if needed
|
||||
if (!testTypeVid && typeCode) {
|
||||
const typeMap = { 'TEST': 1, 'PARAM': 2, 'CALC': 3, 'GROUP': 4, 'TITLE': 5 };
|
||||
testTypeVid = typeMap[typeCode] || null;
|
||||
}
|
||||
|
||||
this.form = {
|
||||
...this.form,
|
||||
...testData,
|
||||
TestType: testTypeVid,
|
||||
// Store TypeCode directly for dialog display
|
||||
TypeCode: typeCode,
|
||||
// Preserve group members if editing group
|
||||
groupMembers: testData.testdefgrp || []
|
||||
};
|
||||
this.showModal = true;
|
||||
this.showViewModal = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load test data');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.TestSiteName?.trim()) e.TestSiteName = "Test name is required";
|
||||
if (!this.form.TestSiteCode?.trim()) e.TestSiteCode = "Test code is required";
|
||||
if (!this.form.TestType) e.TestType = "Test type is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.showMemberSelector = false;
|
||||
this.errors = {};
|
||||
this.memberSearch = '';
|
||||
},
|
||||
|
||||
// Toggle group member
|
||||
toggleMember(test) {
|
||||
if (!this.form.groupMembers) {
|
||||
this.form.groupMembers = [];
|
||||
}
|
||||
const index = this.form.groupMembers.findIndex(m => m.TestSiteID === test.TestSiteID);
|
||||
if (index > -1) {
|
||||
this.form.groupMembers.splice(index, 1);
|
||||
} else {
|
||||
this.form.groupMembers.push({
|
||||
TestSiteID: test.TestSiteID,
|
||||
TestSiteCode: test.TestSiteCode,
|
||||
TestSiteName: test.TestSiteName,
|
||||
MemberTypeCode: test.TypeCode || 'TEST',
|
||||
SeqScr: 0
|
||||
});
|
||||
}
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
// Remove group member
|
||||
removeMember(index) {
|
||||
this.form.groupMembers.splice(index, 1);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
// Add common member quickly
|
||||
addCommonMember(code, type) {
|
||||
const test = this.availableTests.find(t => t.TestSiteCode === code);
|
||||
if (test) {
|
||||
if (!this.form.groupMembers) {
|
||||
this.form.groupMembers = [];
|
||||
}
|
||||
const exists = this.form.groupMembers.some(m => m.TestSiteID === test.TestSiteID);
|
||||
if (!exists) {
|
||||
this.form.groupMembers.push({
|
||||
TestSiteID: test.TestSiteID,
|
||||
TestSiteCode: test.TestSiteCode,
|
||||
TestSiteName: test.TestSiteName,
|
||||
MemberTypeCode: type,
|
||||
SeqScr: this.form.groupMembers.length + 1
|
||||
});
|
||||
this.$forceUpdate();
|
||||
}
|
||||
} else {
|
||||
// Add as custom entry if not found
|
||||
if (!this.form.groupMembers) {
|
||||
this.form.groupMembers = [];
|
||||
}
|
||||
this.form.groupMembers.push({
|
||||
TestSiteID: null,
|
||||
TestSiteCode: code,
|
||||
TestSiteName: code,
|
||||
MemberTypeCode: type,
|
||||
SeqScr: this.form.groupMembers.length + 1
|
||||
});
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
// Save test
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const payload = { ...this.form };
|
||||
|
||||
// Handle group members for GROUP type
|
||||
if (this.getTypeCode(this.form.TestType) === 'GROUP' && this.form.groupMembers?.length > 0) {
|
||||
payload.groupMembers = this.form.groupMembers;
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASEURL}api/tests`, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save test");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(test) {
|
||||
this.deleteTarget = test;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete test
|
||||
async deleteTest() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/tests`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ TestSiteID: this.deleteTarget.TestSiteID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.message || "Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete test");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
},
|
||||
|
||||
// Get type code from VID (with fallback and null safety)
|
||||
getTypeCode(vid) {
|
||||
if (!vid) return '';
|
||||
// First try to find in loaded testTypes
|
||||
if (this.testTypes && Array.isArray(this.testTypes) && this.testTypes.length > 0) {
|
||||
const type = this.testTypes.find(t => t && t.VID === vid);
|
||||
if (type && type.VValue) return type.VValue;
|
||||
}
|
||||
// Fallback to hardcoded mapping
|
||||
const typeMap = { 1: 'TEST', 2: 'PARAM', 3: 'CALC', 4: 'GROUP', 5: 'TITLE' };
|
||||
return typeMap[vid] || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
156
app/Views/v2/master/valuesets/valueset_dialog.php
Normal file
156
app/Views/v2/master/valuesets/valueset_dialog.php
Normal file
@ -0,0 +1,156 @@
|
||||
<!-- Value Set Item Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-list-plus" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Item' : 'New Item'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- General Error -->
|
||||
<div x-show="errors.general" class="p-4 rounded-lg bg-rose-50 border border-rose-200" style="display: none;">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
||||
<p class="text-sm font-medium text-rose-700" x-text="errors.general"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Selection (only show if no selectedDef) -->
|
||||
<div x-show="!selectedDef" class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Category Assignment</h4>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Category <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<select class="select" x-model="form.VSetID" :class="errors.VSetID && 'input-error'">
|
||||
<option value="">Select Category</option>
|
||||
<template x-for="def in defsList" :key="def.VSetID">
|
||||
<option :value="def.VSetID" x-text="def.VSName"></option>
|
||||
</template>
|
||||
</select>
|
||||
<label class="label" x-show="errors.VSetID">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.VSetID"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Item Details</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Value / Key <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.VValue && 'input-error'"
|
||||
x-model="form.VValue"
|
||||
placeholder="e.g. M, F, Active"
|
||||
/>
|
||||
<label class="label" x-show="errors.VValue">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.VValue"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Display Order</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input text-center"
|
||||
x-model="form.VOrder"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Semantic Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-24 pt-2"
|
||||
x-model="form.VDesc"
|
||||
placeholder="Detailed description or definition of this value..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Information Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">System Information</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Item ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input text-center font-mono"
|
||||
x-model="form.VID"
|
||||
placeholder="Auto-generated"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input text-center font-mono"
|
||||
x-model="form.SiteID"
|
||||
placeholder="1"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Item' : 'Create Item')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
362
app/Views/v2/master/valuesets/valueset_nested_crud.php
Normal file
362
app/Views/v2/master/valuesets/valueset_nested_crud.php
Normal file
@ -0,0 +1,362 @@
|
||||
<!-- Nested ValueSet CRUD Modal -->
|
||||
<div
|
||||
x-show="showValueSetModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
style="z-index: 1000;"
|
||||
@click.self="$root.closeValueSetModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-0 max-w-5xl w-full max-h-[90vh] overflow-hidden"
|
||||
@click.stop
|
||||
x-data="valueSetItems()"
|
||||
x-init="selectedDef = $root.selectedDef; if(selectedDef) { fetchList(1); fetchDefsList(); }"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b flex items-center justify-between" style="background: rgb(var(--color-bg)); border-color: rgb(var(--color-border));">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white shadow-md" style="background: rgb(var(--color-primary));">
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));" x-text="selectedDef?.VSName || 'Value Items'"></h3>
|
||||
<p class="text-xs uppercase font-bold opacity-40" style="color: rgb(var(--color-text-muted));">Manage Category Items</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary btn-sm" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Add Item
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="$root.closeValueSetModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="p-4 border-b" style="border-color: rgb(var(--color-border));">
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-gray-400"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter items..."
|
||||
class="input input-sm w-full pl-10"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList(1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="overflow-y-auto" style="max-height: calc(90vh - 200px);">
|
||||
<!-- Loading Overlay -->
|
||||
<div x-show="loading" class="py-20 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading items...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-20">ID</th>
|
||||
<th>Value / Key</th>
|
||||
<th>Definition</th>
|
||||
<th class="text-center">Order</th>
|
||||
<th class="text-center w-32">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="py-20 text-center">
|
||||
<div class="flex flex-col items-center gap-2 opacity-30" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-4xl"></i>
|
||||
<p class="font-bold italic">No items found in this category</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Item
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Data Rows -->
|
||||
<template x-for="v in list" :key="v.VID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="v.VID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="v.VValue || '-'"></div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-sm opacity-70" x-text="v.VDesc || '-'"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="font-mono text-sm" x-text="v.VOrder || 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editValue(v.VID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(v)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Stats Footer -->
|
||||
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));" x-show="list && list.length > 0">
|
||||
<span class="text-sm" style="color: rgb(var(--color-text-muted));" x-text="list.length + ' items'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Form Dialog -->
|
||||
<?= $this->include('v2/master/valuesets/valueset_dialog') ?>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div x-show="showDeleteModal" x-cloak class="modal-overlay" style="z-index: 1100;">
|
||||
<div
|
||||
class="card p-8 max-w-md w-full shadow-2xl"
|
||||
x-show="showDeleteModal"
|
||||
x-transition
|
||||
>
|
||||
<div class="w-16 h-16 rounded-2xl bg-rose-500/10 flex items-center justify-center text-rose-500 mx-auto mb-6">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-center mb-2" style="color: rgb(var(--color-text));">Confirm Removal</h3>
|
||||
<p class="text-center text-sm mb-8" style="color: rgb(var(--color-text-muted));">
|
||||
Are you sure you want to delete <span class="font-bold text-rose-500" x-text="deleteTarget?.VValue"></span>?
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1 bg-rose-600 text-white hover:bg-rose-700 shadow-lg shadow-rose-600/20" @click="deleteValue()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm !border-white/20 !border-t-white"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function valueSetItems() {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
selectedDef: null,
|
||||
keyword: "",
|
||||
totalItems: 0,
|
||||
|
||||
// For dropdown population
|
||||
defsList: [],
|
||||
loadingDefs: false,
|
||||
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
VID: null,
|
||||
VSetID: "",
|
||||
VOrder: 0,
|
||||
VValue: "",
|
||||
VDesc: "",
|
||||
SiteID: 1
|
||||
},
|
||||
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
async fetchList(page = 1) {
|
||||
if (!this.selectedDef) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('VSetID', this.selectedDef.VSetID);
|
||||
if (this.keyword) params.append('param', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/valueset?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
this.totalItems = this.list.length;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
this.totalItems = 0;
|
||||
this.showToast('Failed to load items', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchDefsList() {
|
||||
this.loadingDefs = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef?limit=1000`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.defsList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch defs list:', err);
|
||||
this.defsList = [];
|
||||
} finally {
|
||||
this.loadingDefs = false;
|
||||
}
|
||||
},
|
||||
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
VID: null,
|
||||
VSetID: this.selectedDef?.VSetID || "",
|
||||
VOrder: 0,
|
||||
VValue: "",
|
||||
VDesc: "",
|
||||
SiteID: 1
|
||||
};
|
||||
this.errors = {};
|
||||
|
||||
// If no selectedDef, we need to load all defs for dropdown
|
||||
if (!this.selectedDef && this.defsList.length === 0) {
|
||||
this.fetchDefsList();
|
||||
}
|
||||
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
async editValue(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.showToast('Failed to load item data', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.VValue?.trim()) e.VValue = "Value is required";
|
||||
if (!this.form.VSetID) e.VSetID = "Category is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const url = this.isEditing ? `${BASEURL}api/valueset/${this.form.VID}` : `${BASEURL}api/valueset`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList(1);
|
||||
this.showToast(this.isEditing ? 'Item updated successfully' : 'Item created successfully', 'success');
|
||||
} else {
|
||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
||||
this.errors = { general: errorData.message || 'Failed to save' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Save failed:', err);
|
||||
this.errors = { general: err.message || 'An error occurred while saving' };
|
||||
this.showToast('Failed to save item', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete(v) {
|
||||
this.deleteTarget = v;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
async deleteValue() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/${this.deleteTarget.VID}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList(1);
|
||||
this.showToast('Item deleted successfully', 'success');
|
||||
} else {
|
||||
this.showToast('Failed to delete item', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', err);
|
||||
this.showToast('Failed to delete item', 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
},
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
if (this.$root && this.$root.showToast) {
|
||||
this.$root.showToast(message, type);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
122
app/Views/v2/master/valuesets/valuesetdef_dialog.php
Normal file
122
app/Views/v2/master/valuesets/valuesetdef_dialog.php
Normal file
@ -0,0 +1,122 @@
|
||||
<!-- Value Set Definition Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-layer-group-plus" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Category' : 'New Category'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- General Error -->
|
||||
<div x-show="errors.general" class="p-4 rounded-lg bg-rose-50 border border-rose-200" style="display: none;">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
||||
<p class="text-sm font-medium text-rose-700" x-text="errors.general"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Basic Information</h4>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Category Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.VSName && 'input-error'"
|
||||
x-model="form.VSName"
|
||||
placeholder="e.g. Gender, Country, Status"
|
||||
/>
|
||||
<label class="label" x-show="errors.VSName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.VSName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-24 pt-2"
|
||||
x-model="form.VSDesc"
|
||||
placeholder="Detailed description of this category..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">System Information</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input text-center font-mono"
|
||||
x-model="form.SiteID"
|
||||
placeholder="1"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Category ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input text-center font-mono"
|
||||
x-model="form.VSetID"
|
||||
placeholder="Auto-generated"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Category' : 'Create Category')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
679
app/Views/v2/master/valuesets/valuesets_index.php
Normal file
679
app/Views/v2/master/valuesets/valuesets_index.php
Normal file
@ -0,0 +1,679 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="valueSetManager()" x-init="init()">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-800 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-layer-group text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Value Set Manager</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage value set categories and their items</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Two Column Layout with Independent Scrolling -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<!-- LEFT PANEL: ValueSetDef List -->
|
||||
<div class="card overflow-hidden flex flex-col" style="height: calc(100vh - 280px); min-height: 400px;">
|
||||
<!-- Left Panel Header -->
|
||||
<div class="p-4 border-b flex items-center justify-between" style="border-color: rgb(var(--color-border));">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" style="background: rgb(var(--color-primary));">
|
||||
<i class="fa-solid fa-layer-group text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold" style="color: rgb(var(--color-text));">Categories</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Value Set Definitions</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" @click="showDefForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="p-3 border-b" style="border-color: rgb(var(--color-border));">
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-gray-400" style="z-index: 10;"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search categories..."
|
||||
class="input input-sm w-full input-with-icon"
|
||||
x-model="defKeyword"
|
||||
@keyup.enter="fetchDefs()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="defLoading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading categories...</p>
|
||||
</div>
|
||||
|
||||
<!-- Def List Table -->
|
||||
<div class="overflow-y-auto flex-1" x-show="!defLoading" x-cloak>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16">ID</th>
|
||||
<th>Category Name</th>
|
||||
<th class="w-20 text-center">Items</th>
|
||||
<th class="w-24 text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="!defList || defList.length === 0">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-2" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-folder-open text-4xl opacity-40"></i>
|
||||
<p class="text-sm">No categories found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showDefForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Category
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template x-for="def in defList" :key="def.VSetID">
|
||||
<tr
|
||||
class="hover:bg-opacity-50 cursor-pointer transition-colors"
|
||||
:class="selectedDef?.VSetID === def.VSetID ? 'bg-primary/10' : ''"
|
||||
@click="selectDef(def)"
|
||||
>
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="def.VSetID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="def.VSName || '-'"></div>
|
||||
<div class="text-xs opacity-50" x-text="def.VSDesc || ''"></div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-sm" x-text="(def.ItemCount || 0) + ' items'"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1" @click.stop>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editDef(def.VSetID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDeleteDef(def)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Left Panel Footer -->
|
||||
<div class="p-3 flex items-center justify-between text-xs" style="border-top: 1px solid rgb(var(--color-border));" x-show="defList && defList.length > 0">
|
||||
<span style="color: rgb(var(--color-text-muted));" x-text="defList.length + ' categories'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT PANEL: ValueSet Items -->
|
||||
<div class="card overflow-hidden flex flex-col" style="height: calc(100vh - 280px); min-height: 400px;">
|
||||
<!-- Right Panel Header -->
|
||||
<div class="p-4 border-b flex items-center justify-between" style="border-color: rgb(var(--color-border));">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" style="background: rgb(var(--color-secondary));">
|
||||
<i class="fa-solid fa-list-ul text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold" style="color: rgb(var(--color-text));">Items</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">
|
||||
<template x-if="selectedDef">
|
||||
<span x-text="selectedDef.VSName + ' Items'"></span>
|
||||
</template>
|
||||
<template x-if="!selectedDef">
|
||||
<span>Select a category to view items</span>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="showValueForm()"
|
||||
:disabled="!selectedDef"
|
||||
>
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Item
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar (Right Panel) -->
|
||||
<div class="p-3 border-b" style="border-color: rgb(var(--color-border));" x-show="selectedDef">
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-gray-400" style="z-index: 10;"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter items..."
|
||||
class="input input-sm w-full input-with-icon"
|
||||
x-model="valueKeyword"
|
||||
@keyup.enter="fetchValues()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State - No Selection -->
|
||||
<div x-show="!selectedDef" class="p-16 text-center" x-cloak>
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-hand-pointer text-5xl opacity-30"></i>
|
||||
<p class="text-lg font-medium">Select a category</p>
|
||||
<p class="text-sm opacity-60">Click on a category from the left panel to view and manage its items</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="valueLoading && selectedDef" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading items...</p>
|
||||
</div>
|
||||
|
||||
<!-- Value List Table -->
|
||||
<div class="overflow-y-auto flex-1" x-show="!valueLoading && selectedDef" x-cloak>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16">ID</th>
|
||||
<th>Value</th>
|
||||
<th>Description</th>
|
||||
<th class="w-16 text-center">Order</th>
|
||||
<th class="w-20 text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="!valueList || valueList.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-2" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-4xl opacity-40"></i>
|
||||
<p class="text-sm">No items found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showValueForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Item
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template x-for="value in valueList" :key="value.VID">
|
||||
<tr class="hover:bg-opacity-50">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="value.VID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="value.VValue || '-'"></div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-sm opacity-70" x-text="value.VDesc || '-'"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="font-mono text-sm" x-text="value.VOrder || 0"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editValue(value.VID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDeleteValue(value)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel Footer -->
|
||||
<div class="p-3 flex items-center justify-between text-xs" style="border-top: 1px solid rgb(var(--color-border));" x-show="valueList && valueList.length > 0 && selectedDef">
|
||||
<span style="color: rgb(var(--color-text-muted));" x-text="valueList.length + ' items'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Definition Form Dialog -->
|
||||
<?= $this->include('v2/master/valuesets/valuesetdef_dialog') ?>
|
||||
|
||||
<!-- Include Value Form Dialog -->
|
||||
<?= $this->include('v2/master/valuesets/valueset_dialog') ?>
|
||||
|
||||
<!-- Delete Category Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteDefModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteDefModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete category <strong x-text="deleteDefTarget?.VSName"></strong>?
|
||||
This will also delete all items in this category and cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteDefModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteDef()" :disabled="deletingDef">
|
||||
<span x-show="deletingDef" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deletingDef">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Value Confirmation Modal -->
|
||||
<div
|
||||
x-show="showDeleteValueModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteValueModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete item <strong x-text="deleteValueTarget?.VValue"></strong>?
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteValueModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteValue()" :disabled="deletingValue">
|
||||
<span x-show="deletingValue" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deletingValue">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function valueSetManager() {
|
||||
return {
|
||||
// State - Definitions
|
||||
defLoading: false,
|
||||
defList: [],
|
||||
defKeyword: "",
|
||||
|
||||
// State - Values
|
||||
valueLoading: false,
|
||||
valueList: [],
|
||||
valueKeyword: "",
|
||||
selectedDef: null,
|
||||
|
||||
// Definition Form
|
||||
showDefModal: false,
|
||||
isEditingDef: false,
|
||||
savingDef: false,
|
||||
defErrors: {},
|
||||
defForm: {
|
||||
VSetID: null,
|
||||
VSName: "",
|
||||
VSDesc: "",
|
||||
SiteID: 1
|
||||
},
|
||||
|
||||
// Value Form
|
||||
showValueModal: false,
|
||||
isEditingValue: false,
|
||||
savingValue: false,
|
||||
valueErrors: {},
|
||||
valueForm: {
|
||||
VID: null,
|
||||
VSetID: "",
|
||||
VOrder: 0,
|
||||
VValue: "",
|
||||
VDesc: "",
|
||||
SiteID: 1
|
||||
},
|
||||
|
||||
// Delete Definition
|
||||
showDeleteDefModal: false,
|
||||
deleteDefTarget: null,
|
||||
deletingDef: false,
|
||||
|
||||
// Delete Value
|
||||
showDeleteValueModal: false,
|
||||
deleteValueTarget: null,
|
||||
deletingValue: false,
|
||||
|
||||
// Dropdown data
|
||||
defsList: [],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchDefs();
|
||||
},
|
||||
|
||||
// ==================== DEFINITION METHODS ====================
|
||||
|
||||
async fetchDefs() {
|
||||
this.defLoading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.defKeyword) params.append('param', this.defKeyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.defList = data.data || [];
|
||||
|
||||
// Update selected def in list if exists
|
||||
if (this.selectedDef) {
|
||||
const updated = this.defList.find(d => d.VSetID === this.selectedDef.VSetID);
|
||||
if (updated) {
|
||||
this.selectedDef = updated;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.defList = [];
|
||||
this.showToast('Failed to load categories', 'error');
|
||||
} finally {
|
||||
this.defLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
showDefForm() {
|
||||
this.isEditingDef = false;
|
||||
this.defForm = {
|
||||
VSetID: null,
|
||||
VSName: "",
|
||||
VSDesc: "",
|
||||
SiteID: 1
|
||||
};
|
||||
this.defErrors = {};
|
||||
this.showDefModal = true;
|
||||
},
|
||||
|
||||
async editDef(id) {
|
||||
this.isEditingDef = true;
|
||||
this.defErrors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.defForm = { ...this.defForm, ...data.data };
|
||||
this.showDefModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.showToast('Failed to load category data', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
validateDef() {
|
||||
const e = {};
|
||||
if (!this.defForm.VSName?.trim()) e.VSName = "Category name is required";
|
||||
this.defErrors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
closeDefModal() {
|
||||
this.showDefModal = false;
|
||||
this.defErrors = {};
|
||||
},
|
||||
|
||||
async saveDef() {
|
||||
if (!this.validateDef()) return;
|
||||
|
||||
this.savingDef = true;
|
||||
try {
|
||||
const method = this.isEditingDef ? 'PATCH' : 'POST';
|
||||
const url = this.isEditingDef ? `${BASEURL}api/valuesetdef/${this.defForm.VSetID}` : `${BASEURL}api/valuesetdef`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.defForm),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.closeDefModal();
|
||||
await this.fetchDefs();
|
||||
this.showToast(this.isEditingDef ? 'Category updated successfully' : 'Category created successfully', 'success');
|
||||
} else {
|
||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
||||
this.defErrors = { general: errorData.message || 'Failed to save' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.defErrors = { general: 'Failed to save category' };
|
||||
this.showToast('Failed to save category', 'error');
|
||||
} finally {
|
||||
this.savingDef = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDeleteDef(def) {
|
||||
this.deleteDefTarget = def;
|
||||
this.showDeleteDefModal = true;
|
||||
},
|
||||
|
||||
async deleteDef() {
|
||||
if (!this.deleteDefTarget) return;
|
||||
|
||||
this.deletingDef = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef/${this.deleteDefTarget.VSetID}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteDefModal = false;
|
||||
if (this.selectedDef?.VSetID === this.deleteDefTarget.VSetID) {
|
||||
this.selectedDef = null;
|
||||
this.valueList = [];
|
||||
}
|
||||
await this.fetchDefs();
|
||||
this.showToast('Category deleted successfully', 'success');
|
||||
} else {
|
||||
this.showToast('Failed to delete category', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.showToast('Failed to delete category', 'error');
|
||||
} finally {
|
||||
this.deletingDef = false;
|
||||
this.deleteDefTarget = null;
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== VALUE METHODS ====================
|
||||
|
||||
selectDef(def) {
|
||||
this.selectedDef = def;
|
||||
this.fetchValues();
|
||||
},
|
||||
|
||||
async fetchValues() {
|
||||
if (!this.selectedDef) return;
|
||||
|
||||
this.valueLoading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('VSetID', this.selectedDef.VSetID);
|
||||
if (this.valueKeyword) params.append('param', this.valueKeyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/valueset?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.valueList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.valueList = [];
|
||||
this.showToast('Failed to load items', 'error');
|
||||
} finally {
|
||||
this.valueLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchDefsList() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef?limit=1000`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.defsList = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch defs list:', err);
|
||||
this.defsList = [];
|
||||
}
|
||||
},
|
||||
|
||||
showValueForm() {
|
||||
if (!this.selectedDef) {
|
||||
this.showToast('Please select a category first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditingValue = false;
|
||||
this.valueForm = {
|
||||
VID: null,
|
||||
VSetID: this.selectedDef.VSetID,
|
||||
VOrder: 0,
|
||||
VValue: "",
|
||||
VDesc: "",
|
||||
SiteID: 1
|
||||
};
|
||||
this.valueErrors = {};
|
||||
this.showValueModal = true;
|
||||
},
|
||||
|
||||
async editValue(id) {
|
||||
this.isEditingValue = true;
|
||||
this.valueErrors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.valueForm = { ...this.valueForm, ...data.data };
|
||||
this.showValueModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.showToast('Failed to load item data', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
validateValue() {
|
||||
const e = {};
|
||||
if (!this.valueForm.VValue?.trim()) e.VValue = "Value is required";
|
||||
if (!this.valueForm.VSetID) e.VSetID = "Category is required";
|
||||
this.valueErrors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
closeValueModal() {
|
||||
this.showValueModal = false;
|
||||
this.valueErrors = {};
|
||||
},
|
||||
|
||||
async saveValue() {
|
||||
if (!this.validateValue()) return;
|
||||
|
||||
this.savingValue = true;
|
||||
try {
|
||||
const method = this.isEditingValue ? 'PATCH' : 'POST';
|
||||
const url = this.isEditingValue ? `${BASEURL}api/valueset/${this.valueForm.VID}` : `${BASEURL}api/valueset`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.valueForm),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.closeValueModal();
|
||||
await this.fetchValues();
|
||||
await this.fetchDefs(); // Refresh item counts
|
||||
this.showToast(this.isEditingValue ? 'Item updated successfully' : 'Item created successfully', 'success');
|
||||
} else {
|
||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
||||
this.valueErrors = { general: errorData.message || 'Failed to save' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.valueErrors = { general: 'Failed to save item' };
|
||||
this.showToast('Failed to save item', 'error');
|
||||
} finally {
|
||||
this.savingValue = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDeleteValue(value) {
|
||||
this.deleteValueTarget = value;
|
||||
this.showDeleteValueModal = true;
|
||||
},
|
||||
|
||||
async deleteValue() {
|
||||
if (!this.deleteValueTarget) return;
|
||||
|
||||
this.deletingValue = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/${this.deleteValueTarget.VID}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteValueModal = false;
|
||||
await this.fetchValues();
|
||||
await this.fetchDefs(); // Refresh item counts
|
||||
this.showToast('Item deleted successfully', 'success');
|
||||
} else {
|
||||
this.showToast('Failed to delete item', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.showToast('Failed to delete item', 'error');
|
||||
} finally {
|
||||
this.deletingValue = false;
|
||||
this.deleteValueTarget = null;
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== UTILITIES ====================
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
if (this.$root && this.$root.showToast) {
|
||||
this.$root.showToast(message, type);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
207
app/Views/v2/patients/dialog_form.php
Normal file
207
app/Views/v2/patients/dialog_form.php
Normal file
@ -0,0 +1,207 @@
|
||||
<!-- Patient Form Modal -->
|
||||
<div
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-user-plus" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Patient' : 'New Patient'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-5">
|
||||
<!-- Patient ID -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Patient ID (MRN)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="Auto-generated if empty"
|
||||
x-model="form.PatientID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Name Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">First Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.NameFirst && 'input-error'"
|
||||
x-model="form.NameFirst"
|
||||
/>
|
||||
<label class="label" x-show="errors.NameFirst">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.NameFirst"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Middle Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.NameMiddle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Last Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.NameLast && 'input-error'"
|
||||
x-model="form.NameLast"
|
||||
/>
|
||||
<label class="label" x-show="errors.NameLast">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.NameLast"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gender & Birthdate -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Gender</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Gender">
|
||||
<option value="1">Male</option>
|
||||
<option value="2">Female</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Birth Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
class="input"
|
||||
x-model="form.Birthdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="divider">Contact Information</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Mobile Phone</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
class="input"
|
||||
placeholder="+62..."
|
||||
x-model="form.MobilePhone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input"
|
||||
placeholder="patient@email.com"
|
||||
x-model="form.EmailAddress1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="divider">Address</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Street Address</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Street_1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.City"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Province</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Province"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ZIP Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ZIP"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Patient'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
424
app/Views/v2/patients/patients_index.php
Normal file
424
app/Views/v2/patients/patients_index.php
Normal file
@ -0,0 +1,424 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div x-data="patients()" x-init="init()">
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<!-- Total Patients -->
|
||||
<div class="card group hover:shadow-xl transition-all">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Total Patients</p>
|
||||
<p class="text-3xl font-bold" style="color: rgb(var(--color-text));" x-text="stats.total">0</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform" style="background: rgba(var(--color-primary), 0.15);">
|
||||
<i class="fa-solid fa-users text-2xl" style="color: rgb(var(--color-primary));"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Today -->
|
||||
<div class="card group hover:shadow-xl transition-all">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">New Today</p>
|
||||
<p class="text-3xl font-bold text-emerald-500" x-text="stats.newToday">0</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-emerald-500/15 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-user-plus text-emerald-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending -->
|
||||
<div class="card group hover:shadow-xl transition-all">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Pending Visits</p>
|
||||
<p class="text-3xl font-bold text-amber-500" x-text="stats.pending">0</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-amber-500/15 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-clock text-amber-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Actions Bar -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<!-- Search -->
|
||||
<div class="flex w-full sm:w-auto gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, ID, phone..."
|
||||
class="input flex-1 sm:w-80"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-2"></i> New Patient
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Patient List Table -->
|
||||
<div class="card overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||
<p style="color: rgb(var(--color-text-muted));">Loading patients...</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto" x-show="!loading">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Patient ID</th>
|
||||
<th>Name</th>
|
||||
<th>Gender</th>
|
||||
<th>Birth Date</th>
|
||||
<th>Phone</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<template x-if="!list || list.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||
<p class="text-lg">No patients found</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add First Patient
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Patient Rows -->
|
||||
<template x-for="patient in list" :key="patient.InternalPID">
|
||||
<tr class="cursor-pointer hover:bg-opacity-50" @click="viewPatient(patient.InternalPID)">
|
||||
<td>
|
||||
<span class="badge badge-ghost font-mono text-xs" x-text="patient.PatientID || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold bg-gradient-to-br from-blue-600 to-blue-900">
|
||||
<span class="text-sm" x-text="(patient.NameFirst || '?')[0].toUpperCase()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="(patient.NameFirst || '') + ' ' + (patient.NameLast || '')"></div>
|
||||
<div class="text-xs" style="color: rgb(var(--color-text-muted));" x-text="patient.EmailAddress1 || ''"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge badge-sm"
|
||||
:class="patient.Gender == 1 ? 'badge-info' : 'badge-secondary'"
|
||||
x-text="patient.Gender == 1 ? 'Male' : patient.Gender == 2 ? 'Female' : '-'"
|
||||
></span>
|
||||
</td>
|
||||
<td x-text="formatDate(patient.Birthdate)"></td>
|
||||
<td x-text="patient.MobilePhone || patient.Phone || '-'"></td>
|
||||
<td class="text-center" @click.stop>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editPatient(patient.InternalPID)" title="Edit">
|
||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(patient)" title="Delete">
|
||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));" x-show="list && list.length > 0">
|
||||
<span class="text-sm" style="color: rgb(var(--color-text-muted));" x-text="'Showing ' + list.length + ' patients'"></span>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-ghost btn-sm">«</button>
|
||||
<button class="btn btn-primary btn-sm">1</button>
|
||||
<button class="btn btn-ghost btn-sm">2</button>
|
||||
<button class="btn btn-ghost btn-sm">3</button>
|
||||
<button class="btn btn-ghost btn-sm">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Form Dialog -->
|
||||
<?= $this->include('v2/patients/dialog_form') ?>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<div
|
||||
x-show="showDeleteModal"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="showDeleteModal = false"
|
||||
>
|
||||
<div class="modal-content p-6 max-w-md">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
Confirm Delete
|
||||
</h3>
|
||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||
Are you sure you want to delete patient <strong x-text="deleteTarget?.PatientID"></strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deletePatient()" :disabled="deleting">
|
||||
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||
<span x-show="!deleting">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
function patients() {
|
||||
return {
|
||||
// State
|
||||
loading: false,
|
||||
list: [],
|
||||
keyword: "",
|
||||
|
||||
// Stats
|
||||
stats: {
|
||||
total: 0,
|
||||
newToday: 0,
|
||||
pending: 0
|
||||
},
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
errors: {},
|
||||
form: {
|
||||
InternalPID: null,
|
||||
PatientID: "",
|
||||
NameFirst: "",
|
||||
NameMiddle: "",
|
||||
NameLast: "",
|
||||
Gender: 1,
|
||||
Birthdate: "",
|
||||
MobilePhone: "",
|
||||
EmailAddress1: "",
|
||||
Street_1: "",
|
||||
City: "",
|
||||
Province: "",
|
||||
ZIP: ""
|
||||
},
|
||||
|
||||
// Delete Modal
|
||||
showDeleteModal: false,
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
},
|
||||
|
||||
// Fetch patient list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('search', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/patient?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
this.list = data.data || [];
|
||||
this.stats.total = this.list.length;
|
||||
// Calculate new today (simplified - you may want server-side)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
this.stats.newToday = this.list.filter(p => p.CreateDate && p.CreateDate.startsWith(today)).length;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
},
|
||||
|
||||
// View patient details
|
||||
viewPatient(id) {
|
||||
// Could navigate to detail page or open drawer
|
||||
console.log('View patient:', id);
|
||||
},
|
||||
|
||||
// Show form for new patient
|
||||
showForm() {
|
||||
this.isEditing = false;
|
||||
this.form = {
|
||||
InternalPID: null,
|
||||
PatientID: "",
|
||||
NameFirst: "",
|
||||
NameMiddle: "",
|
||||
NameLast: "",
|
||||
Gender: 1,
|
||||
Birthdate: "",
|
||||
MobilePhone: "",
|
||||
EmailAddress1: "",
|
||||
Street_1: "",
|
||||
City: "",
|
||||
Province: "",
|
||||
ZIP: ""
|
||||
};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
// Edit patient
|
||||
async editPatient(id) {
|
||||
this.isEditing = true;
|
||||
this.errors = {};
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/patient/${id}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.data) {
|
||||
this.form = { ...this.form, ...data.data };
|
||||
this.showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to load patient');
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.NameFirst?.trim()) e.NameFirst = "First name is required";
|
||||
if (!this.form.NameLast?.trim()) e.NameLast = "Last name is required";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Close modal
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save patient
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
let res;
|
||||
if (this.isEditing && this.form.InternalPID) {
|
||||
res = await fetch(`${BASEURL}api/patient`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
} else {
|
||||
res = await fetch(`${BASEURL}api/patient`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
this.closeModal();
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to save");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save patient");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Confirm delete
|
||||
confirmDelete(patient) {
|
||||
this.deleteTarget = patient;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete patient
|
||||
async deletePatient() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/patient`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ InternalPID: this.deleteTarget.InternalPID }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.showDeleteModal = false;
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert("Failed to delete");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete patient");
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
this.deleteTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
130
app/Views/v2/requests/requests_index.php
Normal file
130
app/Views/v2/requests/requests_index.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<div class="w-full space-y-6">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card-glass p-6 animate-fadeIn">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
|
||||
<i class="fa-solid fa-flask text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Lab Requests</h2>
|
||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage laboratory test requests and orders</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Pending</p>
|
||||
<p class="text-3xl font-bold text-amber-500">34</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-amber-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-clock text-amber-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">In Progress</p>
|
||||
<p class="text-3xl font-bold text-blue-500">18</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-blue-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-spinner text-blue-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Completed</p>
|
||||
<p class="text-3xl font-bold text-emerald-500">156</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-emerald-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-check-circle text-emerald-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card group hover:shadow-xl transition-all duration-300">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-1" style="color: rgb(var(--color-text-muted));">Rejected</p>
|
||||
<p class="text-3xl font-bold text-red-500">3</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 rounded-2xl bg-red-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-times-circle text-red-500 text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Card -->
|
||||
<div class="card">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search requests..."
|
||||
class="input input-bordered w-64"
|
||||
/>
|
||||
<select class="select select-bordered">
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="in-progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus mr-2"></i>
|
||||
New Request
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Table Placeholder -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request ID</th>
|
||||
<th>Patient</th>
|
||||
<th>Test Type</th>
|
||||
<th>Priority</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-12" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-database text-4xl mb-3 opacity-30"></i>
|
||||
<p>No data available. Connect to API to load lab requests.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user