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 {number} [params.DepartmentID] - Filter by department 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
|
||||
*/
|
||||
export async function fetchEquipmentList(params = {}) {
|
||||
@ -29,7 +29,7 @@ export async function fetchEquipment(id) {
|
||||
* @param {Object} data - Equipment data
|
||||
* @param {string} data.IEID - Internal Equipment 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.InstrumentID] - Instrument identifier
|
||||
* @param {string} [data.InstrumentName] - Instrument display name
|
||||
@ -40,7 +40,7 @@ export async function createEquipment(data) {
|
||||
const payload = {
|
||||
IEID: data.IEID,
|
||||
DepartmentID: data.DepartmentID,
|
||||
Enable: data.Enable,
|
||||
isEnable: data.isEnable,
|
||||
EquipmentRole: data.EquipmentRole,
|
||||
InstrumentID: data.InstrumentID || null,
|
||||
InstrumentName: data.InstrumentName || null,
|
||||
@ -55,7 +55,7 @@ export async function createEquipment(data) {
|
||||
* @param {number} data.EID - Equipment ID (required)
|
||||
* @param {string} [data.IEID] - Internal Equipment 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.InstrumentID] - Instrument identifier
|
||||
* @param {string} [data.InstrumentName] - Instrument display name
|
||||
@ -66,7 +66,7 @@ export async function updateEquipment(id, data) {
|
||||
const payload = {
|
||||
IEID: data.IEID,
|
||||
DepartmentID: data.DepartmentID,
|
||||
Enable: data.Enable,
|
||||
isEnable: data.isEnable,
|
||||
EquipmentRole: data.EquipmentRole,
|
||||
InstrumentID: data.InstrumentID || null,
|
||||
InstrumentName: data.InstrumentName || null,
|
||||
|
||||
@ -246,6 +246,7 @@ export async function createWorkstation(data) {
|
||||
WorkstationName: data.WorkstationName,
|
||||
SiteID: data.SiteID,
|
||||
DepartmentID: data.DepartmentID,
|
||||
isEnable: data.isEnable,
|
||||
};
|
||||
return post('/api/organization/workstation', payload);
|
||||
}
|
||||
@ -256,6 +257,7 @@ export async function updateWorkstation(id, data) {
|
||||
WorkstationName: data.WorkstationName,
|
||||
SiteID: data.SiteID,
|
||||
DepartmentID: data.DepartmentID,
|
||||
isEnable: data.isEnable,
|
||||
};
|
||||
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.Religion] - Religion
|
||||
* @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.PatCom] - Patient comments
|
||||
* @returns {Promise<Object>} API response
|
||||
|
||||
@ -49,10 +49,10 @@ function buildPayload(formData, isUpdate = false) {
|
||||
Description: formData.Description,
|
||||
SeqScr: parseInt(formData.SeqScr) || 0,
|
||||
SeqRpt: parseInt(formData.SeqRpt) || 0,
|
||||
VisibleScr: formData.VisibleScr ? 1 : 0,
|
||||
VisibleRpt: formData.VisibleRpt ? 1 : 0,
|
||||
CountStat: formData.CountStat ? 1 : 0,
|
||||
Requestable: formData.Requestable ? 1 : 0,
|
||||
isVisibleScr: formData.isVisibleScr ? 1 : 0,
|
||||
isVisibleRpt: formData.isVisibleRpt ? 1 : 0,
|
||||
isCountStat: formData.isCountStat ? 1 : 0,
|
||||
isRequestable: formData.isRequestable ? 1 : 0,
|
||||
// StartDate is auto-set by backend (created_at)
|
||||
details: {},
|
||||
refnum: [],
|
||||
|
||||
@ -22,9 +22,9 @@ export interface TestSummary {
|
||||
TestTypeLabel: string;
|
||||
SeqScr: number;
|
||||
SeqRpt: number;
|
||||
VisibleScr: number | string;
|
||||
VisibleRpt: number | string;
|
||||
CountStat: number;
|
||||
isVisibleScr: number | string;
|
||||
isVisibleRpt: number | string;
|
||||
isCountStat: number;
|
||||
StartDate: string;
|
||||
EndDate?: string;
|
||||
DisciplineID?: number;
|
||||
@ -130,9 +130,9 @@ export interface TestDetail {
|
||||
SiteID: number;
|
||||
SeqScr: number;
|
||||
SeqRpt: number;
|
||||
VisibleScr: number | string | boolean;
|
||||
VisibleRpt: number | string | boolean;
|
||||
CountStat: number | boolean;
|
||||
isVisibleScr: number | string | boolean;
|
||||
isVisibleRpt: number | string | boolean;
|
||||
isCountStat: number | boolean;
|
||||
StartDate?: string;
|
||||
EndDate?: string;
|
||||
|
||||
@ -173,9 +173,9 @@ export interface CreateTestPayload {
|
||||
Description?: string;
|
||||
SeqScr?: number;
|
||||
SeqRpt?: number;
|
||||
VisibleScr?: number | boolean;
|
||||
VisibleRpt?: number | boolean;
|
||||
CountStat?: number | boolean;
|
||||
isVisibleScr?: number | boolean;
|
||||
isVisibleRpt?: number | boolean;
|
||||
isCountStat?: number | boolean;
|
||||
StartDate?: string;
|
||||
|
||||
// Nested details based on TestType
|
||||
@ -226,9 +226,9 @@ export interface TestFormState {
|
||||
SiteID: number;
|
||||
SeqScr: number;
|
||||
SeqRpt: number;
|
||||
VisibleScr: boolean;
|
||||
VisibleRpt: boolean;
|
||||
CountStat: boolean;
|
||||
isVisibleScr: boolean;
|
||||
isVisibleRpt: boolean;
|
||||
isCountStat: boolean;
|
||||
StartDate?: string;
|
||||
details: {
|
||||
DisciplineID?: number;
|
||||
@ -296,7 +296,7 @@ export interface DeleteTestResponse extends ApiResponse<{ TestSiteId: number; En
|
||||
// Filter Options
|
||||
export interface TestFilterOptions {
|
||||
TestType?: TestType;
|
||||
VisibleScr?: number;
|
||||
VisibleRpt?: number;
|
||||
isVisibleScr?: number;
|
||||
isVisibleRpt?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
InstrumentName: '',
|
||||
DepartmentID: null,
|
||||
WorkstationID: null,
|
||||
Enable: 1,
|
||||
isEnable: 1,
|
||||
EquipmentRole: '',
|
||||
});
|
||||
let deleteConfirmOpen = $state(false);
|
||||
@ -38,7 +38,7 @@
|
||||
{ key: 'IEID', label: 'IEID', class: 'font-medium' },
|
||||
{ key: 'InstrumentName', label: 'Name' },
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
InstrumentName: '',
|
||||
DepartmentID: departments.length > 0 ? departments[0].DepartmentID : null,
|
||||
WorkstationID: null,
|
||||
Enable: 1,
|
||||
isEnable: 1,
|
||||
EquipmentRole: '',
|
||||
};
|
||||
modalOpen = true;
|
||||
@ -105,7 +105,7 @@
|
||||
InstrumentName: row.InstrumentName || '',
|
||||
DepartmentID: row.DepartmentID,
|
||||
WorkstationID: row.WorkstationID,
|
||||
Enable: row.Enable ?? 1,
|
||||
isEnable: row.isEnable ?? 1,
|
||||
EquipmentRole: row.EquipmentRole || '',
|
||||
};
|
||||
modalOpen = true;
|
||||
@ -251,8 +251,8 @@
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if column.key === 'Enable'}
|
||||
{@html getStatusBadge(row.Enable)}
|
||||
{:else if column.key === 'isEnable'}
|
||||
{@html getStatusBadge(row.isEnable)}
|
||||
{:else}
|
||||
{row[column.key] || '-'}
|
||||
{/if}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
InstrumentName: '',
|
||||
DepartmentID: null,
|
||||
WorkstationID: null,
|
||||
Enable: 1,
|
||||
isEnable: 1,
|
||||
EquipmentRole: '',
|
||||
}),
|
||||
departments = [],
|
||||
@ -134,7 +134,7 @@
|
||||
<select
|
||||
id="enable"
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={formData.Enable}
|
||||
bind:value={formData.isEnable}
|
||||
>
|
||||
<option value={1}>Active</option>
|
||||
<option value={0}>Inactive</option>
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
WorkstationName: '',
|
||||
SiteID: null,
|
||||
DepartmentID: null,
|
||||
isEnable: 1,
|
||||
});
|
||||
let deleteConfirmOpen = $state(false);
|
||||
let deleteItem = $state(null);
|
||||
@ -37,6 +38,7 @@
|
||||
{ key: 'WorkstationName', label: 'Name' },
|
||||
{ key: 'SiteName', label: 'Site', 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' },
|
||||
];
|
||||
|
||||
@ -98,7 +100,7 @@
|
||||
|
||||
function openCreateModal() {
|
||||
modalMode = 'create';
|
||||
formData = { WorkstationID: null, WorkstationCode: '', WorkstationName: '', SiteID: null, DepartmentID: null };
|
||||
formData = { WorkstationID: null, WorkstationCode: '', WorkstationName: '', SiteID: null, DepartmentID: null, isEnable: 1 };
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
@ -110,6 +112,7 @@
|
||||
WorkstationName: row.WorkstationName,
|
||||
SiteID: row.SiteID,
|
||||
DepartmentID: row.DepartmentID,
|
||||
isEnable: row.isEnable ?? 1,
|
||||
};
|
||||
modalOpen = true;
|
||||
}
|
||||
@ -162,6 +165,12 @@
|
||||
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>
|
||||
|
||||
<div class="p-4">
|
||||
@ -238,6 +247,8 @@
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if column.key === 'isEnable'}
|
||||
{@html getStatusBadge(row.isEnable)}
|
||||
{:else}
|
||||
{row[column.key]}
|
||||
{/if}
|
||||
@ -317,6 +328,20 @@
|
||||
<span class="label-text-alt text-xs text-gray-500">The department this workstation belongs to</span>
|
||||
</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>
|
||||
{#snippet footer()}
|
||||
<button class="btn btn-ghost" onclick={() => (modalOpen = false)} type="button">Cancel</button>
|
||||
|
||||
@ -335,9 +335,11 @@
|
||||
{typeConfig.label}
|
||||
</span>
|
||||
{: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">
|
||||
<span class="badge {row.VisibleScr === '1' || row.VisibleScr === 1 ? '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 {isVisibleScrActive ? 'badge-success' : 'badge-ghost'} badge-sm">S</span>
|
||||
<span class="badge {isVisibleRptActive ? 'badge-success' : 'badge-ghost'} badge-sm">R</span>
|
||||
</div>
|
||||
{:else if column.key === 'actions'}
|
||||
<div class="flex justify-center gap-2">
|
||||
|
||||
@ -29,6 +29,10 @@ import RulesTab from './tabs/RulesTab.svelte';
|
||||
// Initialize form state with proper defaults
|
||||
let formData = $state(getDefaultFormData());
|
||||
|
||||
function parseBooleanFlag(value) {
|
||||
return value === '1' || value === 1 || value === true;
|
||||
}
|
||||
|
||||
const tabConfig = [
|
||||
{ id: 'basic', label: 'Basic Info', component: Info },
|
||||
{ id: 'tech', label: 'Tech Details', component: Settings },
|
||||
@ -115,10 +119,10 @@ import RulesTab from './tabs/RulesTab.svelte';
|
||||
SiteID: 1,
|
||||
SeqScr: 0,
|
||||
SeqRpt: 0,
|
||||
VisibleScr: true,
|
||||
VisibleRpt: true,
|
||||
CountStat: true,
|
||||
Requestable: true,
|
||||
isVisibleScr: true,
|
||||
isVisibleRpt: true,
|
||||
isCountStat: true,
|
||||
isRequestable: true,
|
||||
details: {
|
||||
DisciplineID: null,
|
||||
DepartmentID: null,
|
||||
@ -170,10 +174,10 @@ import RulesTab from './tabs/RulesTab.svelte';
|
||||
SiteID: test.SiteID || 1,
|
||||
SeqScr: test.SeqScr || 0,
|
||||
SeqRpt: test.SeqRpt || 0,
|
||||
VisibleScr: test.VisibleScr === '1' || test.VisibleScr === 1 || test.VisibleScr === true,
|
||||
VisibleRpt: test.VisibleRpt === '1' || test.VisibleRpt === 1 || test.VisibleRpt === true,
|
||||
CountStat: test.CountStat === '1' || test.CountStat === 1 || test.CountStat === true,
|
||||
Requestable: test.Requestable === '1' || test.Requestable === 1 || test.Requestable === true,
|
||||
isVisibleScr: parseBooleanFlag(test.isVisibleScr),
|
||||
isVisibleRpt: parseBooleanFlag(test.isVisibleRpt),
|
||||
isCountStat: parseBooleanFlag(test.isCountStat),
|
||||
isRequestable: parseBooleanFlag(test.isRequestable),
|
||||
details: {
|
||||
DisciplineID: test.DisciplineID || null,
|
||||
DepartmentID: test.DepartmentID || null,
|
||||
|
||||
@ -221,7 +221,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
bind:checked={formData.VisibleScr}
|
||||
bind:checked={formData.isVisibleScr}
|
||||
onchange={handleFieldChange}
|
||||
/>
|
||||
<span class="text-sm">Visible</span>
|
||||
@ -235,7 +235,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
bind:checked={formData.VisibleRpt}
|
||||
bind:checked={formData.isVisibleRpt}
|
||||
onchange={handleFieldChange}
|
||||
/>
|
||||
<span class="text-sm">Visible</span>
|
||||
@ -249,7 +249,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
bind:checked={formData.CountStat}
|
||||
bind:checked={formData.isCountStat}
|
||||
onchange={handleFieldChange}
|
||||
/>
|
||||
<span class="text-sm">Count</span>
|
||||
@ -263,7 +263,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
bind:checked={formData.Requestable}
|
||||
bind:checked={formData.isRequestable}
|
||||
onchange={handleFieldChange}
|
||||
/>
|
||||
<span class="text-sm">Requestable</span>
|
||||
|
||||
@ -189,7 +189,7 @@
|
||||
const params = {
|
||||
perPage: testsPerPage,
|
||||
page: 1,
|
||||
Requestable: 1
|
||||
isRequestable: 1
|
||||
};
|
||||
|
||||
if (selectedDiscipline) {
|
||||
@ -228,7 +228,7 @@
|
||||
const params = {
|
||||
perPage: testsPerPage,
|
||||
page: currentPage,
|
||||
Requestable: 1
|
||||
isRequestable: 1
|
||||
};
|
||||
|
||||
if (testSearchQuery.trim()) {
|
||||
|
||||
@ -45,8 +45,13 @@
|
||||
MaritalStatus: '',
|
||||
Religion: '',
|
||||
Ethnic: '',
|
||||
isDead: false,
|
||||
});
|
||||
|
||||
function parseFlag(value) {
|
||||
return value === '1' || value === 1 || value === true;
|
||||
}
|
||||
|
||||
// Track last loaded province to prevent infinite loops
|
||||
let lastLoadedProvince = $state('');
|
||||
// Track which patient we've initialized the form for
|
||||
@ -116,6 +121,7 @@
|
||||
MaritalStatus: patient.MaritalStatus || '',
|
||||
Religion: patient.Religion || '',
|
||||
Ethnic: patient.Ethnic || '',
|
||||
isDead: parseFlag(patient.isDead),
|
||||
};
|
||||
|
||||
// Load cities for this province
|
||||
@ -154,6 +160,7 @@
|
||||
MaritalStatus: '',
|
||||
Religion: '',
|
||||
Ethnic: '',
|
||||
isDead: false,
|
||||
};
|
||||
cities = [];
|
||||
lastLoadedProvince = '';
|
||||
@ -212,6 +219,7 @@
|
||||
const payload = {
|
||||
...formData,
|
||||
Birthdate: formData.Birthdate ? new Date(formData.Birthdate).toISOString() : undefined,
|
||||
isDead: formData.isDead ? 1 : 0,
|
||||
};
|
||||
|
||||
// Remove empty fields
|
||||
@ -505,6 +513,21 @@
|
||||
placeholder="Enter citizenship"
|
||||
/>
|
||||
</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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user