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:
mahdahar 2026-03-25 13:41:13 +07:00
parent 41ebbb7b33
commit 3e7ba218f2
20 changed files with 150 additions and 9030 deletions

View File

@ -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

View File

@ -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`

View File

@ -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 doesnt 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.

View File

@ -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,

View File

@ -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);
} }

View File

@ -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

View File

@ -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: [],

View File

@ -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;
} }

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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,

View File

@ -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>

View File

@ -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()) {

View File

@ -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>