diff --git a/.cocoindex_code/cocoindex.db/mdb/data.mdb b/.cocoindex_code/cocoindex.db/mdb/data.mdb new file mode 100644 index 0000000..f0a50dd Binary files /dev/null and b/.cocoindex_code/cocoindex.db/mdb/data.mdb differ diff --git a/.cocoindex_code/cocoindex.db/mdb/lock.mdb b/.cocoindex_code/cocoindex.db/mdb/lock.mdb new file mode 100644 index 0000000..904f87c Binary files /dev/null and b/.cocoindex_code/cocoindex.db/mdb/lock.mdb differ diff --git a/.cocoindex_code/settings.yml b/.cocoindex_code/settings.yml new file mode 100644 index 0000000..c910c16 --- /dev/null +++ b/.cocoindex_code/settings.yml @@ -0,0 +1,41 @@ +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' diff --git a/.cocoindex_code/target_sqlite.db b/.cocoindex_code/target_sqlite.db new file mode 100644 index 0000000..36614e4 Binary files /dev/null and b/.cocoindex_code/target_sqlite.db differ diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 2e510af..0000000 --- a/.serena/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/cache -/project.local.yml diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md deleted file mode 100644 index 82a5a9c..0000000 --- a/.serena/memories/project_overview.md +++ /dev/null @@ -1,29 +0,0 @@ -# CLQMS Frontend Project Overview - -- Purpose: Frontend for Clinical Laboratory Quality Management System (CLQMS), handling authenticated lab quality workflows. -- App type: SvelteKit SPA/static frontend that talks to backend API. -- Backend dependency: API expected at `http://localhost:8000` in development; `/api` is proxied in Vite. -- Auth model: JWT-based auth with automatic redirect to `/login` on 401. -- Main docs: `README.md`, `AGENTS.md`, `DEPLOY.md`. - -## Tech stack - -- SvelteKit `^2.50.2` -- Svelte `^5.49.2` with runes (`$props`, `$state`, `$derived`, `$effect`, `$bindable`) -- Vite `^7.3.1` -- Tailwind CSS 4 + DaisyUI 5 -- Lucide Svelte icons -- Package manager: pnpm -- Module system: ES Modules (`"type": "module"`) - -## Rough structure - -- `src/lib/api/` API client and feature endpoints -- `src/lib/stores/` shared stores (auth, config, valuesets) -- `src/lib/components/` reusable UI (Modal, DataTable, Sidebar) -- `src/lib/utils/` helpers and toast utilities -- `src/lib/types/` TS type definitions -- `src/routes/(app)/` authenticated pages (`dashboard`, `patients`, `master-data`) -- `src/routes/login/` public login route -- `static/` static assets -- `build/` production output \ No newline at end of file diff --git a/.serena/memories/style_and_conventions.md b/.serena/memories/style_and_conventions.md deleted file mode 100644 index 4aaf182..0000000 --- a/.serena/memories/style_and_conventions.md +++ /dev/null @@ -1,42 +0,0 @@ -# Style and Conventions - -Primary source: `AGENTS.md`. - -## JavaScript/TypeScript style - -- Use ES modules (`import`/`export`). -- Semicolons required. -- Single quotes for strings. -- 2-space indentation. -- Trailing commas in multi-line arrays/objects. -- Document exported functions with JSDoc including `@param` and `@returns`. - -## Import ordering - -1. Svelte / `$app/*` -2. `$lib/*` -3. External libraries (e.g., `lucide-svelte`) -4. Relative imports (minimize, prefer `$lib`) - -## Naming - -- Components: PascalCase (`LoginForm.svelte`) -- Route/files: lowercase with hyphens -- Variables/stores: camelCase -- Constants: UPPER_SNAKE_CASE -- Event handlers: `handle...` -- Form state fields: `formLoading`, `formError`, etc. - -## Svelte 5 patterns - -- Follow component script order: imports -> props -> state -> derived -> effects -> handlers. -- Prefer DaisyUI component classes (`btn`, `input`, `card`, etc.). -- For icon inputs, use DaisyUI label+input flex pattern (not absolute-positioned icons). -- Access browser-only APIs behind `$app/environment` `browser` checks. - -## API/store patterns - -- Use shared API helpers from `$lib/api/client.js` (`get/post/put/patch/del`). -- Build query strings using `URLSearchParams`. -- Use try/catch with toast error/success utilities. -- LocalStorage keys should be descriptive (e.g., `clqms_username`, `auth_token`). \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md deleted file mode 100644 index ebcd98e..0000000 --- a/.serena/memories/suggested_commands.md +++ /dev/null @@ -1,32 +0,0 @@ -# Suggested Commands (Windows project shell) - -## Core project commands (pnpm) - -- `pnpm install` - install dependencies. -- `pnpm run dev` - run local dev server. -- `pnpm run build` - create production build (`build/`). -- `pnpm run preview` - preview production build. -- `pnpm run prepare` - run SvelteKit sync. - -## Testing/linting/formatting status - -- No lint command configured yet. -- No format command configured yet. -- No test command configured yet. -- Notes in `AGENTS.md` mention future options like Vitest/Playwright, but not currently wired in scripts. - -## Useful Windows shell commands - -- `dir` - list files (cmd). -- `Get-ChildItem` or `ls` - list files (PowerShell). -- `cd ` - change directory. -- `git status` - working tree status. -- `git diff` - inspect changes. -- `git log --oneline -n 10` - recent commits. -- `findstr /S /N /I "text" *` - basic content search in cmd. -- `Select-String -Path .\* -Pattern "text" -Recurse` - content search in PowerShell. - -## Environment notes - -- Node.js 18+ required. -- Backend API should be running at `http://localhost:8000` for dev proxying. \ No newline at end of file diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md deleted file mode 100644 index 41d3acc..0000000 --- a/.serena/memories/task_completion_checklist.md +++ /dev/null @@ -1,15 +0,0 @@ -# Task Completion Checklist - -Given current project setup: - -1. Run relevant build verification: - - `pnpm run build` -2. If runtime behavior changed, also sanity check with: - - `pnpm run dev` (manual smoke test) - - optionally `pnpm run preview` for production-like validation -3. Since lint/format/test scripts are not configured, mention this explicitly in handoff. -4. Ensure code follows `AGENTS.md` conventions: - - semicolons, single quotes, import order, Svelte 5 rune patterns, naming. -5. For API/auth/localStorage changes: - - verify browser-only access guards (`browser`) and auth redirect behavior are preserved. -6. In final handoff, include changed file paths and any manual verification steps performed. \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index 49f31c8..0000000 --- a/.serena/project.yml +++ /dev/null @@ -1,135 +0,0 @@ -# the name by which the project can be referenced within Serena -project_name: "clqms01-fe" - - -# list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp -# csharp_omnisharp dart elixir elm erlang -# fortran fsharp go groovy haskell -# java julia kotlin lua markdown -# matlab nix pascal perl php -# php_phpactor powershell python python_jedi r -# rego ruby ruby_solargraph rust scala -# swift terraform toml typescript typescript_vts -# vue yaml zig -# (This list may be outdated. For the current list, see values of Language enum here: -# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py -# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) -# Note: -# - For C, use cpp -# - For JavaScript, use typescript -# - For Free Pascal/Lazarus, use pascal -# Special requirements: -# Some languages require additional setup/installations. -# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers -# When using multiple languages, the first language server that supports a given file will be used for that file. -# The first language is the default language and the respective language server will be used as a fallback. -# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. -languages: -- typescript - -# the encoding used by text files in the project -# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings -encoding: "utf-8" - -# line ending convention to use when writing source files. -# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) -# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. -line_ending: - -# The language backend to use for this project. -# If not set, the global setting from serena_config.yml is used. -# Valid values: LSP, JetBrains -# Note: the backend is fixed at startup. If a project with a different backend -# is activated post-init, an error will be returned. -language_backend: - -# whether to use project's .gitignore files to ignore files -ignore_all_files_in_gitignore: true - -# list of additional paths to ignore in this project. -# Same syntax as gitignore, so you can use * and **. -# Note: global ignored_paths from serena_config.yml are also applied additively. -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) -included_optional_tools: [] - -# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. -# This cannot be combined with non-empty excluded_tools or included_optional_tools. -fixed_tools: [] - -# list of mode names to that are always to be included in the set of active modes -# The full set of modes to be activated is base_modes + default_modes. -# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. -# Otherwise, this setting overrides the global configuration. -# Set this to [] to disable base modes for this project. -# Set this to a list of mode names to always include the respective modes for this project. -base_modes: - -# list of mode names that are to be activated by default. -# The full set of modes to be activated is base_modes + default_modes. -# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. -# Otherwise, this overrides the setting from the global configuration (serena_config.yml). -# This setting can, in turn, be overridden by CLI parameters (--mode). -default_modes: - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -# time budget (seconds) per tool call for the retrieval of additional symbol information -# such as docstrings or parameter information. -# This overrides the corresponding setting in the global configuration; see the documentation there. -# If null or missing, use the setting from the global configuration. -symbol_info_budget: - -# list of regex patterns which, when matched, mark a memory entry as read‑only. -# Extends the list from the global configuration, merging the two lists. -read_only_memory_patterns: [] diff --git a/docs/api-docs.bundled.yaml b/docs/api-docs.bundled.yaml index 314c9c2..3ce3e13 100644 --- a/docs/api-docs.bundled.yaml +++ b/docs/api-docs.bundled.yaml @@ -23,36 +23,40 @@ servers: tags: - name: Authentication description: User authentication and session management - - name: Patients + - name: Patient description: Patient registration and management - - name: Patient Visits + - name: Patient Visit description: Patient visit/encounter management - name: Organization description: Organization structure (accounts, sites, disciplines, departments, workstations) + - name: Location + description: Location management (rooms, wards, buildings) + - name: Equipment + description: Laboratory equipment and instrument management - name: Specimen description: Specimen and container management - - name: Tests + - name: Test description: Test definitions and test catalog - - name: Orders + - name: Rule + description: Rule engine - rules can be linked to multiple tests via testrule mapping table + - name: Calculation + description: Lightweight calculator endpoint for retrieving computed values by code or name + - name: Order description: Laboratory order management - - name: Results + - name: Result description: Patient results reporting with auto-validation - - name: Reports + - name: Report description: Lab report generation (HTML view) - name: Edge API description: Instrument integration endpoints - - name: Contacts + - name: Contact description: Contact management (doctors, practitioners, etc.) - - name: Locations - description: Location management (rooms, wards, buildings) - - name: ValueSets + - name: ValueSet description: Value set definitions and items + - name: User + description: User management and administration - name: Demo description: Demo/test endpoints (no authentication) - - name: EquipmentList - description: Laboratory equipment and instrument management - - name: Users - description: User management and administration paths: /api/auth/login: post: @@ -222,10 +226,123 @@ paths: responses: '201': description: User created + /api/calc/testcode/{codeOrName}: + post: + tags: + - Calculation + summary: Evaluate a configured calculation by test code or name and return the raw result map. + security: [] + parameters: + - name: codeOrName + in: path + required: true + schema: + type: string + description: TestSiteCode or TestSiteName of the calculated test (case-insensitive). + requestBody: + required: true + content: + application/json: + schema: + type: object + description: Key-value pairs where keys match member tests used in the formula. + additionalProperties: + type: number + example: + TBIL: 5 + DBIL: 3 + responses: + '200': + description: Returns a single key/value pair with the canonical TestSiteCode or an empty object when the calculation is incomplete or missing. + content: + application/json: + schema: + type: object + examples: + success: + value: + IBIL: 2 + incomplete: + value: {} + /api/calc/testsite/{testSiteID}: + post: + tags: + - Calculation + summary: Evaluate a calculation defined for a test site and return a structured result. + security: [] + parameters: + - name: testSiteID + in: path + required: true + schema: + type: integer + description: Identifier for the test site whose definition should be evaluated. + requestBody: + required: true + content: + application/json: + schema: + type: object + description: Variable assignments required by the test site formula. + additionalProperties: + type: number + example: + result: 85 + gender: female + age: 30 + responses: + '200': + description: Returns the calculated result, testSiteID, formula code, and echoed variables. + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + data: + type: object + properties: + result: + type: number + testSiteID: + type: integer + formula: + type: string + variables: + type: object + additionalProperties: + type: number + examples: + success: + value: + status: success + data: + result: 92.4 + testSiteID: 123 + formula: '{result} * {factor} + {age}' + variables: + result: 85 + gender: female + age: 30 + '404': + description: No calculation defined for the requested test site. + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: No calculation defined for this test site /api/contact: get: tags: - - Contacts + - Contact summary: List contacts security: - bearerAuth: [] @@ -258,7 +375,7 @@ paths: $ref: '#/components/schemas/Contact' post: tags: - - Contacts + - Contact summary: Create new contact security: - bearerAuth: [] @@ -323,10 +440,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - patch: + delete: tags: - - Contacts - summary: Update contact + - Contact + summary: Delete contact security: - bearerAuth: [] requestBody: @@ -337,11 +454,67 @@ paths: type: object required: - ContactID - - NameFirst properties: ContactID: type: integer - description: Contact ID to update + description: Contact ID to delete + responses: + '200': + description: Contact deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/contact/{id}: + get: + tags: + - Contact + summary: Get contact by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Contact ID + responses: + '200': + description: Contact details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/Contact' + patch: + tags: + - Contact + summary: Update contact + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Contact ID to update + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - NameFirst + properties: NameFirst: type: string description: First name @@ -394,59 +567,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Contacts - summary: Delete contact - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - ContactID - properties: - ContactID: - type: integer - description: Contact ID to delete - responses: - '200': - description: Contact deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/contact/{id}: - get: - tags: - - Contacts - summary: Get contact by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Contact ID - responses: - '200': - description: Contact details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/Contact' /api/demo/hello: get: tags: @@ -490,7 +610,7 @@ paths: timestamp: type: string format: date-time - /api/edge/results: + /api/edge/result: post: tags: - Edge API @@ -513,7 +633,7 @@ paths: $ref: '#/components/schemas/EdgeResultResponse' '400': description: Invalid JSON payload - /api/edge/orders: + /api/edge/order: get: tags: - Edge API @@ -547,7 +667,7 @@ paths: type: array items: $ref: '#/components/schemas/EdgeOrder' - /api/edge/orders/{orderId}/ack: + /api/edge/order/{orderId}/ack: post: tags: - Edge API @@ -603,7 +723,7 @@ paths: /api/equipmentlist: get: tags: - - EquipmentList + - Equipment summary: List equipment description: Get list of equipment with optional filters security: @@ -655,7 +775,7 @@ paths: $ref: '#/components/schemas/EquipmentList' post: tags: - - EquipmentList + - Equipment summary: Create equipment description: Create a new equipment entry security: @@ -707,11 +827,11 @@ paths: type: string data: type: integer - patch: + delete: tags: - - EquipmentList - summary: Update equipment - description: Update an existing equipment entry + - Equipment + summary: Delete equipment + description: Soft delete an equipment entry security: - bearerAuth: [] requestBody: @@ -725,6 +845,68 @@ paths: properties: EID: type: integer + responses: + '200': + description: Equipment deleted + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + /api/equipmentlist/{id}: + get: + tags: + - Equipment + summary: Get equipment by ID + description: Get a single equipment entry by its EID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Equipment ID + responses: + '200': + description: Equipment details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/EquipmentList' + patch: + tags: + - Equipment + summary: Update equipment + description: Update an existing equipment entry + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Equipment ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: IEID: type: string maxLength: 50 @@ -760,69 +942,10 @@ paths: type: string data: type: integer - delete: - tags: - - EquipmentList - summary: Delete equipment - description: Soft delete an equipment entry - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - EID - properties: - EID: - type: integer - responses: - '200': - description: Equipment deleted - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - /api/equipmentlist/{id}: - get: - tags: - - EquipmentList - summary: Get equipment by ID - description: Get a single equipment entry by its EID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Equipment ID - responses: - '200': - description: Equipment details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/EquipmentList' /api/location: get: tags: - - Locations + - Location summary: List locations security: - bearerAuth: [] @@ -855,7 +978,7 @@ paths: $ref: '#/components/schemas/Location' post: tags: - - Locations + - Location summary: Create location security: - bearerAuth: [] @@ -904,10 +1027,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - patch: + delete: tags: - - Locations - summary: Update location + - Location + summary: Delete location security: - bearerAuth: [] requestBody: @@ -921,7 +1044,62 @@ paths: properties: LocationID: type: integer - description: Location ID to update + description: Location ID to delete + responses: + '200': + description: Location deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + /api/location/{id}: + get: + tags: + - Location + summary: Get location by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Location ID + responses: + '200': + description: Location details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/Location' + patch: + tags: + - Location + summary: Update location + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Location ID to update + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: SiteID: type: integer description: Reference to site @@ -957,63 +1135,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - Locations - summary: Delete location - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - LocationID - properties: - LocationID: - type: integer - description: Location ID to delete - responses: - '200': - description: Location deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - /api/location/{id}: - get: - tags: - - Locations - summary: Get location by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Location ID - responses: - '200': - description: Location details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/Location' /api/ordertest: get: tags: - - Orders + - Order summary: List orders security: - bearerAuth: [] @@ -1049,13 +1174,6 @@ paths: VER: Verified REV: Reviewed REP: Reported - - name: include - in: query - schema: - type: string - enum: - - details - description: Include specimens and tests in response responses: '200': description: List of orders @@ -1071,10 +1189,10 @@ paths: data: type: array items: - $ref: '#/components/schemas/OrderTest' + $ref: '#/components/schemas/OrderTestList' post: tags: - - Orders + - Order summary: Create order with specimens and tests description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation. security: @@ -1151,61 +1269,9 @@ paths: description: Validation error '500': description: Server error - patch: - tags: - - Orders - summary: Update order - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - OrderID - properties: - OrderID: - type: string - Priority: - type: string - enum: - - R - - S - - U - OrderStatus: - type: string - enum: - - ORD - - SCH - - ANA - - VER - - REV - - REP - OrderingProvider: - type: string - DepartmentID: - type: integer - WorkstationID: - type: integer - responses: - '200': - description: Order updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/OrderTest' delete: tags: - - Orders + - Order summary: Delete order security: - bearerAuth: [] @@ -1226,7 +1292,7 @@ paths: /api/ordertest/status: post: tags: - - Orders + - Order summary: Update order status security: - bearerAuth: [] @@ -1275,7 +1341,7 @@ paths: /api/ordertest/{id}: get: tags: - - Orders + - Order summary: Get order by ID description: Returns order details with associated specimens and tests security: @@ -1301,6 +1367,61 @@ paths: type: string data: $ref: '#/components/schemas/OrderTest' + patch: + tags: + - Order + summary: Update order + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Order ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + Priority: + type: string + enum: + - R + - S + - U + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + OrderingProvider: + type: string + DepartmentID: + type: integer + WorkstationID: + type: integer + responses: + '200': + description: Order updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' /api/organization/account/{id}: get: tags: @@ -1346,32 +1467,6 @@ paths: responses: '201': description: Site created - patch: - tags: - - Organization - summary: Update site - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - SiteName: - type: string - SiteCode: - type: string - AccountID: - type: integer - responses: - '200': - description: Site updated delete: tags: - Organization @@ -1408,6 +1503,34 @@ paths: responses: '200': description: Site details + patch: + tags: + - Organization + summary: Update site + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + SiteName: + type: string + SiteCode: + type: string + AccountID: + type: integer + responses: + '200': + description: Site updated /api/organization/discipline: get: tags: @@ -1433,36 +1556,6 @@ paths: responses: '201': description: Discipline created - patch: - tags: - - Organization - summary: Update discipline - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - DisciplineName: - type: string - DisciplineCode: - type: string - SeqScr: - type: integer - description: Display order on screen - SeqRpt: - type: integer - description: Display order in reports - responses: - '200': - description: Discipline updated delete: tags: - Organization @@ -1499,6 +1592,38 @@ paths: responses: '200': description: Discipline details + patch: + tags: + - Organization + summary: Update discipline + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + DisciplineName: + type: string + DisciplineCode: + type: string + SeqScr: + type: integer + description: Display order on screen + SeqRpt: + type: integer + description: Display order in reports + responses: + '200': + description: Discipline updated /api/organization/department: get: tags: @@ -1524,32 +1649,6 @@ paths: responses: '201': description: Department created - patch: - tags: - - Organization - summary: Update department - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - DeptName: - type: string - DeptCode: - type: string - SiteID: - type: integer - responses: - '200': - description: Department updated delete: tags: - Organization @@ -1586,6 +1685,34 @@ paths: responses: '200': description: Department details + patch: + tags: + - Organization + summary: Update department + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + DeptName: + type: string + DeptCode: + type: string + SiteID: + type: integer + responses: + '200': + description: Department updated /api/organization/workstation: get: tags: @@ -1611,34 +1738,6 @@ paths: responses: '201': description: Workstation created - patch: - tags: - - Organization - summary: Update workstation - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - id - properties: - id: - type: integer - WorkstationName: - type: string - WorkstationCode: - type: string - SiteID: - type: integer - DepartmentID: - type: integer - responses: - '200': - description: Workstation updated delete: tags: - Organization @@ -1675,6 +1774,36 @@ paths: responses: '200': description: Workstation details + patch: + tags: + - Organization + summary: Update workstation + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + WorkstationName: + type: string + WorkstationCode: + type: string + SiteID: + type: integer + DepartmentID: + type: integer + responses: + '200': + description: Workstation updated /api/organization/hostapp: get: tags: @@ -1722,30 +1851,6 @@ paths: responses: '201': description: Host application created - patch: - tags: - - Organization - summary: Update host application - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - HostAppName: - type: string - SiteID: - type: integer - responses: - '200': - description: Host application updated delete: tags: - Organization @@ -1786,6 +1891,32 @@ paths: application/json: schema: $ref: '#/components/schemas/HostApp' + patch: + tags: + - Organization + summary: Update host application + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + HostAppName: + type: string + SiteID: + type: integer + responses: + '200': + description: Host application updated /api/organization/hostcompara: get: tags: @@ -1833,32 +1964,6 @@ paths: responses: '201': description: Host communication parameters created - patch: - tags: - - Organization - summary: Update host communication parameters - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - HostAppID - properties: - HostAppID: - type: string - HostIP: - type: string - HostPort: - type: string - HostPwd: - type: string - responses: - '200': - description: Host communication parameters updated delete: tags: - Organization @@ -1899,6 +2004,34 @@ paths: application/json: schema: $ref: '#/components/schemas/HostComPara' + patch: + tags: + - Organization + summary: Update host communication parameters + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + HostIP: + type: string + HostPort: + type: string + HostPwd: + type: string + responses: + '200': + description: Host communication parameters updated /api/organization/codingsys: get: tags: @@ -1946,32 +2079,6 @@ paths: responses: '201': description: Coding system created - patch: - tags: - - Organization - summary: Update coding system - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - CodingSysID - properties: - CodingSysID: - type: integer - CodingSysAbb: - type: string - FullText: - type: string - Description: - type: string - responses: - '200': - description: Coding system updated delete: tags: - Organization @@ -2012,10 +2119,38 @@ paths: application/json: schema: $ref: '#/components/schemas/CodingSys' + patch: + tags: + - Organization + summary: Update coding system + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + CodingSysAbb: + type: string + FullText: + type: string + Description: + type: string + responses: + '200': + description: Coding system updated /api/patvisit: get: tags: - - Patient Visits + - Patient Visit summary: List patient visits security: - bearerAuth: [] @@ -2087,7 +2222,7 @@ paths: description: Number of records per page post: tags: - - Patient Visits + - Patient Visit summary: Create patient visit description: | Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided. @@ -2164,27 +2299,66 @@ paths: type: string InternalPVID: type: integer + delete: + tags: + - Patient Visit + summary: Delete patient visit + security: + - bearerAuth: [] + responses: + '200': + description: Visit deleted successfully + /api/patvisit/{id}: + get: + tags: + - Patient Visit + summary: Get visit by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: PVID (visit identifier like DV00001) + responses: + '200': + description: Visit details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/PatientVisit' patch: tags: - - Patient Visits + - Patient Visit summary: Update patient visit description: | Updates an existing patient visit. InternalPVID is required. Can update main visit data, PatDiag, and add new PatVisitADT records. security: - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Internal visit ID (InternalPVID) requestBody: required: true content: application/json: schema: type: object - required: - - InternalPVID properties: - InternalPVID: - type: integer - description: Visit ID (required) PVID: type: string InternalPID: @@ -2247,47 +2421,10 @@ paths: type: string InternalPVID: type: integer - delete: - tags: - - Patient Visits - summary: Delete patient visit - security: - - bearerAuth: [] - responses: - '200': - description: Visit deleted successfully - /api/patvisit/{id}: - get: - tags: - - Patient Visits - summary: Get visit by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - description: PVID (visit identifier like DV00001) - responses: - '200': - description: Visit details - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - data: - $ref: '#/components/schemas/PatientVisit' /api/patvisit/patient/{patientId}: get: tags: - - Patient Visits + - Patient Visit summary: Get visits by patient ID security: - bearerAuth: [] @@ -2315,7 +2452,7 @@ paths: /api/patvisitadt: post: tags: - - Patient Visits + - Patient Visit summary: Create ADT record description: Create a new Admission/Discharge/Transfer record security: @@ -2333,29 +2470,9 @@ paths: application/json: schema: $ref: '#/components/schemas/SuccessResponse' - patch: - tags: - - Patient Visits - summary: Update ADT record - description: Update an existing ADT record - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PatVisitADT' - responses: - '200': - description: ADT record updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' delete: tags: - - Patient Visits + - Patient Visit summary: Delete ADT visit (soft delete) security: - bearerAuth: [] @@ -2377,7 +2494,7 @@ paths: /api/patvisitadt/visit/{visitId}: get: tags: - - Patient Visits + - Patient Visit summary: Get ADT history by visit ID description: Retrieve the complete Admission/Discharge/Transfer history for a visit, including all locations and doctors security: @@ -2454,31 +2571,10 @@ paths: EndDate: type: string format: date-time - delete: - tags: - - Patient Visits - summary: Delete ADT visit (soft delete) - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - PVADTID - properties: - PVADTID: - type: integer - description: ADT record ID to delete - responses: - '200': - description: ADT visit deleted successfully /api/patvisitadt/{id}: get: tags: - - Patient Visits + - Patient Visit summary: Get ADT record by ID description: Retrieve a single ADT record by its ID, including location and doctor details security: @@ -2553,10 +2649,37 @@ paths: EndDate: type: string format: date-time + patch: + tags: + - Patient Visit + summary: Update ADT record + description: Update an existing ADT record + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: ADT record ID (PVADTID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PatVisitADT' + responses: + '200': + description: ADT record updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' /api/patient: get: tags: - - Patients + - Patient summary: List patients security: - bearerAuth: [] @@ -2601,7 +2724,7 @@ paths: $ref: '#/components/schemas/PatientListResponse' post: tags: - - Patients + - Patient summary: Create new patient security: - bearerAuth: [] @@ -2624,24 +2747,9 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - patch: - tags: - - Patients - summary: Update patient - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Patient' - responses: - '200': - description: Patient updated successfully delete: tags: - - Patients + - Patient summary: Delete patient (soft delete) security: - bearerAuth: [] @@ -2663,7 +2771,7 @@ paths: /api/patient/check: get: tags: - - Patients + - Patient summary: Check if patient exists security: - bearerAuth: [] @@ -2694,7 +2802,7 @@ paths: /api/patient/{id}: get: tags: - - Patients + - Patient summary: Get patient by ID security: - bearerAuth: [] @@ -2717,10 +2825,32 @@ paths: type: string data: $ref: '#/components/schemas/Patient' - /api/reports/{orderID}: + patch: + tags: + - Patient + summary: Update patient + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Internal patient record ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Patient' + responses: + '200': + description: Patient updated successfully + /api/report/{orderID}: get: tags: - - Reports + - Report summary: Generate lab report description: Generate an HTML lab report for a specific order. Returns HTML content that can be viewed in browser or printed to PDF. security: @@ -2752,10 +2882,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - /api/results: + /api/result: get: tags: - - Results + - Result summary: List results description: Retrieve patient test results with optional filters by order or patient security: @@ -2848,10 +2978,10 @@ paths: RefDisplay: type: string nullable: true - /api/results/{id}: + /api/result/{id}: get: tags: - - Results + - Result summary: Get result by ID description: Retrieve a specific result entry with all related data security: @@ -2958,7 +3088,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' patch: tags: - - Results + - Result summary: Update result description: Update a result value with automatic validation against reference ranges. Returns calculated flag (L/H) in response but does not store it. security: @@ -3031,7 +3161,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: tags: - - Results + - Result summary: Delete result description: Soft delete a result entry by setting DelDate security: @@ -3056,6 +3186,264 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /api/rule: + get: + tags: + - Rule + summary: List rules + security: + - bearerAuth: [] + parameters: + - name: EventCode + in: query + schema: + type: string + description: Filter by event code + - name: TestSiteID + in: query + schema: + type: integer + description: Filter by TestSiteID (returns rules linked to this test). Rules are only returned when attached to tests. + - name: search + in: query + schema: + type: string + description: Search by rule code or name + responses: + '200': + description: List of rules + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/RuleDef' + post: + tags: + - Rule + summary: Create rule + description: | + Create a new rule. Rules must be linked to at least one test via TestSiteIDs. + A single rule can be linked to multiple tests. Rules are active only when attached to tests. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + RuleCode: + type: string + example: AUTO_SET_RESULT + RuleName: + type: string + example: Automatically Set Result + Description: + type: string + EventCode: + type: string + example: test_created + TestSiteIDs: + type: array + items: + type: integer + description: Array of TestSiteIDs to link this rule to (required) + example: + - 1 + - 2 + - 3 + ConditionExpr: + type: string + nullable: true + description: Raw DSL expression. Compile it first and persist the compiled JSON in ConditionExprCompiled. + example: if(sex('M'); result_set(0.5); result_set(0.6)) + ConditionExprCompiled: + type: string + nullable: true + description: Compiled JSON payload from POST /api/rule/compile + required: + - RuleCode + - RuleName + - EventCode + - TestSiteIDs + responses: + '201': + description: Rule created + /api/rule/{id}: + get: + tags: + - Rule + summary: Get rule with linked tests + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: RuleID + responses: + '200': + description: Rule details with linked test sites + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/RuleWithDetails' + '404': + description: Rule not found + patch: + tags: + - Rule + summary: Update rule + description: | + Update a rule. TestSiteIDs can be provided to update which tests the rule is linked to. + Tests not in the new list will be unlinked, and new tests will be linked. + Rules are active only when attached to tests. + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: RuleID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + RuleCode: + type: string + RuleName: + type: string + Description: + type: string + EventCode: + type: string + TestSiteIDs: + type: array + items: + type: integer + description: Array of TestSiteIDs to link this rule to + ConditionExpr: + type: string + nullable: true + responses: + '200': + description: Rule updated + '404': + description: Rule not found + delete: + tags: + - Rule + summary: Soft delete rule + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: RuleID + responses: + '200': + description: Rule deleted + '404': + description: Rule not found + /api/rule/validate: + post: + tags: + - Rule + summary: Validate/evaluate an expression + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + expr: + type: string + context: + type: object + additionalProperties: true + required: + - expr + responses: + '200': + description: Validation result + /api/rule/compile: + post: + tags: + - Rule + summary: Compile DSL expression to engine-compatible structure + description: | + Compile a DSL expression to the engine-compatible JSON structure. + Frontend calls this when user clicks "Compile" button. + Returns compiled structure that can be saved to ConditionExprCompiled field. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + expr: + type: string + description: Raw DSL expression + example: if(sex('M'); result_set(0.5); result_set(0.6)) + required: + - expr + responses: + '200': + description: Compilation successful + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + data: + type: object + properties: + raw: + type: string + description: Original DSL expression + compiled: + type: object + description: Parsed structure with conditionExpr, valueExpr, then, else + conditionExprCompiled: + type: string + description: JSON string to save to ConditionExprCompiled field + '400': + description: Compilation failed (invalid syntax) /api/specimen: get: tags: @@ -3081,21 +3469,6 @@ paths: responses: '201': description: Specimen created - patch: - tags: - - Specimen - summary: Update specimen - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Specimen' - responses: - '200': - description: Specimen updated /api/specimen/{id}: get: tags: @@ -3112,6 +3485,28 @@ paths: responses: '200': description: Specimen details + patch: + tags: + - Specimen + summary: Update specimen + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen ID (SID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Specimen' + responses: + '200': + description: Specimen updated delete: tags: - Specimen @@ -3199,15 +3594,6 @@ paths: responses: '201': description: Container definition created - patch: - tags: - - Specimen - summary: Update container definition - security: - - bearerAuth: [] - responses: - '200': - description: Container definition updated /api/specimen/container/{id}: get: tags: @@ -3224,6 +3610,28 @@ paths: responses: '200': description: Container definition details + patch: + tags: + - Specimen + summary: Update container definition + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Container definition ID (ConDefID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerDef' + responses: + '200': + description: Container definition updated /api/specimen/containerdef: get: tags: @@ -3249,12 +3657,26 @@ paths: responses: '201': description: Container definition created + /api/specimen/containerdef/{id}: patch: tags: - Specimen summary: Update container definition (alias) security: - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Container definition ID (ConDefID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerDef' responses: '200': description: Container definition updated @@ -3283,15 +3705,6 @@ paths: responses: '201': description: Specimen preparation created - patch: - tags: - - Specimen - summary: Update specimen preparation - security: - - bearerAuth: [] - responses: - '200': - description: Specimen preparation updated /api/specimen/prep/{id}: get: tags: @@ -3308,6 +3721,28 @@ paths: responses: '200': description: Specimen preparation details + patch: + tags: + - Specimen + summary: Update specimen preparation + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen preparation ID (SpcPrpID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenPrep' + responses: + '200': + description: Specimen preparation updated /api/specimen/status: get: tags: @@ -3333,15 +3768,6 @@ paths: responses: '201': description: Specimen status created - patch: - tags: - - Specimen - summary: Update specimen status - security: - - bearerAuth: [] - responses: - '200': - description: Specimen status updated /api/specimen/status/{id}: get: tags: @@ -3358,6 +3784,28 @@ paths: responses: '200': description: Specimen status details + patch: + tags: + - Specimen + summary: Update specimen status + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen status ID (SpcStaID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenStatus' + responses: + '200': + description: Specimen status updated /api/specimen/collection: get: tags: @@ -3383,15 +3831,6 @@ paths: responses: '201': description: Collection method created - patch: - tags: - - Specimen - summary: Update specimen collection method - security: - - bearerAuth: [] - responses: - '200': - description: Collection method updated /api/specimen/collection/{id}: get: tags: @@ -3408,10 +3847,32 @@ paths: responses: '200': description: Collection method details + patch: + tags: + - Specimen + summary: Update specimen collection method + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen collection ID (SpcColID) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SpecimenCollection' + responses: + '200': + description: Collection method updated /api/test/testmap: get: tags: - - Tests + - Test summary: List all test mappings security: - bearerAuth: [] @@ -3449,7 +3910,7 @@ paths: type: string post: tags: - - Tests + - Test summary: Create test mapping (header only) security: - bearerAuth: [] @@ -3509,54 +3970,9 @@ paths: data: type: integer description: Created TestMapID - patch: - tags: - - Tests - summary: Update test mapping - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapID: - type: integer - description: Test Map ID (required) - TestCode: - type: string - description: Test Code - maps to HostTestCode or ClientTestCode - HostType: - type: string - HostID: - type: string - ClientType: - type: string - ClientID: - type: string - required: - - TestMapID - responses: - '200': - description: Test mapping updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: integer - description: Updated TestMapID delete: tags: - - Tests + - Test summary: Soft delete test mapping (cascades to details) security: - bearerAuth: [] @@ -3593,7 +4009,7 @@ paths: /api/test/testmap/{id}: get: tags: - - Tests + - Test summary: Get test mapping by ID with details security: - bearerAuth: [] @@ -3620,10 +4036,57 @@ paths: $ref: '#/components/schemas/TestMap' '404': description: Test mapping not found + patch: + tags: + - Test + summary: Update test mapping + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestCode: + type: string + description: Test Code - maps to HostTestCode or ClientTestCode + HostType: + type: string + HostID: + type: string + ClientType: + type: string + ClientID: + type: string + responses: + '200': + description: Test mapping updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Updated TestMapID /api/test/testmap/by-testcode/{testCode}: get: tags: - - Tests + - Test summary: Get test mappings by test code with details security: - bearerAuth: [] @@ -3653,7 +4116,7 @@ paths: /api/test/testmap/detail: get: tags: - - Tests + - Test summary: List test mapping details security: - bearerAuth: [] @@ -3681,7 +4144,7 @@ paths: $ref: '#/components/schemas/TestMapDetail' post: tags: - - Tests + - Test summary: Create test mapping detail security: - bearerAuth: [] @@ -3722,42 +4185,9 @@ paths: data: type: integer description: Created TestMapDetailID - patch: - tags: - - Tests - summary: Update test mapping detail - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - TestMapDetailID: - type: integer - description: Test Map Detail ID (required) - TestMapID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - required: - - TestMapDetailID - responses: - '200': - description: Test mapping detail updated delete: tags: - - Tests + - Test summary: Soft delete test mapping detail security: - bearerAuth: [] @@ -3779,7 +4209,7 @@ paths: /api/test/testmap/detail/{id}: get: tags: - - Tests + - Test summary: Get test mapping detail by ID security: - bearerAuth: [] @@ -3804,10 +4234,45 @@ paths: type: string data: $ref: '#/components/schemas/TestMapDetail' + patch: + tags: + - Test + summary: Update test mapping detail + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map Detail ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Test mapping detail updated /api/test/testmap/detail/by-testmap/{testMapID}: get: tags: - - Tests + - Test summary: Get test mapping details by test map ID security: - bearerAuth: [] @@ -3837,7 +4302,7 @@ paths: /api/test/testmap/detail/batch: post: tags: - - Tests + - Test summary: Batch create test mapping details security: - bearerAuth: [] @@ -3867,7 +4332,7 @@ paths: description: Batch create results patch: tags: - - Tests + - Test summary: Batch update test mapping details security: - bearerAuth: [] @@ -3899,7 +4364,7 @@ paths: description: Batch update results delete: tags: - - Tests + - Test summary: Batch delete test mapping details security: - bearerAuth: [] @@ -3918,7 +4383,7 @@ paths: /api/test: get: tags: - - Tests + - Test summary: List test definitions security: - bearerAuth: [] @@ -3994,7 +4459,7 @@ paths: description: Total number of records matching the query post: tags: - - Tests + - Test summary: Create test definition security: - bearerAuth: [] @@ -4083,7 +4548,62 @@ paths: type: integer details: type: object - description: Type-specific details + description: | + Type-specific details. For CALC and GROUP types, include members array. + + **Important**: Members must use `TestSiteID` (the actual test ID), NOT `Member` or `SeqScr`. + Invalid TestSiteIDs will result in a 400 error. + properties: + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: + - NMRIC + - RANGE + - TEXT + - VSET + - NORES + RefType: + type: string + enum: + - RANGE + - THOLD + - VSET + - TEXT + - NOREF + FormulaCode: + type: string + description: Formula expression for CALC type (e.g., "{TBIL} - {DBIL}") + Unit1: + type: string + Factor: + type: number + Unit2: + type: string + Decimal: + type: integer + default: 2 + Method: + type: string + ExpectedTAT: + type: integer + members: + type: array + description: | + Array of member tests for CALC and GROUP types. + Each member object must contain `TestSiteID` (the actual test ID). + Do NOT use `Member` or `SeqScr` - these will be rejected with validation error. + items: + type: object + properties: + TestSiteID: + type: integer + description: The actual TestSiteID of the member test (required) + required: + - TestSiteID refnum: type: array items: @@ -4101,6 +4621,463 @@ paths: - TestSiteCode - TestSiteName - TestType + examples: + TEST_no_ref: + summary: Technical test without reference or map + value: + SiteID: 1 + TestSiteCode: TEST_NREF + TestSiteName: Numeric Test + TestType: TEST + SeqScr: 500 + SeqRpt: 500 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + Unit1: mg/dL + Method: CBC Analyzer + PARAM_no_ref: + summary: Parameter without reference or map + value: + SiteID: 1 + TestSiteCode: PARAM_NRF + TestSiteName: Clinical Parameter + TestType: PARAM + SeqScr: 10 + SeqRpt: 10 + VisibleScr: 1 + VisibleRpt: 0 + CountStat: 0 + details: + DisciplineID: 10 + DepartmentID: 0 + Unit1: cm + Method: Manual entry + TEST_range_single: + summary: Technical test with numeric range reference (single) + value: + SiteID: 1 + TestSiteCode: TEST_RANGE + TestSiteName: Glucose Range + TestType: TEST + SeqScr: 105 + SeqRpt: 105 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + TEST_range_multiple_map: + summary: Numeric reference with multiple ranges and test map + value: + SiteID: 1 + TestSiteCode: TEST_RMAP + TestSiteName: Glucose Panic Range + TestType: TEST + SeqScr: 110 + SeqRpt: 110 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: RANGE + Unit1: mg/dL + Method: Hexokinase + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 70 + HighSign: LE + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + - NumRefType: NMRC + RangeType: REF + Sex: '1' + LowSign: '>' + Low: 75 + HighSign: < + High: 105 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: GLU + HostTestName: Glucose + ConDefID: 1 + ClientTestCode: GLU_C + ClientTestName: Glucose Client + - HostTestCode: CREA + HostTestName: Creatinine + ConDefID: 2 + ClientTestCode: CREA_C + ClientTestName: Creatinine Client + - HostType: WST + HostID: '3' + ClientType: INST + ClientID: '2' + details: + - HostTestCode: HB + HostTestName: Hemoglobin + ConDefID: 3 + ClientTestCode: HB_C + ClientTestName: Hemoglobin Client + TEST_threshold: + summary: Technical test with threshold reference + value: + SiteID: 1 + TestSiteCode: TEST_THLD + TestSiteName: Sodium Threshold + TestType: TEST + SeqScr: 115 + SeqRpt: 115 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 125 + Flag: H + TEST_threshold_map: + summary: Threshold reference plus test map + value: + SiteID: 1 + TestSiteCode: TEST_TMAP + TestSiteName: Potassium Panic + TestType: TEST + SeqScr: 120 + SeqRpt: 120 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mmol/L + Method: Auto Analyzer + refnum: + - NumRefType: THOLD + RangeType: PANIC + Sex: '2' + LowSign: LT + Low: 120 + AgeStart: 0 + AgeEnd: 125 + Flag: H + - NumRefType: THOLD + RangeType: PANIC + Sex: '1' + LowSign: < + Low: 121 + AgeStart: 0 + AgeEnd: 125 + Flag: H + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: HB + HostTestName: Hemoglobin + ConDefID: 3 + ClientTestCode: HB_C + ClientTestName: Hemoglobin Client + - HostTestCode: GLU + HostTestName: Glucose + ConDefID: 1 + ClientTestCode: GLU_C + ClientTestName: Glucose Client + TEST_text: + summary: Technical test with text reference + value: + SiteID: 1 + TestSiteCode: TEST_TEXT + TestSiteName: Disease Stage + TestType: TEST + SeqScr: 130 + SeqRpt: 130 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + Method: Morphology + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 18 + AgeEnd: 99 + RefTxt: NORM=Normal;HIGH=High + Flag: 'N' + TEST_text_map: + summary: Text reference plus test map + value: + SiteID: 1 + TestSiteCode: TEST_TXM + TestSiteName: Disease Stage (Map) + TestType: TEST + SeqScr: 135 + SeqRpt: 135 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 1 + DepartmentID: 1 + ResultType: TEXT + RefType: TEXT + reftxt: + - SpcType: GEN + TxtRefType: TEXT + Sex: '2' + AgeStart: 18 + AgeEnd: 99 + RefTxt: NORM=Normal + Flag: 'N' + - SpcType: GEN + TxtRefType: TEXT + Sex: '1' + AgeStart: 18 + AgeEnd: 99 + RefTxt: ABN=Abnormal + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '1' + details: + - HostTestCode: STAGE + HostTestName: Disease Stage + ConDefID: 4 + ClientTestCode: STAGE_C + ClientTestName: Disease Stage Client + TEST_valueset: + summary: Technical test using a value set result + value: + SiteID: 1 + TestSiteCode: TEST_VSET + TestSiteName: Urine Color + TestType: TEST + SeqScr: 140 + SeqRpt: 140 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + Method: Visual + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 120 + RefTxt: NORM=Normal;MACRO=Macro + Flag: 'N' + TEST_valueset_map: + summary: Value set reference with test map + value: + SiteID: 1 + TestSiteCode: TEST_VMAP + TestSiteName: Urine Color (Map) + TestType: TEST + SeqScr: 145 + SeqRpt: 145 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + reftxt: + - SpcType: GEN + TxtRefType: VSET + Sex: '2' + AgeStart: 0 + AgeEnd: 120 + RefTxt: NORM=Normal;ABN=Abnormal + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '8' + details: + - HostTestCode: UCOLOR + HostTestName: Urine Color + ConDefID: 12 + ClientTestCode: UCOLOR_C + ClientTestName: Urine Color Client + TEST_valueset_map_no_reftxt: + summary: Value set result with mapping but without explicit text reference entries + value: + SiteID: 1 + TestSiteCode: TEST_VSETM + TestSiteName: Urine Result Map + TestType: TEST + SeqScr: 150 + SeqRpt: 150 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + DisciplineID: 4 + DepartmentID: 4 + ResultType: VSET + RefType: VSET + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '8' + details: + - HostTestCode: UGLUC + HostTestName: Urine Glucose + ConDefID: 12 + ClientTestCode: UGLUC_C + ClientTestName: Urine Glucose Client + CALC_basic: + summary: Calculated test with members (no references) + value: + SiteID: 1 + TestSiteCode: CALC_BASE + TestSiteName: Estimated GFR + TestType: CALC + SeqScr: 190 + SeqRpt: 190 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 + details: + DisciplineID: 2 + DepartmentID: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + members: + - TestSiteID: 21 + - TestSiteID: 22 + CALC_full: + summary: Calculated test with numeric reference ranges and map + value: + SiteID: 1 + TestSiteCode: CALC_FULL + TestSiteName: Estimated GFR (Map) + TestType: CALC + SeqScr: 195 + SeqRpt: 195 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 + details: + DisciplineID: 2 + DepartmentID: 2 + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + members: + - TestSiteID: 21 + - TestSiteID: 22 + refnum: + - NumRefType: NMRC + RangeType: REF + Sex: '2' + LowSign: GE + Low: 10 + HighSign: LE + High: 20 + AgeStart: 18 + AgeEnd: 120 + Flag: 'N' + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: EGFR + HostTestName: eGFR + ConDefID: 1 + ClientTestCode: EGFR_C + ClientTestName: eGFR Client + GROUP_with_members: + summary: Group/profile test with members and mapping + value: + SiteID: 1 + TestSiteCode: GROUP_PNL + TestSiteName: Lipid Profile + TestType: GROUP + SeqScr: 10 + SeqRpt: 10 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + details: + members: + - TestSiteID: 169 + - TestSiteID: 170 + testmap: + - HostType: SITE + HostID: '1' + ClientType: WST + ClientID: '3' + details: + - HostTestCode: LIPID + HostTestName: Lipid Profile + ConDefID: 1 + ClientTestCode: LIPID_C + ClientTestName: Lipid Client responses: '201': description: Test definition created @@ -4119,9 +5096,22 @@ paths: properties: TestSiteId: type: integer + '400': + description: Validation error (e.g., invalid member TestSiteID) + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' patch: tags: - - Tests + - Test summary: Update test definition security: - bearerAuth: [] @@ -4207,7 +5197,62 @@ paths: type: integer details: type: object - description: Type-specific details + description: | + Type-specific details. For CALC and GROUP types, include members array. + + **Important**: Members must use `TestSiteID` (the actual test ID), NOT `Member` or `SeqScr`. + Invalid TestSiteIDs will result in a 400 error. + properties: + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: + - NMRIC + - RANGE + - TEXT + - VSET + - NORES + RefType: + type: string + enum: + - RANGE + - THOLD + - VSET + - TEXT + - NOREF + FormulaCode: + type: string + description: Formula expression for CALC type (e.g., "{TBIL} - {DBIL}") + Unit1: + type: string + Factor: + type: number + Unit2: + type: string + Decimal: + type: integer + default: 2 + Method: + type: string + ExpectedTAT: + type: integer + members: + type: array + description: | + Array of member tests for CALC and GROUP types. + Each member object must contain `TestSiteID` (the actual test ID). + Do NOT use `Member` or `SeqScr` - these will be rejected with validation error. + items: + type: object + properties: + TestSiteID: + type: integer + description: The actual TestSiteID of the member test (required) + required: + - TestSiteID refnum: type: array items: @@ -4240,10 +5285,23 @@ paths: properties: TestSiteId: type: integer + '400': + description: Validation error (e.g., invalid member TestSiteID) + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' /api/test/{id}: get: tags: - - Tests + - Test summary: Get test definition by ID security: - bearerAuth: [] @@ -4272,7 +5330,7 @@ paths: description: Test not found delete: tags: - - Tests + - Test summary: Soft delete test definition security: - bearerAuth: [] @@ -4317,10 +5375,10 @@ paths: description: Test not found '422': description: Test already disabled - /api/users: + /api/user: get: tags: - - Users + - User summary: List users with pagination and search security: - bearerAuth: [] @@ -4378,7 +5436,7 @@ paths: description: Server error post: tags: - - Users + - User summary: Create new user security: - bearerAuth: [] @@ -4428,12 +5486,44 @@ paths: type: object '500': description: Server error + /api/user/{id}: + get: + tags: + - User + summary: Get user by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User details + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found + '500': + description: Server error patch: tags: - - Users + - User summary: Update existing user security: - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID requestBody: required: true content: @@ -4469,34 +5559,9 @@ paths: description: User not found '500': description: Server error - /api/users/{id}: - get: - tags: - - Users - summary: Get user by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: User ID - responses: - '200': - description: User details - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '404': - description: User not found - '500': - description: Server error delete: tags: - - Users + - User summary: Delete user (soft delete) security: - bearerAuth: [] @@ -4533,7 +5598,7 @@ paths: /api/valueset: get: tags: - - ValueSets + - ValueSet summary: List lib value sets description: List all library/system value sets from JSON files with item counts. Returns an array of objects with value, label, and count properties. security: @@ -4572,7 +5637,7 @@ paths: /api/valueset/{key}: get: tags: - - ValueSets + - ValueSet summary: Get lib value set by key description: | Get a specific library/system value set from JSON files. @@ -4699,7 +5764,7 @@ paths: /api/valueset/refresh: post: tags: - - ValueSets + - ValueSet summary: Refresh lib ValueSet cache description: Clear and reload the library/system ValueSet cache from JSON files. Call this after modifying JSON files in app/Libraries/Data/. security: @@ -4721,7 +5786,7 @@ paths: /api/valueset/user/items: get: tags: - - ValueSets + - ValueSet summary: List user value set items description: List value set items from database (user-defined) security: @@ -4758,7 +5823,7 @@ paths: $ref: '#/components/schemas/ValueSetItem' post: tags: - - ValueSets + - ValueSet summary: Create user value set item description: Create value set item in database (user-defined) security: @@ -4804,7 +5869,7 @@ paths: /api/valueset/user/items/{id}: get: tags: - - ValueSets + - ValueSet summary: Get user value set item by ID description: Get value set item from database (user-defined) security: @@ -4829,7 +5894,7 @@ paths: $ref: '#/components/schemas/ValueSetItem' put: tags: - - ValueSets + - ValueSet summary: Update user value set item description: Update value set item in database (user-defined) security: @@ -4878,7 +5943,7 @@ paths: $ref: '#/components/schemas/ValueSetItem' delete: tags: - - ValueSets + - ValueSet summary: Delete user value set item description: Delete value set item from database (user-defined) security: @@ -4904,7 +5969,7 @@ paths: /api/valueset/user/def: get: tags: - - ValueSets + - ValueSet summary: List user value set definitions description: List value set definitions from database (user-defined) security: @@ -4952,7 +6017,7 @@ paths: type: integer post: tags: - - ValueSets + - ValueSet summary: Create user value set definition description: Create value set definition in database (user-defined) security: @@ -4990,7 +6055,7 @@ paths: /api/valueset/user/def/{id}: get: tags: - - ValueSets + - ValueSet summary: Get user value set definition by ID description: Get value set definition from database (user-defined) security: @@ -5015,7 +6080,7 @@ paths: $ref: '#/components/schemas/ValueSetDef' put: tags: - - ValueSets + - ValueSet summary: Update user value set definition description: Update value set definition in database (user-defined) security: @@ -5058,7 +6123,7 @@ paths: $ref: '#/components/schemas/ValueSetDef' delete: tags: - - ValueSets + - ValueSet summary: Delete user value set definition description: Delete value set definition from database (user-defined) security: @@ -5912,7 +6977,10 @@ components: type: object testdefgrp: type: array - description: Group members (for GROUP and CALC types) + description: | + Group members (for GROUP and CALC types). + When creating or updating, provide members in details.members array with TestSiteID field. + Do NOT use Member or SeqScr fields when creating/updating. items: type: object properties: @@ -5924,10 +6992,9 @@ components: description: Parent group TestSiteID Member: type: integer - description: Member TestSiteID (foreign key to testdefsite) - MemberTestSiteID: - type: integer - description: Member's actual TestSiteID (same as Member, for clarity) + description: | + Member TestSiteID (foreign key to testdefsite). + **Note**: This field is in the response. When creating/updating, use TestSiteID in details.members array instead. TestSiteCode: type: string description: Member test code @@ -6273,6 +7340,72 @@ components: type: string format: date-time description: Soft delete timestamp + OrderTestList: + type: object + properties: + InternalOID: + type: integer + description: Internal order ID + OrderID: + type: string + description: Order ID (e.g., 0025030300001) + PlacerID: + type: string + nullable: true + InternalPID: + type: integer + description: Patient internal ID + SiteID: + type: integer + PVADTID: + type: integer + description: Visit ADT ID + ReqApp: + type: string + nullable: true + Priority: + type: string + enum: + - R + - S + - U + description: | + R: Routine + S: Stat + U: Urgent + PriorityLabel: + type: string + description: Priority display text + TrnDate: + type: string + format: date-time + description: Transaction/Order date + EffDate: + type: string + format: date-time + description: Effective date + CreateDate: + type: string + format: date-time + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + description: | + ORD: Ordered + SCH: Scheduled + ANA: Analysis + VER: Verified + REV: Reviewed + REP: Reported + OrderStatusLabel: + type: string + description: Order status display text OrderTest: type: object properties: @@ -6716,6 +7849,73 @@ components: type: integer total_pages: type: integer + RuleDef: + type: object + properties: + RuleID: + type: integer + RuleCode: + type: string + example: AUTO_SET_RESULT + RuleName: + type: string + example: Automatically Set Result + Description: + type: string + nullable: true + EventCode: + type: string + example: ORDER_CREATED + ConditionExpr: + type: string + nullable: true + description: Raw DSL expression (editable) + example: if(sex('M'); result_set(0.5); result_set(0.6)) + ConditionExprCompiled: + type: string + nullable: true + description: Compiled JSON structure (auto-generated from ConditionExpr) + example: '{"conditionExpr":"patient[\"Sex\"] == \"M\"","valueExpr":"(patient[\"Sex\"] == \"M\") ? 0.5 : 0.6","then":[{"type":"RESULT_SET","value":0.5,"valueExpr":"0.5"}],"else":[{"type":"RESULT_SET","value":0.6,"valueExpr":"0.6"}]}' + CreateDate: + type: string + format: date-time + nullable: true + StartDate: + type: string + format: date-time + nullable: true + EndDate: + type: string + format: date-time + nullable: true + RuleWithDetails: + allOf: + - $ref: '#/components/schemas/RuleDef' + - type: object + properties: + linkedTests: + type: array + items: + type: integer + description: Array of TestSiteIDs this rule is linked to. Rules are active only when attached to tests. + TestRule: + type: object + description: Mapping between a rule and a test site (testrule table). Rules are active when linked via this table. + properties: + TestRuleID: + type: integer + RuleID: + type: integer + TestSiteID: + type: integer + CreateDate: + type: string + format: date-time + nullable: true + EndDate: + type: string + format: date-time + nullable: true Contact: type: object properties: @@ -6838,16 +8038,54 @@ components: type: string description: Test name nullable: true + TestType: + type: string + description: Test type code identifying the test category + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE SID: type: string description: Order ID reference SampleID: type: string description: Sample ID (same as OrderID) + SeqScr: + type: integer + nullable: true + description: Sequence number for this test on the screen + SeqRpt: + type: integer + nullable: true + description: Sequence number for this test in reports Result: type: string description: Test result value nullable: true + Discipline: + type: object + description: Discipline metadata used for ordering tests + properties: + DisciplineID: + type: integer + nullable: true + DisciplineCode: + type: string + nullable: true + DisciplineName: + type: string + nullable: true + SeqScr: + type: integer + nullable: true + description: Discipline sequence on the screen + SeqRpt: + type: integer + nullable: true + description: Discipline sequence in reports ResultDateTime: type: string format: date-time diff --git a/docs/backend-api-calculation.md b/docs/backend-api-calculation.md deleted file mode 100644 index e7ecfb8..0000000 --- a/docs/backend-api-calculation.md +++ /dev/null @@ -1,166 +0,0 @@ -# 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/docs/rules.yaml b/docs/rules.yaml index 014a9d5..932af1b 100644 --- a/docs/rules.yaml +++ b/docs/rules.yaml @@ -1,4 +1,4 @@ -/api/rules: +/api/rule: get: tags: [Rules] summary: List rules @@ -100,7 +100,7 @@ '201': description: Rule created -/api/rules/{id}: +/api/rule/{id}: get: tags: [Rules] summary: Get rule (with actions) @@ -178,7 +178,7 @@ '404': description: Rule not found -/api/rules/validate: +/api/rule/validate: post: tags: [Rules] summary: Validate/evaluate an expression @@ -201,7 +201,7 @@ '200': description: Validation result -/api/rules/{id}/actions: +/api/rule/{id}/actions: get: tags: [Rules] summary: List actions for a rule @@ -261,7 +261,7 @@ '201': description: Action created -/api/rules/{id}/actions/{actionId}: +/api/rule/{id}/actions/{actionId}: patch: tags: [Rules] summary: Update action diff --git a/docs/test-calc-engine.md b/docs/test-calc-engine.md new file mode 100644 index 0000000..94687a6 --- /dev/null +++ b/docs/test-calc-engine.md @@ -0,0 +1,337 @@ +# 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` diff --git a/docs/test-rule-engine.md b/docs/test-rule-engine.md new file mode 100644 index 0000000..1762b0a --- /dev/null +++ b/docs/test-rule-engine.md @@ -0,0 +1,421 @@ +# 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": "", + "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": "", + "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. diff --git a/src/lib/api/calculator.js b/src/lib/api/calculator.js new file mode 100644 index 0000000..aa38a49 --- /dev/null +++ b/src/lib/api/calculator.js @@ -0,0 +1,11 @@ +import { post } from './client.js'; + +/** + * Evaluate a configured calculation for a test site by forwarding the captured variables. + * @param {number} testSiteId - Identifier of the test site (TestSiteID) whose calculation definition should be used. + * @param {Record} [variables={}] - Input values required by the configured formula. + * @returns {Promise} API response from `/api/calc/testsite/{testSiteId}`. + */ +export async function calculateByTestSite(testSiteId, variables = {}) { + return post(`/api/calc/testsite/${testSiteId}`, variables); +} diff --git a/src/lib/api/contacts.js b/src/lib/api/contacts.js index 3a0d429..9e68ac2 100644 --- a/src/lib/api/contacts.js +++ b/src/lib/api/contacts.js @@ -13,8 +13,8 @@ export async function createContact(data) { return post('/api/contact', data); } -export async function updateContact(data) { - return patch('/api/contact', data); +export async function updateContact(id, data) { + return patch(`/api/contact/${id}`, data); } export async function deleteContact(id) { diff --git a/src/lib/api/containers.js b/src/lib/api/containers.js index 4dfb7c4..4b50031 100644 --- a/src/lib/api/containers.js +++ b/src/lib/api/containers.js @@ -21,9 +21,8 @@ export async function createContainer(data) { return post('/api/specimen/container', payload); } -export async function updateContainer(data) { +export async function updateContainer(id, data) { const payload = { - ConDefID: data.ConDefID, ConCode: data.ConCode, ConName: data.ConName, ConDesc: data.ConDesc, @@ -31,7 +30,7 @@ export async function updateContainer(data) { Additive: data.Additive, Color: data.Color, }; - return patch('/api/specimen/container', payload); + return patch(`/api/specimen/container/${id}`, payload); } export async function deleteContainer(id) { diff --git a/src/lib/api/counters.js b/src/lib/api/counters.js index 72e3d3b..1338d57 100644 --- a/src/lib/api/counters.js +++ b/src/lib/api/counters.js @@ -13,8 +13,8 @@ export async function createCounter(data) { return post('/api/counter', data); } -export async function updateCounter(data) { - return patch('/api/counter', data); +export async function updateCounter(id, data) { + return patch(`/api/counter/${id}`, data); } export async function deleteCounter(id) { diff --git a/src/lib/api/equipment.js b/src/lib/api/equipment.js index f5ac9b8..89cfc07 100644 --- a/src/lib/api/equipment.js +++ b/src/lib/api/equipment.js @@ -62,9 +62,8 @@ export async function createEquipment(data) { * @param {number} [data.WorkstationID] - Workstation ID * @returns {Promise} Updated equipment ID */ -export async function updateEquipment(data) { +export async function updateEquipment(id, data) { const payload = { - EID: data.EID, IEID: data.IEID, DepartmentID: data.DepartmentID, Enable: data.Enable, @@ -73,7 +72,7 @@ export async function updateEquipment(data) { InstrumentName: data.InstrumentName || null, WorkstationID: data.WorkstationID || null, }; - return patch('/api/equipmentlist', payload); + return patch(`/api/equipmentlist/${id}`, payload); } /** diff --git a/src/lib/api/locations.js b/src/lib/api/locations.js index 141649f..2241e92 100644 --- a/src/lib/api/locations.js +++ b/src/lib/api/locations.js @@ -19,15 +19,14 @@ export async function createLocation(data) { return post('/api/location', payload); } -export async function updateLocation(data) { +export async function updateLocation(id, data) { const payload = { - LocationID: data.LocationID, LocCode: data.Code, LocFull: data.Name, LocType: data.Type, Parent: data.ParentID, }; - return patch('/api/location', payload); + return patch(`/api/location/${id}`, payload); } export async function deleteLocation(id) { diff --git a/src/lib/api/occupations.js b/src/lib/api/occupations.js index 7c5ebe7..d35a9e9 100644 --- a/src/lib/api/occupations.js +++ b/src/lib/api/occupations.js @@ -18,12 +18,11 @@ export async function createOccupation(data) { return post('/api/occupation', payload); } -export async function updateOccupation(data) { +export async function updateOccupation(id, data) { const payload = { - OccupationID: data.OccupationID, OccCode: data.OccCode, OccText: data.OccText, Description: data.Description, }; - return patch('/api/occupation', payload); + return patch(`/api/occupation/${id}`, payload); } diff --git a/src/lib/api/orders.js b/src/lib/api/orders.js index 3936479..e159510 100644 --- a/src/lib/api/orders.js +++ b/src/lib/api/orders.js @@ -53,8 +53,8 @@ export async function createOrder(data) { * @param {number} [data.WorkstationID] - Workstation ID * @returns {Promise} API response with updated order data */ -export async function updateOrder(data) { - return patch('/api/ordertest', data); +export async function updateOrder(id, data) { + return patch(`/api/ordertest/${id}`, data); } /** diff --git a/src/lib/api/organization.js b/src/lib/api/organization.js index 84fad9f..6983c8c 100644 --- a/src/lib/api/organization.js +++ b/src/lib/api/organization.js @@ -21,16 +21,15 @@ export async function createDiscipline(data) { return post('/api/organization/discipline', payload); } -export async function updateDiscipline(data) { +export async function updateDiscipline(id, data) { const payload = { - id: data.DisciplineID, DisciplineCode: data.DisciplineCode, DisciplineName: data.DisciplineName, Parent: data.Parent || null, SeqScr: data.SeqScr, SeqRpt: data.SeqRpt, }; - return patch('/api/organization/discipline', payload); + return patch(`/api/organization/discipline/${id}`, payload); } export async function deleteDiscipline(id) { @@ -56,14 +55,13 @@ export async function createDepartment(data) { return post('/api/organization/department', payload); } -export async function updateDepartment(data) { +export async function updateDepartment(id, data) { const payload = { - id: data.DepartmentID, DeptCode: data.DeptCode, DeptName: data.DeptName, SiteID: data.SiteID, }; - return patch('/api/organization/department', payload); + return patch(`/api/organization/department/${id}`, payload); } export async function deleteDepartment(id) { @@ -89,14 +87,13 @@ export async function createSite(data) { return post('/api/organization/site', payload); } -export async function updateSite(data) { +export async function updateSite(id, data) { const payload = { - id: data.SiteID, SiteCode: data.SiteCode, SiteName: data.SiteName, AccountID: data.AccountID, }; - return patch('/api/organization/site', payload); + return patch(`/api/organization/site/${id}`, payload); } export async function deleteSite(id) { @@ -131,13 +128,12 @@ export async function createHostApp(data) { return post('/api/organization/hostapp', payload); } -export async function updateHostApp(data) { +export async function updateHostApp(id, data) { const payload = { - id: data.HostAppID, HostAppName: data.HostAppName, SiteID: data.SiteID, }; - return patch('/api/organization/hostapp', payload); + return patch(`/api/organization/hostapp/${id}`, payload); } export async function deleteHostApp(id) { @@ -164,15 +160,14 @@ export async function createHostComPara(data) { return post('/api/organization/hostcompara', payload); } -export async function updateHostComPara(data) { +export async function updateHostComPara(id, data) { const payload = { - id: data.HostComParaID, HostAppID: data.HostAppID, HostIP: data.HostIP, HostPort: data.HostPort, HostPwd: data.HostPwd, }; - return patch('/api/organization/hostcompara', payload); + return patch(`/api/organization/hostcompara/${id}`, payload); } export async function deleteHostComPara(id) { @@ -198,14 +193,13 @@ export async function createCodingSystem(data) { return post('/api/organization/codingsys', payload); } -export async function updateCodingSystem(data) { +export async function updateCodingSystem(id, data) { const payload = { - id: data.CodingSysID, CodingSysAbb: data.CodingSysAbb, FullText: data.FullText, Description: data.Description, }; - return patch('/api/organization/codingsys', payload); + return patch(`/api/organization/codingsys/${id}`, payload); } export async function deleteCodingSystem(id) { @@ -232,14 +226,13 @@ export async function createAccount(data) { return post('/api/organization/account', payload); } -export async function updateAccount(data) { +export async function updateAccount(id, data) { const payload = { - id: data.AccountID, AccountName: data.AccountName, Initial: data.Initial, Parent: data.Parent, }; - return patch('/api/organization/account', payload); + return patch(`/api/organization/account/${id}`, payload); } export async function deleteAccount(id) { @@ -257,15 +250,14 @@ export async function createWorkstation(data) { return post('/api/organization/workstation', payload); } -export async function updateWorkstation(data) { +export async function updateWorkstation(id, data) { const payload = { - id: data.WorkstationID, WorkstationCode: data.WorkstationCode, WorkstationName: data.WorkstationName, SiteID: data.SiteID, DepartmentID: data.DepartmentID, }; - return patch('/api/organization/workstation', payload); + return patch(`/api/organization/workstation/${id}`, payload); } export async function deleteWorkstation(id) { diff --git a/src/lib/api/patients.js b/src/lib/api/patients.js index 8b89090..75c4732 100644 --- a/src/lib/api/patients.js +++ b/src/lib/api/patients.js @@ -84,8 +84,8 @@ export async function createPatient(data) { * @param {Object} data - Patient data (must include PatientID) * @returns {Promise} API response */ -export async function updatePatient(data) { - return patch('/api/patient', data); +export async function updatePatient(id, data) { + return patch(`/api/patient/${id}`, data); } /** diff --git a/src/lib/api/rules.js b/src/lib/api/rule.js similarity index 83% rename from src/lib/api/rules.js rename to src/lib/api/rule.js index 5b7672a..7951ab8 100644 --- a/src/lib/api/rules.js +++ b/src/lib/api/rule.js @@ -21,7 +21,7 @@ import { get, post, patch, del } from './client.js'; */ export async function fetchRules(params = {}) { const query = new URLSearchParams(params).toString(); - return get(query ? `/api/rules?${query}` : '/api/rules'); + return get(query ? `/api/rule?${query}` : '/api/rule'); } /** @@ -30,7 +30,7 @@ export async function fetchRules(params = {}) { * @returns {Promise} */ export async function fetchRule(id) { - return get(`/api/rules/${id}`); + return get(`/api/rule/${id}`); } /** @@ -39,7 +39,7 @@ export async function fetchRule(id) { * @returns {Promise} */ export async function createRule(payload) { - return post('/api/rules', payload); + return post('/api/rule', payload); } /** @@ -49,7 +49,7 @@ export async function createRule(payload) { * @returns {Promise} */ export async function updateRule(id, payload) { - return patch(`/api/rules/${id}`, payload); + return patch(`/api/rule/${id}`, payload); } /** @@ -58,7 +58,7 @@ export async function updateRule(id, payload) { * @returns {Promise} */ export async function deleteRule(id) { - return del(`/api/rules/${id}`); + return del(`/api/rule/${id}`); } /** @@ -68,7 +68,7 @@ export async function deleteRule(id) { * @returns {Promise} */ export async function linkTestToRule(ruleId, testSiteId) { - return post(`/api/rules/${ruleId}/link`, { TestSiteID: testSiteId }); + return post(`/api/rule/${ruleId}/link`, { TestSiteID: testSiteId }); } /** @@ -78,7 +78,7 @@ export async function linkTestToRule(ruleId, testSiteId) { * @returns {Promise} */ export async function unlinkTestFromRule(ruleId, testSiteId) { - return post(`/api/rules/${ruleId}/unlink`, { TestSiteID: testSiteId }); + return post(`/api/rule/${ruleId}/unlink`, { TestSiteID: testSiteId }); } /** @@ -88,7 +88,7 @@ export async function unlinkTestFromRule(ruleId, testSiteId) { * @returns {Promise} */ export async function validateExpression(expr, context = {}) { - return post('/api/rules/validate', { expr, context }); + return post('/api/rule/validate', { expr, context }); } /** @@ -97,7 +97,7 @@ export async function validateExpression(expr, context = {}) { * @returns {Promise<{ compiled: any; conditionExprCompiled: string }>} */ export async function compileRuleExpr(expr) { - return post('/api/rules/compile', { expr }); + return post('/api/rule/compile', { expr }); } /** @@ -106,7 +106,7 @@ export async function compileRuleExpr(expr) { * @returns {Promise} */ export async function fetchRuleActions(id) { - return get(`/api/rules/${id}/actions`); + return get(`/api/rule/${id}/actions`); } /** @@ -116,7 +116,7 @@ export async function fetchRuleActions(id) { * @returns {Promise} */ export async function createRuleAction(id, payload) { - return post(`/api/rules/${id}/actions`, payload); + return post(`/api/rule/${id}/actions`, payload); } /** @@ -127,7 +127,7 @@ export async function createRuleAction(id, payload) { * @returns {Promise} */ export async function updateRuleAction(id, actionId, payload) { - return patch(`/api/rules/${id}/actions/${actionId}`, payload); + return patch(`/api/rule/${id}/actions/${actionId}`, payload); } /** @@ -137,5 +137,5 @@ export async function updateRuleAction(id, actionId, payload) { * @returns {Promise} */ export async function deleteRuleAction(id, actionId) { - return del(`/api/rules/${id}/actions/${actionId}`); + return del(`/api/rule/${id}/actions/${actionId}`); } diff --git a/src/lib/api/specialties.js b/src/lib/api/specialties.js index 6748a11..065b7ce 100644 --- a/src/lib/api/specialties.js +++ b/src/lib/api/specialties.js @@ -13,8 +13,8 @@ export async function createSpecialty(data) { return post('/api/medicalspecialty', data); } -export async function updateSpecialty(data) { - return patch('/api/medicalspecialty', data); +export async function updateSpecialty(id, data) { + return patch(`/api/medicalspecialty/${id}`, data); } export async function deleteSpecialty(id) { diff --git a/src/lib/api/testmap.js b/src/lib/api/testmap.js index 2d4526b..3e759bc 100644 --- a/src/lib/api/testmap.js +++ b/src/lib/api/testmap.js @@ -150,8 +150,8 @@ export async function createTestMap(data) { * @param {UpdateTestMapPayload} data - Header data * @returns {Promise<{success: boolean, data: number, message?: string}>} API response */ -export async function updateTestMap(data) { - return patch('/api/test/testmap', data); +export async function updateTestMap(id, data) { + return patch(`/api/test/testmap/${id}`, data); } /** @@ -210,8 +210,8 @@ export async function createTestMapDetail(data) { * @param {UpdateTestMapDetailPayload} data - Detail data * @returns {Promise<{success: boolean, message?: string}>} API response */ -export async function updateTestMapDetail(data) { - return patch('/api/test/testmap/detail', data); +export async function updateTestMapDetail(id, data) { + return patch(`/api/test/testmap/detail/${id}`, data); } /** diff --git a/src/lib/api/tests.js b/src/lib/api/tests.js index c7d2c33..5a15b71 100644 --- a/src/lib/api/tests.js +++ b/src/lib/api/tests.js @@ -177,9 +177,9 @@ export async function createTest(formData) { * @param {any} formData - The form state * @returns {Promise} API response */ -export async function updateTest(formData) { +export async function updateTest(id, formData) { const payload = buildPayload(formData, true); - return patch('/api/test', payload); + return patch(`/api/test/${id}`, payload); } /** diff --git a/src/lib/api/visits.js b/src/lib/api/visits.js index 1f995d4..b9941eb 100644 --- a/src/lib/api/visits.js +++ b/src/lib/api/visits.js @@ -17,8 +17,8 @@ export async function createVisit(data) { return post('/api/patvisit', data); } -export async function updateVisit(data) { - return patch('/api/patvisit', data); +export async function updateVisit(id, data) { + return patch(`/api/patvisit/${encodeURIComponent(id)}`, data); } export async function deleteVisit(id) { @@ -29,8 +29,8 @@ export async function createADT(data) { return post('/api/patvisitadt', data); } -export async function updateADT(data) { - return patch('/api/patvisitadt', data); +export async function updateADT(id, data) { + return patch(`/api/patvisitadt/${encodeURIComponent(id)}`, data); } export async function fetchVisitADTHistory(visitId) { diff --git a/src/lib/components/rules/RuleFormPage.svelte b/src/lib/components/rules/RuleFormPage.svelte index 71424f2..b4fd10f 100644 --- a/src/lib/components/rules/RuleFormPage.svelte +++ b/src/lib/components/rules/RuleFormPage.svelte @@ -1,7 +1,7 @@ + + + {#if !testSiteId} +
+ + Save the test first, so we can link rules by `TestSiteID`. +
+ {:else} +
+
+
+ +
+ + (ruleCode = event.currentTarget.value)} + disabled={isSaving} + /> +
+ {#if formErrors.RuleCode} +

{formErrors.RuleCode}

+ {/if} +
+
+ +
+ + (ruleName = event.currentTarget.value)} + disabled={isSaving} + /> +
+ {#if formErrors.RuleName} +

{formErrors.RuleName}

+ {/if} +
+
+ +
+ + + {#if formErrors.EventCode} +

{formErrors.EventCode}

+ {/if} +
+ +
+
+ + {statusBadge.text} +
+ +
+ Use the DSL syntax described in docs. + +
+ {#if compileError} +
+ + {compileError} +
+ {/if} +
+ + {#if compiledExprObject && compileStatus === 'success'} +
+

Compiled preview

+
{JSON.stringify(compiledExprObject, null, 2)}
+
+ {/if} +
+ {/if} + + {#snippet footer()} + + + {/snippet} + + {#if saveMessage} +
{saveMessage}
+ {/if} +
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 a4022ad..4a7d15d 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 @@ -140,6 +140,7 @@ Formula Syntax: Use curly braces to reference test codes, e.g., {'{HGB}'} + {'{MCV}'} +

Supported operators/functions are listed in docs/calculator-operators.md.

diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/RulesTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/RulesTab.svelte new file mode 100644 index 0000000..26dd8e8 --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/RulesTab.svelte @@ -0,0 +1,172 @@ + + +
+
+
+

Rules

+

Attach DSL-powered rules to this test. One test can have multiple rules.

+
+
+ {rulesCount} linked + +
+
+ + {#if !hasTestId} +
+ + Save the test first so we can link rules via `TestSiteID`. +
+ {:else} +
+ + +
+ +
+ {#if loading} +
+ + Loading linked rules... +
+ {:else if rules.length === 0} +
+ No rules are linked yet. Create one to run custom logic
when events fire. +
+ {:else} +
+ {#each rules as rule (rule.RuleID)} +
+
+
+
{rule.EventCode || 'ORDER_CREATED'}
+
{rule.RuleCode || 'UNTITLED'}
+
{rule.RuleName || 'Unnamed rule'}
+
+
+ + {rule.ConditionExprCompiled ? 'Compiled' : 'Needs compile'} + +
+
+
+

{rule.ConditionExpr || 'No expression defined'}

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

Rule engine reference

+

Rules run when `test_created` or `result_updated` fire. The DSL uses if(condition; then; else) syntax – see docs/test-rule-engine.md.

+
+ + +
diff --git a/src/routes/(app)/orders/+page.svelte b/src/routes/(app)/orders/+page.svelte index 6d34f61..caf5ec6 100644 --- a/src/routes/(app)/orders/+page.svelte +++ b/src/routes/(app)/orders/+page.svelte @@ -175,10 +175,7 @@ import OrderSearchBar from './OrderSearchBar.svelte'; try { if (orderForm.order) { // Update existing order - await updateOrder({ - OrderID: orderForm.order.OrderID, - ...formData - }); + await updateOrder(orderForm.order.OrderID, formData); toastSuccess('Order updated successfully'); } else { // Create new order diff --git a/src/routes/(app)/patients/PatientFormModal.svelte b/src/routes/(app)/patients/PatientFormModal.svelte index 0cca202..98ffa7b 100644 --- a/src/routes/(app)/patients/PatientFormModal.svelte +++ b/src/routes/(app)/patients/PatientFormModal.svelte @@ -222,7 +222,7 @@ }); if (isEdit) { - await updatePatient(payload); + await updatePatient(payload.InternalPID || patient?.InternalPID, payload); toastSuccess('Patient updated successfully'); } else { await createPatient(payload); diff --git a/src/routes/(app)/patients/VisitListModal.svelte b/src/routes/(app)/patients/VisitListModal.svelte index 4536331..5905b0d 100644 --- a/src/routes/(app)/patients/VisitListModal.svelte +++ b/src/routes/(app)/patients/VisitListModal.svelte @@ -220,7 +220,7 @@ let savedVisit; if (isEdit) { - savedVisit = await updateVisit(payload); + savedVisit = await updateVisit(payload.InternalPVID, payload); toastSuccess('Visit updated successfully'); } else { savedVisit = await createVisit(payload); diff --git a/src/routes/(app)/results/ResultEntryModal.svelte b/src/routes/(app)/results/ResultEntryModal.svelte index 39e0ae3..2379853 100644 --- a/src/routes/(app)/results/ResultEntryModal.svelte +++ b/src/routes/(app)/results/ResultEntryModal.svelte @@ -18,6 +18,7 @@ Calculator, RefreshCw } from 'lucide-svelte'; +import { calculateByTestSite } from '$lib/api/calculator.js'; import { updateResult } from '$lib/api/results.js'; import { error as toastError, success as toastSuccess } from '$lib/utils/toast.js'; import Modal from '$lib/components/Modal.svelte'; @@ -121,11 +122,6 @@ console.log('Calc defs:', calcDefs.size, 'calculated tests'); console.log('Calc test IDs:', Array.from(calcDefs.keys())); - // Trigger initial calculation for CALC tests - if (calcDefs.size > 0) { - console.log('Triggering initial calculation...'); - setTimeout(() => recalculateAll(), 0); - } } /** @@ -332,6 +328,15 @@ await computeCalculations(calcsToCompute); } } + + async function recalculateSingle(testSiteId) { + const def = calcDefs.get(testSiteId); + const row = results.find(r => r.TestSiteID === testSiteId); + if (!def || !row) return; + + await computeCalculations([{ calcId: testSiteId, def, row }]); + await recalculateFrom(testSiteId); + } /** * Compute multiple calculations via backend API @@ -339,77 +344,72 @@ */ async function computeCalculations(calcsToCompute) { calcLoading = true; - + try { - // Build batch request - const calculations = calcsToCompute.map(({ def }) => { - // Collect member values + const calculations = calcsToCompute.map(({ def, row }) => { const values = {}; - let incomplete = false; - - for (const member of def.members) { - const rawValue = resultMapById.get(member.id); - if (rawValue === '' || rawValue == null) { - incomplete = true; - break; + for (const [code, rawValue] of resultMapByCode) { + if (def.code && code === def.code) continue; + const numValue = rawValue === '' || rawValue == null ? null : parseFloat(rawValue); + if (Number.isFinite(numValue)) { + values[code] = numValue; } - const numValue = parseFloat(rawValue); - if (!Number.isFinite(numValue)) { - incomplete = true; - break; - } - values[member.code] = numValue; } - + return { testSiteId: def.id, + row, formula: def.formula, values, - decimal: def.decimal, - incomplete + decimal: def.decimal }; }); - - // Filter out incomplete calculations - const validCalculations = calculations.filter(c => !c.incomplete); - - if (validCalculations.length === 0) { - // Clear results for incomplete calculations - for (const { row } of calcsToCompute) { - const index = results.findIndex(r => r.TestSiteID === row.TestSiteID); - if (index !== -1) { - results[index].Result = ''; - results[index].changedByAutoCalc = true; - results[index].lastAutoCalcAt = Date.now(); - results[index].warning = 'Missing dependency values'; - updateResultFlag(index); + + const calcOutputs = await Promise.all(calculations.map(async calc => { + try { + const response = await calculateByTestSite(calc.testSiteId, calc.values); + const apiResult = Number(response?.data?.result); + + if (response?.status !== 'success' || !Number.isFinite(apiResult)) { + throw new Error(response?.message || 'Calculator service returned an invalid response'); } + + return { + testSiteId: calc.testSiteId, + row: calc.row, + result: apiResult, + decimal: calc.decimal + }; + } catch (err) { + return { + testSiteId: calc.testSiteId, + row: calc.row, + error: { type: 'CALC_API_ERROR', message: err?.message || 'Failed to calculate result' } + }; } - return; - } - - // TODO: Replace with actual backend API call - // const response = await evaluateCalculations(validCalculations); - // For now, compute locally until backend is ready - const calcOutputs = computeLocally(validCalculations); - - // Update results + })); + for (const calcResult of calcOutputs) { const index = results.findIndex(r => r.TestSiteID === calcResult.testSiteId); if (index === -1) continue; - + if (calcResult.error) { results[index].warning = calcResult.error.message; } else { - results[index].Result = String(calcResult.resultRounded); + const factor = Math.pow(10, calcResult.decimal ?? 2); + const rounded = Math.round(calcResult.result * factor) / factor; + + results[index].Result = String(rounded); results[index].changedByAutoCalc = true; results[index].lastAutoCalcAt = Date.now(); results[index].warning = null; + results[index].error = null; updateResultFlag(index); - - // Update lookup maps + resultMapById.set(calcResult.testSiteId, results[index].Result); - resultMapByCode.set(results[index].TestSiteCode, results[index].Result); + if (results[index].TestSiteCode) { + resultMapByCode.set(results[index].TestSiteCode, results[index].Result); + } } } } catch (err) { @@ -419,58 +419,6 @@ calcLoading = false; } } - - /** - * Temporary local computation until backend API is ready - * @param {Array} calculations - Calculations to compute - * @returns {Array} Computation results - */ - function computeLocally(calculations) { - return calculations.map(calc => { - try { - // Replace variable names with values (word boundary matching) - let expression = calc.formula; - for (const [code, value] of Object.entries(calc.values)) { - // Use word boundary regex to match exact variable names - const regex = new RegExp(`\\b${code}\\b`, 'g'); - expression = expression.replace(regex, value); - } - - // Validate expression characters - if (!/^[\d\s+\-*/.()]+$/.test(expression)) { - return { - testSiteId: calc.testSiteId, - error: { type: 'INVALID_EXPRESSION', message: 'Invalid characters in formula' } - }; - } - - // Evaluate (temporary - will be replaced by backend) - const result = Function('return ' + expression)(); - - if (!Number.isFinite(result)) { - return { - testSiteId: calc.testSiteId, - error: { type: 'NON_FINITE', message: 'Result is not a valid number' } - }; - } - - // Round to specified decimal places - const factor = Math.pow(10, calc.decimal); - const resultRounded = Math.round(result * factor) / factor; - - return { - testSiteId: calc.testSiteId, - result, - resultRounded - }; - } catch (err) { - return { - testSiteId: calc.testSiteId, - error: { type: 'EVAL_ERROR', message: err.message || 'Formula evaluation failed' } - }; - } - }); - } /** * Handle input change - update flag and trigger recalculation of dependent fields @@ -698,7 +646,7 @@ Specimen Information
- {#each order.Specimens as specimen} + {#each order.Specimens as specimen, index (specimen.SpecimenID ?? specimen.Barcode ?? index)}
@@ -808,7 +756,7 @@ />