chore: align API boolean naming and prune stale docs
Frontend now uses is* flags for visibility/enable fields and removes stale docs plus cocoindex cache to keep the repo lean.
This commit is contained in:
parent
41ebbb7b33
commit
3e7ba218f2
Binary file not shown.
Binary file not shown.
@ -1,41 +0,0 @@
|
|||||||
exclude_patterns:
|
|
||||||
- '**/.*'
|
|
||||||
- '**/__pycache__'
|
|
||||||
- '**/node_modules'
|
|
||||||
- '**/target'
|
|
||||||
- '**/build/assets'
|
|
||||||
- '**/dist'
|
|
||||||
- '**/vendor/*.*/*'
|
|
||||||
- '**/vendor/*'
|
|
||||||
- '**/.cocoindex_code'
|
|
||||||
include_patterns:
|
|
||||||
- '**/*.py'
|
|
||||||
- '**/*.pyi'
|
|
||||||
- '**/*.js'
|
|
||||||
- '**/*.jsx'
|
|
||||||
- '**/*.ts'
|
|
||||||
- '**/*.tsx'
|
|
||||||
- '**/*.mjs'
|
|
||||||
- '**/*.cjs'
|
|
||||||
- '**/*.rs'
|
|
||||||
- '**/*.go'
|
|
||||||
- '**/*.java'
|
|
||||||
- '**/*.c'
|
|
||||||
- '**/*.h'
|
|
||||||
- '**/*.cpp'
|
|
||||||
- '**/*.hpp'
|
|
||||||
- '**/*.cc'
|
|
||||||
- '**/*.cxx'
|
|
||||||
- '**/*.hxx'
|
|
||||||
- '**/*.hh'
|
|
||||||
- '**/*.cs'
|
|
||||||
- '**/*.sql'
|
|
||||||
- '**/*.sh'
|
|
||||||
- '**/*.bash'
|
|
||||||
- '**/*.zsh'
|
|
||||||
- '**/*.md'
|
|
||||||
- '**/*.mdx'
|
|
||||||
- '**/*.txt'
|
|
||||||
- '**/*.rst'
|
|
||||||
- '**/*.php'
|
|
||||||
- '**/*.lua'
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,337 +0,0 @@
|
|||||||
# Calculator Service Operators Reference
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The `CalculatorService` (`app/Services/CalculatorService.php`) evaluates formulas with Symfony's `ExpressionLanguage`. This document lists the operators, functions, and constants that are available in the current implementation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
All endpoints live under `/api` and accept JSON. Responses use the standard `{ status, message, data }` envelope unless stated otherwise.
|
|
||||||
|
|
||||||
### Calculate By Test Site
|
|
||||||
|
|
||||||
Uses the `testdefcal` definition for a test site. The incoming body supplies the variables required by the formula.
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/calc/testsite/123
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"result": 85,
|
|
||||||
"gender": "female",
|
|
||||||
"age": 30
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"result": 92.4,
|
|
||||||
"testSiteID": 123,
|
|
||||||
"formula": "{result} * {factor} + {age}",
|
|
||||||
"variables": {
|
|
||||||
"result": 85,
|
|
||||||
"gender": "female",
|
|
||||||
"age": 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Calculate By Code Or Name
|
|
||||||
|
|
||||||
Evaluates a configured calculation by `TestSiteCode` or `TestSiteName`. Returns a compact map with a single key/value or `{}` on failure.
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/calc/testcode/GLU
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"result": 110,
|
|
||||||
"factor": 1.1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"GLU": 121
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Supported Operators
|
|
||||||
|
|
||||||
### Arithmetic Operators
|
|
||||||
|
|
||||||
| Operator | Description | Example | Result |
|
|
||||||
|----------|-------------|---------|--------|
|
|
||||||
| `+` | Addition | `5 + 3` | `8` |
|
|
||||||
| `-` | Subtraction | `10 - 4` | `6` |
|
|
||||||
| `*` | Multiplication | `6 * 7` | `42` |
|
|
||||||
| `/` | Division | `20 / 4` | `5` |
|
|
||||||
| `%` | Modulo | `20 % 6` | `2` |
|
|
||||||
| `**` | Exponentiation (power) | `2 ** 3` | `8` |
|
|
||||||
|
|
||||||
### Comparison Operators
|
|
||||||
|
|
||||||
| Operator | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `==` | Equal | `{result} == 10` |
|
|
||||||
| `!=` | Not equal | `{result} != 10` |
|
|
||||||
| `<` | Less than | `{result} < 10` |
|
|
||||||
| `<=` | Less than or equal | `{result} <= 10` |
|
|
||||||
| `>` | Greater than | `{result} > 10` |
|
|
||||||
| `>=` | Greater than or equal | `{result} >= 10` |
|
|
||||||
|
|
||||||
### Logical Operators
|
|
||||||
|
|
||||||
| Operator | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `and` / `&&` | Logical AND | `{result} > 0 and {factor} > 0` |
|
|
||||||
| `or` / `||` | Logical OR | `{gender} == 1 or {gender} == 2` |
|
|
||||||
| `!` / `not` | Logical NOT | `not ({result} > 0)` |
|
|
||||||
|
|
||||||
### Conditional Operators
|
|
||||||
|
|
||||||
| Operator | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `?:` | Ternary | `{result} > 10 ? {result} : 10` |
|
|
||||||
| `??` | Null coalescing | `{result} ?? 0` |
|
|
||||||
|
|
||||||
### Parentheses
|
|
||||||
|
|
||||||
Use parentheses to control operation precedence:
|
|
||||||
|
|
||||||
```
|
|
||||||
(2 + 3) * 4 // Result: 20
|
|
||||||
2 + 3 * 4 // Result: 14
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- `^` is bitwise XOR (not exponentiation). Use `**` for powers.
|
|
||||||
- Variables must be numeric after normalization (gender is mapped to 0/1/2).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
Only the default ExpressionLanguage functions are available:
|
|
||||||
|
|
||||||
| Function | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `min(a, b, ...)` | Minimum value | `min({result}, 10)` |
|
|
||||||
| `max(a, b, ...)` | Maximum value | `max({result}, 10)` |
|
|
||||||
| `constant(name)` | PHP constant by name | `constant("PHP_INT_MAX")` |
|
|
||||||
| `enum(name)` | PHP enum case by name | `enum("App\\Enum\\Status::Active")` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Constants
|
|
||||||
|
|
||||||
ExpressionLanguage recognizes boolean and null literals:
|
|
||||||
|
|
||||||
| Constant | Value | Description |
|
|
||||||
|----------|-------|-------------|
|
|
||||||
| `true` | `true` | Boolean true |
|
|
||||||
| `false` | `false` | Boolean false |
|
|
||||||
| `null` | `null` | Null value |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Variables in CalculatorService
|
|
||||||
|
|
||||||
When using `calculateFromDefinition()`, the following variables are automatically available:
|
|
||||||
|
|
||||||
| Variable | Description | Type |
|
|
||||||
|----------|-------------|------|
|
|
||||||
| `{result}` | The test result value | Float |
|
|
||||||
| `{factor}` | Calculation factor (default: 1) | Float |
|
|
||||||
| `{gender}` | Gender value (0=Unknown, 1=Female, 2=Male) | Integer |
|
|
||||||
| `{age}` | Patient age | Float |
|
|
||||||
| `{ref_low}` | Reference range low value | Float |
|
|
||||||
| `{ref_high}` | Reference range high value | Float |
|
|
||||||
|
|
||||||
### Gender Mapping
|
|
||||||
|
|
||||||
The `gender` variable accepts the following values:
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `0` | Unknown |
|
|
||||||
| `1` | Female |
|
|
||||||
| `2` | Male |
|
|
||||||
|
|
||||||
Or use string values: `'unknown'`, `'female'`, `'male'`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implicit Multiplication
|
|
||||||
|
|
||||||
Implicit multiplication is not supported. Always use `*` between values:
|
|
||||||
|
|
||||||
| Expression | Use Instead |
|
|
||||||
|------------|-------------|
|
|
||||||
| `2x` | `2 * x` |
|
|
||||||
| `{result}{factor}` | `{result} * {factor}` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Basic Calculation
|
|
||||||
|
|
||||||
```php
|
|
||||||
use App\Services\CalculatorService;
|
|
||||||
|
|
||||||
$calculator = new CalculatorService();
|
|
||||||
|
|
||||||
// Simple arithmetic
|
|
||||||
$result = $calculator->calculate("5 + 3 * 2");
|
|
||||||
// Result: 11
|
|
||||||
|
|
||||||
// Using min/max
|
|
||||||
$result = $calculator->calculate("max({result}, 10)", ['result' => 7]);
|
|
||||||
// Result: 10
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Variables
|
|
||||||
|
|
||||||
```php
|
|
||||||
$formula = "{result} * {factor} + 10";
|
|
||||||
$variables = [
|
|
||||||
'result' => 5.2,
|
|
||||||
'factor' => 2
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $calculator->calculate($formula, $variables);
|
|
||||||
// Result: 20.4
|
|
||||||
```
|
|
||||||
|
|
||||||
### BMI Calculation
|
|
||||||
|
|
||||||
```php
|
|
||||||
$formula = "{weight} / ({height} ** 2)";
|
|
||||||
$variables = [
|
|
||||||
'weight' => 70, // kg
|
|
||||||
'height' => 1.75 // meters
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $calculator->calculate($formula, $variables);
|
|
||||||
// Result: 22.86
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gender-Based Calculation
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Apply different multipliers based on gender
|
|
||||||
$formula = "{result} * (1 + 0.1 * {gender})";
|
|
||||||
$variables = [
|
|
||||||
'result' => 100,
|
|
||||||
'gender' => 1 // Female = 1
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $calculator->calculate($formula, $variables);
|
|
||||||
// Result: 110
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complex Formula
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Pythagorean theorem
|
|
||||||
$formula = "(({a} ** 2 + {b} ** 2) ** 0.5)";
|
|
||||||
$variables = [
|
|
||||||
'a' => 3,
|
|
||||||
'b' => 4
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $calculator->calculate($formula, $variables);
|
|
||||||
// Result: 5
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using calculateFromDefinition
|
|
||||||
|
|
||||||
```php
|
|
||||||
$calcDef = [
|
|
||||||
'FormulaCode' => '{result} * {factor} + {gender}',
|
|
||||||
'Factor' => 2
|
|
||||||
];
|
|
||||||
|
|
||||||
$testValues = [
|
|
||||||
'result' => 10,
|
|
||||||
'gender' => 1 // Female
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $calculator->calculateFromDefinition($calcDef, $testValues);
|
|
||||||
// Result: 21 (10 * 2 + 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Formula Validation
|
|
||||||
|
|
||||||
Validate formulas before storing them:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$validation = $calculator->validate("{result} / {factor}");
|
|
||||||
// Returns: ['valid' => true, 'error' => null]
|
|
||||||
|
|
||||||
$validation = $calculator->validate("{result} /");
|
|
||||||
// Returns: ['valid' => false, 'error' => 'Error message']
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extract Variables
|
|
||||||
|
|
||||||
Get a list of variables used in a formula:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$variables = $calculator->extractVariables("{result} * {factor} + {age}");
|
|
||||||
// Returns: ['result', 'factor', 'age']
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The service throws exceptions for invalid formulas or missing variables:
|
|
||||||
|
|
||||||
```php
|
|
||||||
try {
|
|
||||||
$result = $calculator->calculate("{result} / 0");
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Handle division by zero or other errors
|
|
||||||
log_message('error', $e->getMessage());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Common errors:
|
|
||||||
|
|
||||||
- **Invalid formula syntax**: Malformed expressions
|
|
||||||
- **Missing variable**: Variable placeholder not provided in data array
|
|
||||||
- **Non-numeric value**: Variables must be numeric
|
|
||||||
- **Division by zero**: Mathematical error
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Always validate formulas** before storing in database
|
|
||||||
2. **Use placeholder syntax** `{variable_name}` for clarity
|
|
||||||
3. **Handle exceptions** in production code
|
|
||||||
4. **Test edge cases** like zero values and boundary conditions
|
|
||||||
5. **Document formulas** with comments in your code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Symfony ExpressionLanguage](https://symfony.com/doc/current/components/expression_language.html)
|
|
||||||
- `app/Services/CalculatorService.php`
|
|
||||||
@ -1,421 +0,0 @@
|
|||||||
# Test Rule Engine Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The CLQMS Rule Engine evaluates business rules that inspect orders, patients, and tests, then executes actions when the compiled condition matches.
|
|
||||||
|
|
||||||
Rules are authored using a domain specific language stored in `ruledef.ConditionExpr`. Before the platform executes any rule, the DSL must be compiled into JSON and stored in `ConditionExprCompiled`, and each rule must be linked to the tests it should influence via `testrule`.
|
|
||||||
|
|
||||||
### Execution Flow
|
|
||||||
|
|
||||||
1. Write or edit the DSL in `ConditionExpr`.
|
|
||||||
2. POST the expression to `POST /api/rule/compile` to validate syntax and produce compiled JSON.
|
|
||||||
3. Save the compiled payload into `ConditionExprCompiled` and persist the rule in `ruledef`.
|
|
||||||
4. Link the rule to one or more tests through `testrule.TestSiteID` (rules only run for linked tests).
|
|
||||||
5. When the configured event fires (`test_created` or `result_updated`), the engine evaluates `ConditionExprCompiled` and runs the resulting `then` or `else` actions.
|
|
||||||
|
|
||||||
> **Note:** The rule engine currently fires only for `test_created` and `result_updated`. Other event codes can exist in the database but are not triggered by the application unless additional `RuleEngineService::run(...)` calls are added.
|
|
||||||
|
|
||||||
## Event Triggers
|
|
||||||
|
|
||||||
| Event Code | Status | Trigger Point |
|
|
||||||
|------------|--------|----------------|
|
|
||||||
| `test_created` | Active | Fired after a new test row is persisted; the handler calls `RuleEngineService::run('test_created', ...)` to evaluate test-scoped rules |
|
|
||||||
| `result_updated` | Active | Fired whenever a test result is saved or updated so result-dependent rules run immediately |
|
|
||||||
|
|
||||||
Other event codes remain in the database for future workflows, but only `test_created` and `result_updated` are executed by the current application flow.
|
|
||||||
|
|
||||||
## Rule Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
Rule
|
|
||||||
├── Event Trigger (when to run)
|
|
||||||
├── Conditions (when to match)
|
|
||||||
└── Actions (what to do)
|
|
||||||
```
|
|
||||||
|
|
||||||
The DSL expression lives in `ConditionExpr`. The compile endpoint (`/api/rule/compile`) renders the lifeblood of execution, producing `conditionExpr`, `valueExpr`, `then`, and `else` nodes that the engine consumes at runtime.
|
|
||||||
|
|
||||||
## Syntax Guide
|
|
||||||
|
|
||||||
### Basic Format
|
|
||||||
|
|
||||||
```
|
|
||||||
if(condition; then-action; else-action)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logical Operators
|
|
||||||
|
|
||||||
- Use `&&` for AND (all sub-conditions must match).
|
|
||||||
- Use `||` for OR (any matching branch satisfies the rule).
|
|
||||||
- Surround mixed logic with parentheses for clarity and precedence.
|
|
||||||
|
|
||||||
### Multi-Action Syntax
|
|
||||||
|
|
||||||
Actions within any branch are separated by `:` and evaluated in order. Every `then` and `else` branch must end with an action; use `nothing` when no further work is required.
|
|
||||||
|
|
||||||
```
|
|
||||||
if(sex('M'); result_set(0.5):test_insert('HBA1C'); nothing)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiple Rules
|
|
||||||
|
|
||||||
Create each rule as its own `ruledef` row; do not chain expressions with commas. The `testrule` table manages rule-to-test mappings, so multiple rules can attach to the same test. Example:
|
|
||||||
|
|
||||||
1. Insert `RULE_MALE_RESULT` and `RULE_SENIOR_COMMENT` in `ruledef`.
|
|
||||||
2. Add two `testrule` rows linking each rule to the appropriate `TestSiteID`.
|
|
||||||
|
|
||||||
Each rule compiles and runs independently when its trigger fires and the test is linked.
|
|
||||||
|
|
||||||
## Available Functions
|
|
||||||
|
|
||||||
### Conditions
|
|
||||||
|
|
||||||
| Function | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `sex('M'|'F')` | Match patient sex | `sex('M')` |
|
|
||||||
| `priority('R'|'S'|'U')` | Match order priority | `priority('S')` |
|
|
||||||
| `age > 18` | Numeric age comparisons (`>`, `<`, `>=`, `<=`) | `age >= 18 && age <= 65` |
|
|
||||||
| `requested('CODE')` | Check whether the order already requested a test (queries `patres`) | `requested('GLU')` |
|
|
||||||
|
|
||||||
### Logical Operators
|
|
||||||
|
|
||||||
| Operator | Meaning | Example |
|
|
||||||
|----------|---------|---------|
|
|
||||||
| `&&` | AND (all truthy) | `sex('M') && age > 40` |
|
|
||||||
| `||` | OR (any truthy) | `sex('M') || age > 65` |
|
|
||||||
| `()` | Group expressions | `(sex('M') && age > 40) || priority('S')` |
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
| Action | Description | Example |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| `result_set(value)` | (deprecated) Write to `patres.Result` for the current context test | `result_set(0.5)` |
|
|
||||||
| `result_set('CODE', value)` | Target a specific test by `TestSiteCode`, allowing multiple tests to be updated in one rule | `result_set('tesA', 0.5)` |
|
|
||||||
| `test_insert('CODE')` | Insert a test row by `TestSiteCode` if it doesn’t already exist for the order | `test_insert('HBA1C')` |
|
|
||||||
| `test_delete('CODE')` | Remove a previously requested test from the current order when the rule deems it unnecessary | `test_delete('INS')` |
|
|
||||||
| `comment_insert('text')` | Insert an order comment (`ordercom`) describing priority or clinical guidance | `comment_insert('Male patient - review')` |
|
|
||||||
| `nothing` | Explicit no-op to terminate an action chain | `nothing` |
|
|
||||||
|
|
||||||
> **Note:** `set_priority()` was removed. Use `comment_insert()` for priority notes without altering billing.
|
|
||||||
|
|
||||||
## Runtime Requirements
|
|
||||||
|
|
||||||
1. **Compiled expression required:** Rules without `ConditionExprCompiled` are ignored (see `RuleEngineService::run`).
|
|
||||||
2. **Order context:** `context.order.InternalOID` must exist for any action that writes to `patres` or `ordercom`.
|
|
||||||
3. **TestSiteID:** `result_set()` needs `testSiteID` (either provided in context or from `order.TestSiteID`). When you provide a `TestSiteCode` as the first argument (`result_set('tesA', value)`), the engine resolves that code before writing the result. `test_insert()` resolves a `TestSiteID` via the `TestSiteCode` in `TestDefSiteModel`, and `test_delete()` removes the matching `TestSiteID` rows when needed.
|
|
||||||
4. **Requested check:** `requested('CODE')` inspects `patres` rows for the same `OrderID` and `TestSiteCode`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
```
|
|
||||||
if(sex('M'); result_set('tesA', 0.5):result_set('tesB', 1.2); result_set('tesA', 0.6):result_set('tesB', 1.0))
|
|
||||||
```
|
|
||||||
Sets both `tesA`/`tesB` results together per branch.
|
|
||||||
|
|
||||||
```
|
|
||||||
if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing)
|
|
||||||
```
|
|
||||||
Adds new tests when glucose is already requested.
|
|
||||||
|
|
||||||
```
|
|
||||||
if(sex('M') && age > 40; result_set(1.2); result_set(1.0))
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0))
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL'))
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing)
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL'))
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
if(requested('GLU'); test_delete('INS'):comment_insert('Duplicate insulin request removed'); nothing)
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
All endpoints live under `/api/rule` and accept JSON. Responses use the standard `{ status, message, data }` envelope.
|
|
||||||
|
|
||||||
### List Rules
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/rule?EventCode=test_created&TestSiteID=12&search=glucose
|
|
||||||
```
|
|
||||||
|
|
||||||
Query Params:
|
|
||||||
|
|
||||||
- `EventCode` (optional) filter by event code.
|
|
||||||
- `TestSiteID` (optional) filter rules linked to a test site.
|
|
||||||
- `search` (optional) partial match against `RuleName`.
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "fetch success",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"RuleID": 1,
|
|
||||||
"RuleCode": "RULE_001",
|
|
||||||
"RuleName": "Sex-based result",
|
|
||||||
"EventCode": "test_created",
|
|
||||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
|
||||||
"ConditionExprCompiled": "{...}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Rule
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/rule/1
|
|
||||||
```
|
|
||||||
|
|
||||||
Response includes `linkedTests`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "fetch success",
|
|
||||||
"data": {
|
|
||||||
"RuleID": 1,
|
|
||||||
"RuleCode": "RULE_001",
|
|
||||||
"RuleName": "Sex-based result",
|
|
||||||
"EventCode": "test_created",
|
|
||||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
|
||||||
"ConditionExprCompiled": "{...}",
|
|
||||||
"linkedTests": [1, 2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Rule
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/rule
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"RuleCode": "RULE_001",
|
|
||||||
"RuleName": "Sex-based result",
|
|
||||||
"EventCode": "test_created",
|
|
||||||
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
|
||||||
"ConditionExprCompiled": "<compiled JSON here>",
|
|
||||||
"TestSiteIDs": [1, 2]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Rule created successfully",
|
|
||||||
"data": {
|
|
||||||
"RuleID": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Rule
|
|
||||||
|
|
||||||
```http
|
|
||||||
PATCH /api/rule/1
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"RuleName": "Sex-based result v2",
|
|
||||||
"ConditionExpr": "if(sex('M'); result_set(0.7); result_set(0.6))",
|
|
||||||
"ConditionExprCompiled": "<compiled JSON here>",
|
|
||||||
"TestSiteIDs": [1, 3]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Rule updated successfully",
|
|
||||||
"data": {
|
|
||||||
"RuleID": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Delete Rule
|
|
||||||
|
|
||||||
```http
|
|
||||||
DELETE /api/rule/1
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Rule deleted successfully",
|
|
||||||
"data": {
|
|
||||||
"RuleID": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile DSL
|
|
||||||
|
|
||||||
Validates the DSL and returns a compiled JSON structure that should be persisted in `ConditionExprCompiled`.
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/rule/compile
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"expr": "if(sex('M'); result_set(0.5); result_set(0.6))"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"raw": "if(sex('M'); result_set(0.5); result_set(0.6))",
|
|
||||||
"compiled": {
|
|
||||||
"conditionExpr": "sex('M')",
|
|
||||||
"then": ["result_set(0.5)"],
|
|
||||||
"else": ["result_set(0.6)"]
|
|
||||||
},
|
|
||||||
"conditionExprCompiled": "{...}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Evaluate Expression (Validation)
|
|
||||||
|
|
||||||
This endpoint evaluates an expression against a runtime context. It does not compile DSL or persist the result.
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/rule/validate
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"expr": "order[\"Age\"] > 18",
|
|
||||||
"context": {
|
|
||||||
"order": {
|
|
||||||
"Age": 25
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"valid": true,
|
|
||||||
"result": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Tables
|
|
||||||
|
|
||||||
- **ruledef** – stores rule metadata, raw DSL, and compiled JSON.
|
|
||||||
- **testrule** – mapping table that links rules to tests via `TestSiteID`.
|
|
||||||
- **ruleaction** – deprecated. Actions are now embedded in `ConditionExprCompiled`.
|
|
||||||
|
|
||||||
### Key Columns
|
|
||||||
|
|
||||||
| Column | Table | Description |
|
|
||||||
|--------|-------|-------------|
|
|
||||||
| `EventCode` | ruledef | The trigger event (typically `test_created` or `result_updated`). |
|
|
||||||
| `ConditionExpr` | ruledef | Raw DSL expression (semicolon syntax). |
|
|
||||||
| `ConditionExprCompiled` | ruledef | JSON payload consumed at runtime (`then`, `else`, etc.). |
|
|
||||||
| `ActionType` / `ActionParams` | ruleaction | Deprecated; actions live in compiled JSON now. |
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. Always run `POST /api/rule/compile` before persisting a rule so `ConditionExprCompiled` exists.
|
|
||||||
2. Link each rule to the relevant tests via `testrule.TestSiteID`—rules are scoped to linked tests.
|
|
||||||
3. Use multi-action (`:`) to bundle several actions in a single branch; finish the branch with `nothing` if no further work is needed.
|
|
||||||
4. Prefer `comment_insert()` over the removed `set_priority()` action when documenting priority decisions.
|
|
||||||
5. Group complex boolean logic with parentheses for clarity when mixing `&&` and `||`.
|
|
||||||
6. Use `requested('CODE')` responsibly; it performs a database lookup on `patres` so avoid invoking it in high-frequency loops without reason.
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
### Syntax Changes (v2.0)
|
|
||||||
|
|
||||||
The DSL moved from ternary (`condition ? action : action`) to semicolon syntax. Existing rules must be migrated via the provided script.
|
|
||||||
|
|
||||||
| Old Syntax | New Syntax |
|
|
||||||
|------------|------------|
|
|
||||||
| `if(condition ? action : action)` | `if(condition; action; action)` |
|
|
||||||
|
|
||||||
#### Migration Examples
|
|
||||||
|
|
||||||
```
|
|
||||||
# BEFORE
|
|
||||||
if(sex('M') ? result_set(0.5) : result_set(0.6))
|
|
||||||
|
|
||||||
# AFTER
|
|
||||||
if(sex('M'); result_set(0.5); result_set(0.6))
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
# BEFORE
|
|
||||||
if(sex('F') ? set_priority('S') : nothing)
|
|
||||||
|
|
||||||
# AFTER
|
|
||||||
if(sex('F'); comment_insert('Female patient - review priority'); nothing)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Migration Process
|
|
||||||
|
|
||||||
Run the migration which:
|
|
||||||
|
|
||||||
1. Converts ternary syntax to semicolon syntax.
|
|
||||||
2. Recompiles every expression into JSON so the engine consumes `ConditionExprCompiled` directly.
|
|
||||||
3. Eliminates reliance on the `ruleaction` table.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php spark migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Rule Not Executing
|
|
||||||
|
|
||||||
1. Ensure the rule has a compiled payload (`ConditionExprCompiled`).
|
|
||||||
2. Confirm the rule is linked to the relevant `TestSiteID` in `testrule`.
|
|
||||||
3. Verify the `EventCode` matches the currently triggered event (`test_created` or `result_updated`).
|
|
||||||
4. Check that `EndDate IS NULL` for both `ruledef` and `testrule` (soft deletes disable execution).
|
|
||||||
5. Use `/api/rule/compile` to validate the DSL and view errors.
|
|
||||||
|
|
||||||
### Invalid Expression
|
|
||||||
|
|
||||||
1. POST the expression to `/api/rule/compile` to get a detailed compilation error.
|
|
||||||
2. If using `/api/rule/validate`, supply the expected `context` payload; the endpoint simply evaluates the expression without saving it.
|
|
||||||
|
|
||||||
### Runtime Errors
|
|
||||||
|
|
||||||
- `RESULT_SET requires context.order.InternalOID` or `testSiteID`: include those fields in the context passed to `RuleEngineService::run()`.
|
|
||||||
- `TEST_INSERT` failures mean the provided `TestSiteCode` does not exist or the rule attempted to insert a duplicate test; check `testdefsite` and existing `patres` rows.
|
|
||||||
- `COMMENT_INSERT requires comment`: ensure the action provides text.
|
|
||||||
@ -7,7 +7,7 @@ import { get, post, patch, del } from './client.js';
|
|||||||
* @param {string} [params.InstrumentName] - Filter by instrument name
|
* @param {string} [params.InstrumentName] - Filter by instrument name
|
||||||
* @param {number} [params.DepartmentID] - Filter by department ID
|
* @param {number} [params.DepartmentID] - Filter by department ID
|
||||||
* @param {number} [params.WorkstationID] - Filter by workstation ID
|
* @param {number} [params.WorkstationID] - Filter by workstation ID
|
||||||
* @param {number} [params.Enable] - Filter by enable status (0 or 1)
|
* @param {number} [params.isEnable] - Filter by enable status (0 or 1)
|
||||||
* @returns {Promise<Object>} List of equipment
|
* @returns {Promise<Object>} List of equipment
|
||||||
*/
|
*/
|
||||||
export async function fetchEquipmentList(params = {}) {
|
export async function fetchEquipmentList(params = {}) {
|
||||||
@ -29,7 +29,7 @@ export async function fetchEquipment(id) {
|
|||||||
* @param {Object} data - Equipment data
|
* @param {Object} data - Equipment data
|
||||||
* @param {string} data.IEID - Internal Equipment ID (required)
|
* @param {string} data.IEID - Internal Equipment ID (required)
|
||||||
* @param {number} data.DepartmentID - Department ID (required)
|
* @param {number} data.DepartmentID - Department ID (required)
|
||||||
* @param {number} data.Enable - Enable status 0 or 1 (required)
|
* @param {number} data.isEnable - Enable status 0 or 1 (required)
|
||||||
* @param {string} data.EquipmentRole - Equipment role code (required)
|
* @param {string} data.EquipmentRole - Equipment role code (required)
|
||||||
* @param {string} [data.InstrumentID] - Instrument identifier
|
* @param {string} [data.InstrumentID] - Instrument identifier
|
||||||
* @param {string} [data.InstrumentName] - Instrument display name
|
* @param {string} [data.InstrumentName] - Instrument display name
|
||||||
@ -40,7 +40,7 @@ export async function createEquipment(data) {
|
|||||||
const payload = {
|
const payload = {
|
||||||
IEID: data.IEID,
|
IEID: data.IEID,
|
||||||
DepartmentID: data.DepartmentID,
|
DepartmentID: data.DepartmentID,
|
||||||
Enable: data.Enable,
|
isEnable: data.isEnable,
|
||||||
EquipmentRole: data.EquipmentRole,
|
EquipmentRole: data.EquipmentRole,
|
||||||
InstrumentID: data.InstrumentID || null,
|
InstrumentID: data.InstrumentID || null,
|
||||||
InstrumentName: data.InstrumentName || null,
|
InstrumentName: data.InstrumentName || null,
|
||||||
@ -55,7 +55,7 @@ export async function createEquipment(data) {
|
|||||||
* @param {number} data.EID - Equipment ID (required)
|
* @param {number} data.EID - Equipment ID (required)
|
||||||
* @param {string} [data.IEID] - Internal Equipment ID
|
* @param {string} [data.IEID] - Internal Equipment ID
|
||||||
* @param {number} [data.DepartmentID] - Department ID
|
* @param {number} [data.DepartmentID] - Department ID
|
||||||
* @param {number} [data.Enable] - Enable status 0 or 1
|
* @param {number} [data.isEnable] - Enable status 0 or 1
|
||||||
* @param {string} [data.EquipmentRole] - Equipment role code
|
* @param {string} [data.EquipmentRole] - Equipment role code
|
||||||
* @param {string} [data.InstrumentID] - Instrument identifier
|
* @param {string} [data.InstrumentID] - Instrument identifier
|
||||||
* @param {string} [data.InstrumentName] - Instrument display name
|
* @param {string} [data.InstrumentName] - Instrument display name
|
||||||
@ -66,7 +66,7 @@ export async function updateEquipment(id, data) {
|
|||||||
const payload = {
|
const payload = {
|
||||||
IEID: data.IEID,
|
IEID: data.IEID,
|
||||||
DepartmentID: data.DepartmentID,
|
DepartmentID: data.DepartmentID,
|
||||||
Enable: data.Enable,
|
isEnable: data.isEnable,
|
||||||
EquipmentRole: data.EquipmentRole,
|
EquipmentRole: data.EquipmentRole,
|
||||||
InstrumentID: data.InstrumentID || null,
|
InstrumentID: data.InstrumentID || null,
|
||||||
InstrumentName: data.InstrumentName || null,
|
InstrumentName: data.InstrumentName || null,
|
||||||
|
|||||||
@ -246,6 +246,7 @@ export async function createWorkstation(data) {
|
|||||||
WorkstationName: data.WorkstationName,
|
WorkstationName: data.WorkstationName,
|
||||||
SiteID: data.SiteID,
|
SiteID: data.SiteID,
|
||||||
DepartmentID: data.DepartmentID,
|
DepartmentID: data.DepartmentID,
|
||||||
|
isEnable: data.isEnable,
|
||||||
};
|
};
|
||||||
return post('/api/organization/workstation', payload);
|
return post('/api/organization/workstation', payload);
|
||||||
}
|
}
|
||||||
@ -256,6 +257,7 @@ export async function updateWorkstation(id, data) {
|
|||||||
WorkstationName: data.WorkstationName,
|
WorkstationName: data.WorkstationName,
|
||||||
SiteID: data.SiteID,
|
SiteID: data.SiteID,
|
||||||
DepartmentID: data.DepartmentID,
|
DepartmentID: data.DepartmentID,
|
||||||
|
isEnable: data.isEnable,
|
||||||
};
|
};
|
||||||
return patch(`/api/organization/workstation/${id}`, payload);
|
return patch(`/api/organization/workstation/${id}`, payload);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export async function checkPatientExists(params = {}) {
|
|||||||
* @param {string} [data.MaritalStatus] - Marital status (A, B, D, M, S, W)
|
* @param {string} [data.MaritalStatus] - Marital status (A, B, D, M, S, W)
|
||||||
* @param {string} [data.Religion] - Religion
|
* @param {string} [data.Religion] - Religion
|
||||||
* @param {string} [data.Ethnic] - Ethnicity
|
* @param {string} [data.Ethnic] - Ethnicity
|
||||||
* @param {string} [data.DeathIndicator] - Death indicator (Y/N)
|
* @param {number} [data.isDead] - Death indicator (0 or 1)
|
||||||
* @param {string} [data.TimeOfDeath] - Time of death
|
* @param {string} [data.TimeOfDeath] - Time of death
|
||||||
* @param {string} [data.PatCom] - Patient comments
|
* @param {string} [data.PatCom] - Patient comments
|
||||||
* @returns {Promise<Object>} API response
|
* @returns {Promise<Object>} API response
|
||||||
|
|||||||
@ -49,10 +49,10 @@ function buildPayload(formData, isUpdate = false) {
|
|||||||
Description: formData.Description,
|
Description: formData.Description,
|
||||||
SeqScr: parseInt(formData.SeqScr) || 0,
|
SeqScr: parseInt(formData.SeqScr) || 0,
|
||||||
SeqRpt: parseInt(formData.SeqRpt) || 0,
|
SeqRpt: parseInt(formData.SeqRpt) || 0,
|
||||||
VisibleScr: formData.VisibleScr ? 1 : 0,
|
isVisibleScr: formData.isVisibleScr ? 1 : 0,
|
||||||
VisibleRpt: formData.VisibleRpt ? 1 : 0,
|
isVisibleRpt: formData.isVisibleRpt ? 1 : 0,
|
||||||
CountStat: formData.CountStat ? 1 : 0,
|
isCountStat: formData.isCountStat ? 1 : 0,
|
||||||
Requestable: formData.Requestable ? 1 : 0,
|
isRequestable: formData.isRequestable ? 1 : 0,
|
||||||
// StartDate is auto-set by backend (created_at)
|
// StartDate is auto-set by backend (created_at)
|
||||||
details: {},
|
details: {},
|
||||||
refnum: [],
|
refnum: [],
|
||||||
|
|||||||
@ -22,9 +22,9 @@ export interface TestSummary {
|
|||||||
TestTypeLabel: string;
|
TestTypeLabel: string;
|
||||||
SeqScr: number;
|
SeqScr: number;
|
||||||
SeqRpt: number;
|
SeqRpt: number;
|
||||||
VisibleScr: number | string;
|
isVisibleScr: number | string;
|
||||||
VisibleRpt: number | string;
|
isVisibleRpt: number | string;
|
||||||
CountStat: number;
|
isCountStat: number;
|
||||||
StartDate: string;
|
StartDate: string;
|
||||||
EndDate?: string;
|
EndDate?: string;
|
||||||
DisciplineID?: number;
|
DisciplineID?: number;
|
||||||
@ -130,9 +130,9 @@ export interface TestDetail {
|
|||||||
SiteID: number;
|
SiteID: number;
|
||||||
SeqScr: number;
|
SeqScr: number;
|
||||||
SeqRpt: number;
|
SeqRpt: number;
|
||||||
VisibleScr: number | string | boolean;
|
isVisibleScr: number | string | boolean;
|
||||||
VisibleRpt: number | string | boolean;
|
isVisibleRpt: number | string | boolean;
|
||||||
CountStat: number | boolean;
|
isCountStat: number | boolean;
|
||||||
StartDate?: string;
|
StartDate?: string;
|
||||||
EndDate?: string;
|
EndDate?: string;
|
||||||
|
|
||||||
@ -173,9 +173,9 @@ export interface CreateTestPayload {
|
|||||||
Description?: string;
|
Description?: string;
|
||||||
SeqScr?: number;
|
SeqScr?: number;
|
||||||
SeqRpt?: number;
|
SeqRpt?: number;
|
||||||
VisibleScr?: number | boolean;
|
isVisibleScr?: number | boolean;
|
||||||
VisibleRpt?: number | boolean;
|
isVisibleRpt?: number | boolean;
|
||||||
CountStat?: number | boolean;
|
isCountStat?: number | boolean;
|
||||||
StartDate?: string;
|
StartDate?: string;
|
||||||
|
|
||||||
// Nested details based on TestType
|
// Nested details based on TestType
|
||||||
@ -226,9 +226,9 @@ export interface TestFormState {
|
|||||||
SiteID: number;
|
SiteID: number;
|
||||||
SeqScr: number;
|
SeqScr: number;
|
||||||
SeqRpt: number;
|
SeqRpt: number;
|
||||||
VisibleScr: boolean;
|
isVisibleScr: boolean;
|
||||||
VisibleRpt: boolean;
|
isVisibleRpt: boolean;
|
||||||
CountStat: boolean;
|
isCountStat: boolean;
|
||||||
StartDate?: string;
|
StartDate?: string;
|
||||||
details: {
|
details: {
|
||||||
DisciplineID?: number;
|
DisciplineID?: number;
|
||||||
@ -296,7 +296,7 @@ export interface DeleteTestResponse extends ApiResponse<{ TestSiteId: number; En
|
|||||||
// Filter Options
|
// Filter Options
|
||||||
export interface TestFilterOptions {
|
export interface TestFilterOptions {
|
||||||
TestType?: TestType;
|
TestType?: TestType;
|
||||||
VisibleScr?: number;
|
isVisibleScr?: number;
|
||||||
VisibleRpt?: number;
|
isVisibleRpt?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
InstrumentName: '',
|
InstrumentName: '',
|
||||||
DepartmentID: null,
|
DepartmentID: null,
|
||||||
WorkstationID: null,
|
WorkstationID: null,
|
||||||
Enable: 1,
|
isEnable: 1,
|
||||||
EquipmentRole: '',
|
EquipmentRole: '',
|
||||||
});
|
});
|
||||||
let deleteConfirmOpen = $state(false);
|
let deleteConfirmOpen = $state(false);
|
||||||
@ -38,7 +38,7 @@
|
|||||||
{ key: 'IEID', label: 'IEID', class: 'font-medium' },
|
{ key: 'IEID', label: 'IEID', class: 'font-medium' },
|
||||||
{ key: 'InstrumentName', label: 'Name' },
|
{ key: 'InstrumentName', label: 'Name' },
|
||||||
{ key: 'DepartmentName', label: 'Department' },
|
{ key: 'DepartmentName', label: 'Department' },
|
||||||
{ key: 'Enable', label: 'Status', class: 'w-24 text-center' },
|
{ key: 'isEnable', label: 'Status', class: 'w-24 text-center' },
|
||||||
{ key: 'actions', label: 'Actions', class: 'w-32 text-center' },
|
{ key: 'actions', label: 'Actions', class: 'w-32 text-center' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -90,7 +90,7 @@
|
|||||||
InstrumentName: '',
|
InstrumentName: '',
|
||||||
DepartmentID: departments.length > 0 ? departments[0].DepartmentID : null,
|
DepartmentID: departments.length > 0 ? departments[0].DepartmentID : null,
|
||||||
WorkstationID: null,
|
WorkstationID: null,
|
||||||
Enable: 1,
|
isEnable: 1,
|
||||||
EquipmentRole: '',
|
EquipmentRole: '',
|
||||||
};
|
};
|
||||||
modalOpen = true;
|
modalOpen = true;
|
||||||
@ -105,7 +105,7 @@
|
|||||||
InstrumentName: row.InstrumentName || '',
|
InstrumentName: row.InstrumentName || '',
|
||||||
DepartmentID: row.DepartmentID,
|
DepartmentID: row.DepartmentID,
|
||||||
WorkstationID: row.WorkstationID,
|
WorkstationID: row.WorkstationID,
|
||||||
Enable: row.Enable ?? 1,
|
isEnable: row.isEnable ?? 1,
|
||||||
EquipmentRole: row.EquipmentRole || '',
|
EquipmentRole: row.EquipmentRole || '',
|
||||||
};
|
};
|
||||||
modalOpen = true;
|
modalOpen = true;
|
||||||
@ -251,8 +251,8 @@
|
|||||||
<Trash2 class="w-4 h-4" />
|
<Trash2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if column.key === 'Enable'}
|
{:else if column.key === 'isEnable'}
|
||||||
{@html getStatusBadge(row.Enable)}
|
{@html getStatusBadge(row.isEnable)}
|
||||||
{:else}
|
{:else}
|
||||||
{row[column.key] || '-'}
|
{row[column.key] || '-'}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
InstrumentName: '',
|
InstrumentName: '',
|
||||||
DepartmentID: null,
|
DepartmentID: null,
|
||||||
WorkstationID: null,
|
WorkstationID: null,
|
||||||
Enable: 1,
|
isEnable: 1,
|
||||||
EquipmentRole: '',
|
EquipmentRole: '',
|
||||||
}),
|
}),
|
||||||
departments = [],
|
departments = [],
|
||||||
@ -134,7 +134,7 @@
|
|||||||
<select
|
<select
|
||||||
id="enable"
|
id="enable"
|
||||||
class="select select-sm select-bordered w-full"
|
class="select select-sm select-bordered w-full"
|
||||||
bind:value={formData.Enable}
|
bind:value={formData.isEnable}
|
||||||
>
|
>
|
||||||
<option value={1}>Active</option>
|
<option value={1}>Active</option>
|
||||||
<option value={0}>Inactive</option>
|
<option value={0}>Inactive</option>
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
WorkstationName: '',
|
WorkstationName: '',
|
||||||
SiteID: null,
|
SiteID: null,
|
||||||
DepartmentID: null,
|
DepartmentID: null,
|
||||||
|
isEnable: 1,
|
||||||
});
|
});
|
||||||
let deleteConfirmOpen = $state(false);
|
let deleteConfirmOpen = $state(false);
|
||||||
let deleteItem = $state(null);
|
let deleteItem = $state(null);
|
||||||
@ -37,6 +38,7 @@
|
|||||||
{ key: 'WorkstationName', label: 'Name' },
|
{ key: 'WorkstationName', label: 'Name' },
|
||||||
{ key: 'SiteName', label: 'Site', class: 'w-40' },
|
{ key: 'SiteName', label: 'Site', class: 'w-40' },
|
||||||
{ key: 'DepartmentName', label: 'Department', class: 'w-40' },
|
{ key: 'DepartmentName', label: 'Department', class: 'w-40' },
|
||||||
|
{ key: 'isEnable', label: 'Status', class: 'w-24 text-center' },
|
||||||
{ key: 'actions', label: 'Actions', class: 'w-32 text-center' },
|
{ key: 'actions', label: 'Actions', class: 'w-32 text-center' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -98,7 +100,7 @@
|
|||||||
|
|
||||||
function openCreateModal() {
|
function openCreateModal() {
|
||||||
modalMode = 'create';
|
modalMode = 'create';
|
||||||
formData = { WorkstationID: null, WorkstationCode: '', WorkstationName: '', SiteID: null, DepartmentID: null };
|
formData = { WorkstationID: null, WorkstationCode: '', WorkstationName: '', SiteID: null, DepartmentID: null, isEnable: 1 };
|
||||||
modalOpen = true;
|
modalOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +112,7 @@
|
|||||||
WorkstationName: row.WorkstationName,
|
WorkstationName: row.WorkstationName,
|
||||||
SiteID: row.SiteID,
|
SiteID: row.SiteID,
|
||||||
DepartmentID: row.DepartmentID,
|
DepartmentID: row.DepartmentID,
|
||||||
|
isEnable: row.isEnable ?? 1,
|
||||||
};
|
};
|
||||||
modalOpen = true;
|
modalOpen = true;
|
||||||
}
|
}
|
||||||
@ -162,6 +165,12 @@
|
|||||||
deleting = false;
|
deleting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStatusBadge(value) {
|
||||||
|
return value === 1
|
||||||
|
? '<span class="badge badge-success badge-sm">Active</span>'
|
||||||
|
: '<span class="badge badge-error badge-sm">Inactive</span>';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@ -238,6 +247,8 @@
|
|||||||
<Trash2 class="w-4 h-4" />
|
<Trash2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if column.key === 'isEnable'}
|
||||||
|
{@html getStatusBadge(row.isEnable)}
|
||||||
{:else}
|
{:else}
|
||||||
{row[column.key]}
|
{row[column.key]}
|
||||||
{/if}
|
{/if}
|
||||||
@ -317,6 +328,20 @@
|
|||||||
<span class="label-text-alt text-xs text-gray-500">The department this workstation belongs to</span>
|
<span class="label-text-alt text-xs text-gray-500">The department this workstation belongs to</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="workstationStatus">
|
||||||
|
<span class="label-text text-sm font-medium">Status</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="workstationStatus"
|
||||||
|
class="select select-sm select-bordered w-full"
|
||||||
|
bind:value={formData.isEnable}
|
||||||
|
>
|
||||||
|
<option value={1}>Active</option>
|
||||||
|
<option value={0}>Inactive</option>
|
||||||
|
</select>
|
||||||
|
<span class="label-text-alt text-xs text-gray-500">Whether this workstation is active</span>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
<button class="btn btn-ghost" onclick={() => (modalOpen = false)} type="button">Cancel</button>
|
<button class="btn btn-ghost" onclick={() => (modalOpen = false)} type="button">Cancel</button>
|
||||||
|
|||||||
@ -335,9 +335,11 @@
|
|||||||
{typeConfig.label}
|
{typeConfig.label}
|
||||||
</span>
|
</span>
|
||||||
{:else if column.key === 'Visible'}
|
{:else if column.key === 'Visible'}
|
||||||
|
{@const isVisibleScrActive = row.isVisibleScr === 1 || row.isVisibleScr === '1' || row.isVisibleScr === true}
|
||||||
|
{@const isVisibleRptActive = row.isVisibleRpt === 1 || row.isVisibleRpt === '1' || row.isVisibleRpt === true}
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
<span class="badge {row.VisibleScr === '1' || row.VisibleScr === 1 ? 'badge-success' : 'badge-ghost'} badge-sm">S</span>
|
<span class="badge {isVisibleScrActive ? 'badge-success' : 'badge-ghost'} badge-sm">S</span>
|
||||||
<span class="badge {row.VisibleRpt === '1' || row.VisibleRpt === 1 ? 'badge-success' : 'badge-ghost'} badge-sm">R</span>
|
<span class="badge {isVisibleRptActive ? 'badge-success' : 'badge-ghost'} badge-sm">R</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if column.key === 'actions'}
|
{:else if column.key === 'actions'}
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
|
|||||||
@ -29,6 +29,10 @@ import RulesTab from './tabs/RulesTab.svelte';
|
|||||||
// Initialize form state with proper defaults
|
// Initialize form state with proper defaults
|
||||||
let formData = $state(getDefaultFormData());
|
let formData = $state(getDefaultFormData());
|
||||||
|
|
||||||
|
function parseBooleanFlag(value) {
|
||||||
|
return value === '1' || value === 1 || value === true;
|
||||||
|
}
|
||||||
|
|
||||||
const tabConfig = [
|
const tabConfig = [
|
||||||
{ id: 'basic', label: 'Basic Info', component: Info },
|
{ id: 'basic', label: 'Basic Info', component: Info },
|
||||||
{ id: 'tech', label: 'Tech Details', component: Settings },
|
{ id: 'tech', label: 'Tech Details', component: Settings },
|
||||||
@ -115,10 +119,10 @@ import RulesTab from './tabs/RulesTab.svelte';
|
|||||||
SiteID: 1,
|
SiteID: 1,
|
||||||
SeqScr: 0,
|
SeqScr: 0,
|
||||||
SeqRpt: 0,
|
SeqRpt: 0,
|
||||||
VisibleScr: true,
|
isVisibleScr: true,
|
||||||
VisibleRpt: true,
|
isVisibleRpt: true,
|
||||||
CountStat: true,
|
isCountStat: true,
|
||||||
Requestable: true,
|
isRequestable: true,
|
||||||
details: {
|
details: {
|
||||||
DisciplineID: null,
|
DisciplineID: null,
|
||||||
DepartmentID: null,
|
DepartmentID: null,
|
||||||
@ -170,10 +174,10 @@ import RulesTab from './tabs/RulesTab.svelte';
|
|||||||
SiteID: test.SiteID || 1,
|
SiteID: test.SiteID || 1,
|
||||||
SeqScr: test.SeqScr || 0,
|
SeqScr: test.SeqScr || 0,
|
||||||
SeqRpt: test.SeqRpt || 0,
|
SeqRpt: test.SeqRpt || 0,
|
||||||
VisibleScr: test.VisibleScr === '1' || test.VisibleScr === 1 || test.VisibleScr === true,
|
isVisibleScr: parseBooleanFlag(test.isVisibleScr),
|
||||||
VisibleRpt: test.VisibleRpt === '1' || test.VisibleRpt === 1 || test.VisibleRpt === true,
|
isVisibleRpt: parseBooleanFlag(test.isVisibleRpt),
|
||||||
CountStat: test.CountStat === '1' || test.CountStat === 1 || test.CountStat === true,
|
isCountStat: parseBooleanFlag(test.isCountStat),
|
||||||
Requestable: test.Requestable === '1' || test.Requestable === 1 || test.Requestable === true,
|
isRequestable: parseBooleanFlag(test.isRequestable),
|
||||||
details: {
|
details: {
|
||||||
DisciplineID: test.DisciplineID || null,
|
DisciplineID: test.DisciplineID || null,
|
||||||
DepartmentID: test.DepartmentID || null,
|
DepartmentID: test.DepartmentID || null,
|
||||||
|
|||||||
@ -221,7 +221,7 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-sm checkbox-primary"
|
class="checkbox checkbox-sm checkbox-primary"
|
||||||
bind:checked={formData.VisibleScr}
|
bind:checked={formData.isVisibleScr}
|
||||||
onchange={handleFieldChange}
|
onchange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm">Visible</span>
|
<span class="text-sm">Visible</span>
|
||||||
@ -235,7 +235,7 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-sm checkbox-primary"
|
class="checkbox checkbox-sm checkbox-primary"
|
||||||
bind:checked={formData.VisibleRpt}
|
bind:checked={formData.isVisibleRpt}
|
||||||
onchange={handleFieldChange}
|
onchange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm">Visible</span>
|
<span class="text-sm">Visible</span>
|
||||||
@ -249,7 +249,7 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-sm checkbox-primary"
|
class="checkbox checkbox-sm checkbox-primary"
|
||||||
bind:checked={formData.CountStat}
|
bind:checked={formData.isCountStat}
|
||||||
onchange={handleFieldChange}
|
onchange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm">Count</span>
|
<span class="text-sm">Count</span>
|
||||||
@ -263,7 +263,7 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-sm checkbox-primary"
|
class="checkbox checkbox-sm checkbox-primary"
|
||||||
bind:checked={formData.Requestable}
|
bind:checked={formData.isRequestable}
|
||||||
onchange={handleFieldChange}
|
onchange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm">Requestable</span>
|
<span class="text-sm">Requestable</span>
|
||||||
|
|||||||
@ -189,7 +189,7 @@
|
|||||||
const params = {
|
const params = {
|
||||||
perPage: testsPerPage,
|
perPage: testsPerPage,
|
||||||
page: 1,
|
page: 1,
|
||||||
Requestable: 1
|
isRequestable: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedDiscipline) {
|
if (selectedDiscipline) {
|
||||||
@ -228,7 +228,7 @@
|
|||||||
const params = {
|
const params = {
|
||||||
perPage: testsPerPage,
|
perPage: testsPerPage,
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
Requestable: 1
|
isRequestable: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
if (testSearchQuery.trim()) {
|
if (testSearchQuery.trim()) {
|
||||||
|
|||||||
@ -45,8 +45,13 @@
|
|||||||
MaritalStatus: '',
|
MaritalStatus: '',
|
||||||
Religion: '',
|
Religion: '',
|
||||||
Ethnic: '',
|
Ethnic: '',
|
||||||
|
isDead: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function parseFlag(value) {
|
||||||
|
return value === '1' || value === 1 || value === true;
|
||||||
|
}
|
||||||
|
|
||||||
// Track last loaded province to prevent infinite loops
|
// Track last loaded province to prevent infinite loops
|
||||||
let lastLoadedProvince = $state('');
|
let lastLoadedProvince = $state('');
|
||||||
// Track which patient we've initialized the form for
|
// Track which patient we've initialized the form for
|
||||||
@ -116,6 +121,7 @@
|
|||||||
MaritalStatus: patient.MaritalStatus || '',
|
MaritalStatus: patient.MaritalStatus || '',
|
||||||
Religion: patient.Religion || '',
|
Religion: patient.Religion || '',
|
||||||
Ethnic: patient.Ethnic || '',
|
Ethnic: patient.Ethnic || '',
|
||||||
|
isDead: parseFlag(patient.isDead),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load cities for this province
|
// Load cities for this province
|
||||||
@ -154,6 +160,7 @@
|
|||||||
MaritalStatus: '',
|
MaritalStatus: '',
|
||||||
Religion: '',
|
Religion: '',
|
||||||
Ethnic: '',
|
Ethnic: '',
|
||||||
|
isDead: false,
|
||||||
};
|
};
|
||||||
cities = [];
|
cities = [];
|
||||||
lastLoadedProvince = '';
|
lastLoadedProvince = '';
|
||||||
@ -212,6 +219,7 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
Birthdate: formData.Birthdate ? new Date(formData.Birthdate).toISOString() : undefined,
|
Birthdate: formData.Birthdate ? new Date(formData.Birthdate).toISOString() : undefined,
|
||||||
|
isDead: formData.isDead ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove empty fields
|
// Remove empty fields
|
||||||
@ -505,6 +513,21 @@
|
|||||||
placeholder="Enter citizenship"
|
placeholder="Enter citizenship"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<label class="label" for="isDeadToggle">
|
||||||
|
<span class="label-text text-sm font-medium">Patient Deceased</span>
|
||||||
|
<span class="label-text-alt text-xs text-gray-500">Mark patient as deceased</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
id="isDeadToggle"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-sm toggle-error"
|
||||||
|
bind:checked={formData.isDead}
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-600">{formData.isDead ? 'Yes' : 'No'}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user