diff --git a/TODO.md b/TODO.md index cdf253f..224b40e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,15 +1,4 @@ -# its MVP so Keep It Simple STUPID +# TODO -## todos -- test single -- test group -- test calc -- test param -### backend -- testgroup detail is wrong - -## done -- patient page -> add edit patient button on patient list -- patient page -> add view order modal -- visit page -> on visit modal, show visitid when create -- visit page -> on visit modal, make uniform design when create and edit \ No newline at end of file +- Investigate why calculated tests in `ResultEntryModal.svelte` are failing to run: confirm formula loading (FormulaCode vs testdefcal), member detection, and local compute validation rules. +- Support `{CODE}` tokens and backend formula formats in the temporary `computeLocally` logic, then wire refresh button to compute the target calc directly. diff --git a/docs/backend-api-calculation.md b/docs/backend-api-calculation.md new file mode 100644 index 0000000..e7ecfb8 --- /dev/null +++ b/docs/backend-api-calculation.md @@ -0,0 +1,166 @@ +# Backend API Specification: Calculation Engine + +## Overview +Endpoint to evaluate calculated test formulas and return computed values with proper rounding and error handling. + +## Endpoint + +``` +POST /api/calculate/evaluate +``` + +## Request Body + +```typescript +{ + // The formula expression using test codes as variables + // Example: "CHOL - HDL - (TG/5)" + formula: string; + + // Map of test codes to their current numeric values + // Example: { "CHOL": 180, "HDL": 45, "TG": 150 } + values: Record; + + // Decimal precision for rounding (0-6) + // Default: 2 + decimal?: number; +} +``` + +## Response Body + +### Success (200) + +```typescript +{ + status: "success"; + data: { + // The computed result value + result: number; + + // The result rounded to specified decimal places + resultRounded: number; + + // Formula that was evaluated (for verification) + evaluatedFormula: string; + } +} +``` + +### Error (400/422) + +```typescript +{ + status: "error"; + message: string; + error: { + // Error type for frontend handling + type: "MISSING_VALUE" | "INVALID_EXPRESSION" | "DIVISION_BY_ZERO" | "SYNTAX_ERROR"; + + // Missing variable names if applicable + missingVars?: string[]; + + // Position of syntax error if applicable + position?: number; + } +} +``` + +## Formula Syntax + +### Supported Operators +- Arithmetic: `+`, `-`, `*`, `/`, `^` (power) +- Parentheses: `(` `)` for grouping +- Functions: `abs()`, `round()`, `floor()`, `ceil()`, `min()`, `max()`, `sqrt()` + +### Variable Names +- Test codes are used as variable names directly +- Case-sensitive (CHOL ≠ chol) +- Must match exactly (word boundaries) + +### Examples + +**Simple subtraction:** +``` +Formula: "CHOL - HDL" +Values: { "CHOL": 180, "HDL": 45 } +Result: 135 +``` + +**Complex with division:** +``` +Formula: "CHOL - HDL - (TG/5)" +Values: { "CHOL": 180, "HDL": 45, "TG": 150 } +Result: 105 +``` + +**With decimal rounding:** +``` +Formula: "(HGB * MCV) / 100" +Values: { "HGB": 14.2, "MCV": 87.5 } +Decimal: 2 +Result: 12.43 +``` + +## Validation Rules + +1. **Missing Values**: If any variable in formula is not provided in values, return MISSING_VALUE error +2. **Division by Zero**: Return DIVISION_BY_ZERO error if encountered +3. **Invalid Syntax**: Return SYNTAX_ERROR with position if formula cannot be parsed +4. **Non-numeric Values**: Return MISSING_VALUE if any value is not a valid number + +## Batch Endpoint (Optional) + +For efficiency when recalculating multiple CALC tests: + +``` +POST /api/calculate/evaluate-batch +``` + +```typescript +// Request +{ + calculations: [ + { + testSiteId: number; + formula: string; + values: Record; + decimal?: number; + } + ] +} + +// Response +{ + status: "success"; + data: { + results: [ + { + testSiteId: number; + result: number; + resultRounded: number; + error?: { + type: string; + message: string; + } + } + ] + } +} +``` + +## Frontend Integration + +The frontend will: +1. Build dependency graph from test definitions +2. Detect when member test values change +3. Call this API to compute dependent CALC tests +4. Update UI with computed values +5. Mark CALC tests as `changedByAutoCalc` for save tracking + +## Security Considerations + +1. Never use `eval()` or similar unsafe evaluation +2. Use a proper expression parser (mathjs, muparser, or custom parser) +3. Sanitize/validate formula input before parsing +4. Limit computation time to prevent DoS diff --git a/package.json b/package.json index 74470e5..e95ba2b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "vite": "^7.3.1" }, "dependencies": { - "lucide-svelte": "^0.563.0" + "lucide-svelte": "^0.563.0", + "mathjs": "^15.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a8834d..f994d71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: lucide-svelte: specifier: ^0.563.0 version: 0.563.0(svelte@5.50.0) + mathjs: + specifier: ^15.1.1 + version: 15.1.1 devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.0 @@ -48,6 +51,10 @@ importers: packages: + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -546,6 +553,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + complex.js@2.4.3: + resolution: {integrity: sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -553,6 +563,9 @@ packages: daisyui@5.5.18: resolution: {integrity: sha512-VVzjpOitMGB6DWIBeRSapbjdOevFqyzpk9u5Um6a4tyId3JFrU5pbtF0vgjXDth76mJZbueN/j9Ok03SPrh/og==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -580,6 +593,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-latex@1.2.0: + resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==} + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} @@ -609,6 +625,9 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -702,6 +721,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mathjs@15.1.1: + resolution: {integrity: sha512-rM668DTtpSzMVoh/cKAllyQVEbBApM5g//IMGD8vD7YlrIz9ITRr3SrdhjaDxcBNTdyETWwPebj2unZyHD7ZdA==} + engines: {node: '>= 18'} + hasBin: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -744,6 +768,9 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} @@ -766,6 +793,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -774,6 +804,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + typed-function@4.2.2: + resolution: {integrity: sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==} + engines: {node: '>= 18'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -833,6 +867,8 @@ packages: snapshots: + '@babel/runtime@7.28.6': {} + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -1158,10 +1194,14 @@ snapshots: clsx@2.1.1: {} + complex.js@2.4.3: {} + cookie@0.6.0: {} daisyui@5.5.18: {} + decimal.js@10.6.0: {} + deepmerge@4.3.1: {} detect-libc@2.1.2: {} @@ -1206,6 +1246,8 @@ snapshots: escalade@3.2.0: {} + escape-latex@1.2.0: {} + esm-env@1.2.2: {} esrap@2.2.3: @@ -1227,6 +1269,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + javascript-natural-sort@0.7.1: {} + jiti@2.6.1: {} kleur@4.1.5: {} @@ -1290,6 +1334,18 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mathjs@15.1.1: + dependencies: + '@babel/runtime': 7.28.6 + complex.js: 2.4.3 + decimal.js: 10.6.0 + escape-latex: 1.2.0 + fraction.js: 5.3.4 + javascript-natural-sort: 0.7.1 + seedrandom: 3.0.5 + tiny-emitter: 2.1.0 + typed-function: 4.2.2 + mri@1.2.0: {} mrmime@2.0.1: {} @@ -1347,6 +1403,8 @@ snapshots: dependencies: mri: 1.2.0 + seedrandom@3.0.5: {} + set-cookie-parser@3.0.1: {} sirv@3.0.2: @@ -1379,6 +1437,8 @@ snapshots: tapable@2.3.0: {} + tiny-emitter@2.1.0: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -1386,6 +1446,8 @@ snapshots: totalist@3.0.1: {} + typed-function@4.2.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 diff --git a/src/lib/api/rules.js b/src/lib/api/rules.js index 6577ca1..5b7672a 100644 --- a/src/lib/api/rules.js +++ b/src/lib/api/rules.js @@ -4,15 +4,19 @@ import { get, post, patch, del } from './client.js'; * @typedef {import('$lib/types/rules.types.js').RuleDef} RuleDef * @typedef {import('$lib/types/rules.types.js').RuleAction} RuleAction * @typedef {import('$lib/types/rules.types.js').RuleWithActions} RuleWithActions + * @typedef {import('$lib/types/rules.types.js').RuleWithLinks} RuleWithLinks + * @typedef {import('$lib/types/rules.types.js').LinkedTestSummary} LinkedTestSummary * @typedef {import('$lib/types/rules.types.js').RulesListResponse} RulesListResponse * @typedef {import('$lib/types/rules.types.js').RuleDetailResponse} RuleDetailResponse * @typedef {import('$lib/types/rules.types.js').RuleActionsListResponse} RuleActionsListResponse * @typedef {import('$lib/types/rules.types.js').ValidateExprResponse} ValidateExprResponse + * @typedef {import('$lib/types/rules.types.js').TestRuleMapping} TestRuleMapping */ /** * List rules - * @param {{ EventCode?: string, Active?: 0|1|number, ScopeType?: string, TestSiteID?: number, search?: string }} [params] + * Filter by TestSiteID to see rules linked to a specific test + * @param {{ EventCode?: string, Active?: 0|1|number, TestSiteID?: number, search?: string }} [params] * @returns {Promise} */ export async function fetchRules(params = {}) { @@ -21,7 +25,7 @@ export async function fetchRules(params = {}) { } /** - * Get rule (with actions) + * Get rule (with actions and linked tests) * @param {number} id * @returns {Promise} */ @@ -30,8 +34,8 @@ export async function fetchRule(id) { } /** - * Create a rule; optionally include initial actions - * @param {Partial & { actions?: Array> }} payload + * Create a rule with test mappings and optional initial actions + * @param {Partial & { TestSiteIDs: number[], actions?: Array> }} payload * @returns {Promise} */ export async function createRule(payload) { @@ -39,9 +43,9 @@ export async function createRule(payload) { } /** - * Update a rule + * Update a rule and its test mappings * @param {number} id - * @param {Partial} payload + * @param {Partial & { TestSiteIDs?: number[] }} payload * @returns {Promise} */ export async function updateRule(id, payload) { @@ -57,6 +61,26 @@ export async function deleteRule(id) { return del(`/api/rules/${id}`); } +/** + * Link a test to a rule (many-to-many mapping) + * @param {number} ruleId + * @param {number} testSiteId + * @returns {Promise} + */ +export async function linkTestToRule(ruleId, testSiteId) { + return post(`/api/rules/${ruleId}/link`, { TestSiteID: testSiteId }); +} + +/** + * Unlink a test from a rule + * @param {number} ruleId + * @param {number} testSiteId + * @returns {Promise} + */ +export async function unlinkTestFromRule(ruleId, testSiteId) { + return post(`/api/rules/${ruleId}/unlink`, { TestSiteID: testSiteId }); +} + /** * Validate/evaluate an expression * @param {string} expr @@ -67,6 +91,15 @@ export async function validateExpression(expr, context = {}) { return post('/api/rules/validate', { expr, context }); } +/** + * Compile a DSL expression into compiled payload + * @param {string} expr + * @returns {Promise<{ compiled: any; conditionExprCompiled: string }>} + */ +export async function compileRuleExpr(expr) { + return post('/api/rules/compile', { expr }); +} + /** * List actions for a rule * @param {number} id diff --git a/src/lib/components/rules/ActionsEditor.svelte b/src/lib/components/rules/ActionsEditor.svelte deleted file mode 100644 index 58617a0..0000000 --- a/src/lib/components/rules/ActionsEditor.svelte +++ /dev/null @@ -1,282 +0,0 @@ - - -
-
-
-

- - Actions -

-

SET_RESULT actions executed in sequence

-
- -
- -
- - - - - - - - - - - - {#if actions.length === 0} - - - - {:else} - {#each actions as action, index (action.RuleActionID ?? index)} - {@const p = ensureParams(action)} - {@const targetMode = getTargetMode(action)} - {@const valueMode = getValueMode(action)} - - - - - - - - {/each} - {/if} - -
SeqTypeTargetValue
No actions yet
- - - - -
- - - {#if targetMode === 'CODE'} - handleTargetSelect(action, sel)} - placeholder="Search test site..." - disabled={disabled} - /> - {:else} - - {/if} -
-
-
- - - {#if valueMode === 'EXPR'} -
-
- - - ExpressionLanguage (ternary) - - -
- -

Example: `cond ? a : (cond2 ? b : c)`

-
- {:else} - - {/if} -
-
- -
-
- - {#if errors && errors.length} -
-
Some actions have validation errors.
-
- {/if} -
- - diff --git a/src/lib/components/rules/ExprTesterModal.svelte b/src/lib/components/rules/ExprTesterModal.svelte deleted file mode 100644 index b2aa46c..0000000 --- a/src/lib/components/rules/ExprTesterModal.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - - -
-
-
- - -
- -
- -
- - -

Use a JSON object; it will be passed as context.

-
- - {#if errorMessage} -
- -
{errorMessage}
-
- {/if} - - {#if result} -
- {#if result.valid} - - {:else} - - {/if} -
- {#if result.valid} - Expression is valid. - {:else} - Expression is invalid. - {/if} -
-
-
-
Response
-
{JSON.stringify(result, null, 2)}
-
- {/if} -
-
diff --git a/src/lib/components/rules/RuleForm.svelte b/src/lib/components/rules/RuleForm.svelte deleted file mode 100644 index 90e2897..0000000 --- a/src/lib/components/rules/RuleForm.svelte +++ /dev/null @@ -1,224 +0,0 @@ - - -
-
-
-
Name *
- - {#if fieldError('Name')} -

{fieldError('Name')}

- {/if} -
- -
-
Description
- - {#if fieldError('Description')} -

{fieldError('Description')}

- {/if} -
- -
-
-
Event
- -
- -
-
Priority
- - {#if fieldError('Priority')} -

{fieldError('Priority')}

- {/if} -
-
- -
-
-
Scope
- - {#if fieldError('ScopeType')} -

{fieldError('ScopeType')}

- {/if} -
- -
-
Active
- - {#if fieldError('Active')} -

{fieldError('Active')}

- {/if} -
-
- - {#if value.ScopeType === 'TESTSITE'} -
-
Test Site
-
-
- - {#if value.TestSiteID && !selectedTestSite} -

Current TestSiteID: {value.TestSiteID}

- {/if} - {#if fieldError('TestSiteID')} -

{fieldError('TestSiteID')}

- {/if} -
-
- -
-
-
- {/if} -
- -
-
-
-
Condition Expression
- ExpressionLanguage (ternary) -
-
- -
- {#if fieldError('ConditionExpr')} -

{fieldError('ConditionExpr')}

- {/if} -
-
- - Examples -
-
order.isStat ? true : false
-order.patient.age > 65 ? (order.priority > 5 ? true : false) : false
-
-
-
-
diff --git a/src/lib/components/rules/RuleFormPage.svelte b/src/lib/components/rules/RuleFormPage.svelte new file mode 100644 index 0000000..71424f2 --- /dev/null +++ b/src/lib/components/rules/RuleFormPage.svelte @@ -0,0 +1,465 @@ + + +
+ +
+ +
+

+ {mode === 'create' ? 'New Rule' : 'Edit Rule'} +

+

Global rule with DSL compilation

+
+
+ +
+ +
+ +
+
+

Rule Information

+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+
+
+ + +
+
+
+

Condition Expression (DSL)

+
+ + {#if statusBadge.icon} + + {/if} + {statusBadge.text} + +
+
+ +
+ + + + +
+ + + {#if compileStatus === 'stale'} + Expression changed - needs recompile + {/if} +
+ + + {#if compileError} +
+ + {compileError} +
+ {/if} + + +
+
+ + DSL Examples +
+
if(sex('F') ? set_result(0.7) : set_result(1))
+order.isStat ? set_result('URGENT') : set_result('NORMAL')
+patient.age > 65 ? set_result(true) : skip()
+
+
+
+
+
+ + +
+ +
+
+

Compiled Output

+ + {#if compiledExprObject && compileStatus === 'success'} +
+
{JSON.stringify(
+                  compiledExprObject,
+                  null,
+                  2
+                )}
+
+ {:else} +
+ +

Compile expression to see output

+
+ {/if} +
+
+ + +
+
+

Actions

+ +
+ {#if saveMessage} +
+ + {saveMessage} +
+ {/if} + +
+ + + +
+
+
+
+
+
+
diff --git a/src/lib/components/rules/TestSiteSearch.svelte b/src/lib/components/rules/TestSiteSearch.svelte deleted file mode 100644 index f339912..0000000 --- a/src/lib/components/rules/TestSiteSearch.svelte +++ /dev/null @@ -1,177 +0,0 @@ - - - diff --git a/src/lib/types/rules.types.ts b/src/lib/types/rules.types.ts index 5387d99..ccc7cb5 100644 --- a/src/lib/types/rules.types.ts +++ b/src/lib/types/rules.types.ts @@ -1,22 +1,25 @@ /** * Rules Engine Type Definitions - * Based on CLQMS API Specification + * Based on CLQMS API Specification - Refactored Rule System */ export type RuleEventCode = 'ORDER_CREATED'; -export type RuleScopeType = 'GLOBAL' | 'TESTSITE'; export type RuleActionType = 'SET_RESULT'; export interface RuleDef { RuleID: number; - Name: string; + RuleCode: string; + RuleName: string; Description?: string; EventCode: RuleEventCode | string; - ScopeType: RuleScopeType; - TestSiteID?: number | null; ConditionExpr?: string | null; + ConditionExprCompiled?: string | null; Priority?: number; Active: 0 | 1 | number; + /** ISO timestamp strings */ + createdAt?: string; + updatedAt?: string; + deletedAt?: string | null; } export interface RuleAction { @@ -25,11 +28,9 @@ export interface RuleAction { Seq: number; ActionType: RuleActionType | string; ActionParams: string | Record; -} - -export interface RuleWithActions { - rule: RuleDef; - actions: RuleAction[]; + /** ISO timestamp strings */ + createdAt?: string; + updatedAt?: string; } export interface ApiResponse { @@ -39,11 +40,15 @@ export interface ApiResponse { } export interface RulesListResponse extends ApiResponse {} -export interface RuleDetailResponse extends ApiResponse {} -export interface RuleActionsListResponse extends ApiResponse {} +export interface RuleDetailResponse extends ApiResponse {} export interface ValidateExprResponse extends ApiResponse<{ valid: boolean; result?: any; error?: string }> {} +export interface CompileExprResponse { + compiled: any; + conditionExprCompiled: string; +} + export type SetResultActionParams = { testSiteID?: number; testSiteCode?: string; diff --git a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte index 13c73bf..cc30436 100644 --- a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte +++ b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte @@ -11,7 +11,7 @@ import MappingsTab from './tabs/MappingsTab.svelte'; import RefNumTab from './tabs/RefNumTab.svelte'; import RefTxtTab from './tabs/RefTxtTab.svelte'; - import ThresholdTab from './tabs/ThresholdTab.svelte'; +import ThresholdTab from './tabs/ThresholdTab.svelte'; let { open = $bindable(false), mode = 'create', testId = null, initialTestType = 'TEST', disciplines = [], departments = [], tests = [], onsave = null } = $props(); diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte index 642d1c4..a4022ad 100644 --- a/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte @@ -1,5 +1,5 @@

Calculated Test Formula

-
+
Formula Syntax: Use curly braces to reference test codes, e.g., {'{HGB}'} + {'{MCV}'}
-
+ +
+
+ +
+ {#if formulaSyntax.tone === 'success'} + {formulaSyntax.text} + {:else if formulaSyntax.tone === 'warning'} + {formulaSyntax.text} + {:else if formulaSyntax.tone === 'error'} + {formulaSyntax.text} + {:else} + {formulaSyntax.text} + {/if} +
+
+ + + {#if validationErrors.FormulaCode} +

{validationErrors.FormulaCode}

+ {/if} + + {#if members.length > 0 && missingMemberCodes.length > 0} +

+ Missing member references: {missingMemberCodes.map((code) => `{${code}}`).join(', ')} +

+ {/if} +
+ +
-
-

Available Tests

-