Cleanup: Remove obsolete docs, opencode configs, and openspec files; update test models and API docs

This commit is contained in:
mahdahar 2026-03-09 16:49:03 +07:00
parent 282c642da6
commit 011a2456c2
40 changed files with 476 additions and 15914 deletions

View File

@ -1,149 +0,0 @@
---
description: Implement tasks from an OpenSpec change (Experimental)
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- Context file paths (varies by schema)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx-continue`
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
4. **Read context files**
Read the files listed in `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]``- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! You can archive this change with `/opsx-archive`.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

View File

@ -1,154 +0,0 @@
---
description: Archive a completed change in the experimental workflow
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name after `/opsx-archive` (e.g., `/opsx-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Prompt user for confirmation to continue
- Proceed if user confirms
3. **Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Prompt user for confirmation to continue
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4. **Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move the change directory to archive
```bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Spec sync status (synced / sync skipped / no delta specs)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs
All artifacts complete. All tasks complete.
```
**Output On Success (No Delta Specs)**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** No delta specs
All artifacts complete. All tasks complete.
```
**Output On Success With Warnings**
```
## Archive Complete (with warnings)
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** Sync skipped (user chose to skip)
**Warnings:**
- Archived with 2 incomplete artifacts
- Archived with 3 incomplete tasks
- Delta spec sync was skipped (user chose to skip)
Review the archive if this was not intentional.
```
**Output On Error (Archive Exists)**
```
## Archive Failed
**Change:** <change-name>
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
Target archive directory already exists.
**Options:**
1. Rename the existing archive
2. Delete the existing archive if it's a duplicate
3. Wait until a different date to archive
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@ -1,170 +0,0 @@
---
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Input**: The argument after `/opsx-explore` is whatever the user wants to think about. Could be:
- A vague idea: "real-time collaboration"
- A specific problem: "the auth system is getting unwieldy"
- A change name: "add-dark-mode" (to explore in context of that change)
- A comparison: "postgres vs sqlite for this"
- Nothing (just enter explore mode)
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
If the user mentioned a specific change name, read its artifacts for context.
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Read existing artifacts for context**
- `openspec/changes/<name>/proposal.md`
- `openspec/changes/<name>/design.md`
- `openspec/changes/<name>/tasks.md`
- etc.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own

View File

@ -1,103 +0,0 @@
---
description: Propose a new change - create it and generate all artifacts in one step
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx-apply
---
**Input**: The argument after `/opsx-propose` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx-apply` to start implementing."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@ -1,156 +0,0 @@
---
name: openspec-apply-change
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
4. **Read context files**
Read the files listed in `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]``- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! Ready to archive this change.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

View File

@ -1,114 +0,0 @@
---
name: openspec-archive-change
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
3. **Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4. **Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move the change directory to archive
```bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Whether specs were synced (if applicable)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
All artifacts complete. All tasks complete.
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use openspec-sync-specs approach (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@ -1,288 +0,0 @@
---
name: openspec-explore
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Read existing artifacts for context**
- `openspec/changes/<name>/proposal.md`
- `openspec/changes/<name>/design.md`
- `openspec/changes/<name>/tasks.md`
- etc.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Handling Different Entry Points
**User brings a vague idea:**
```
User: I'm thinking about adding real-time collaboration
You: Real-time collab is a big space. Let me think about this...
COLLABORATION SPECTRUM
════════════════════════════════════════════
Awareness Coordination Sync
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Presence│ │Cursors │ │ CRDT │
│ "3 │ │ Multi │ │Conflict│
│online" │ │ select │ │ free │
└────────┘ └────────┘ └────────┘
│ │ │
trivial moderate complex
Where's your head at?
```
**User brings a specific problem:**
```
User: The auth system is a mess
You: [reads codebase]
┌─────────────────────────────────────────────┐
│ CURRENT AUTH FLOW │
└─────────────────────────────────────────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Google │ │ GitHub │ │ Email │
│ OAuth │ │ OAuth │ │ Magic │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
┌───────────┐
│ Session │
└─────┬─────┘
┌───────────┐
│ Perms │
└───────────┘
I see three tangles. Which one's burning?
```
**User is stuck mid-implementation:**
```
User: /opsx-explore add-auth-system
The OAuth integration is more complex than expected
You: [reads change artifacts]
You're on task 4: "Implement OAuth flow"
Let me trace what's involved...
[draws diagram, explores options, suggests paths]
Want to update the design to reflect this?
Or add a spike task to investigate?
```
**User wants to compare options:**
```
User: Should we use Postgres or SQLite?
You: Generic answer is boring. What's the context?
User: A CLI tool that tracks local dev environments
You: That changes everything.
┌─────────────────────────────────────────────────┐
│ CLI TOOL DATA STORAGE │
└─────────────────────────────────────────────────┘
Key constraints:
• No daemon running
• Must work offline
• Single user
SQLite Postgres
Deployment embedded ✓ needs server ✗
Offline yes ✓ no ✗
Single file yes ✓ no ✗
SQLite. Not even close.
Unless... is there a sync component?
```
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When it feels like things are crystallizing, you might summarize:
```
## What We Figured Out
**The problem**: [crystallized understanding]
**The approach**: [if one emerged]
**Open questions**: [if any remain]
**Next steps** (if ready):
- Create a change proposal
- Keep exploring: just keep talking
```
But this summary is optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own

View File

@ -1,110 +0,0 @@
---
name: openspec-propose
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx-apply
---
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx-apply` or ask me to implement to start working on the tasks."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@ -0,0 +1,223 @@
# CLQMS Code Style and Conventions
## Naming Conventions
| Element | Convention | Example |
|---------|-----------|---------|
| Classes | PascalCase | `PatientController`, `PatientModel` |
| Methods | camelCase | `createPatient()`, `getPatients()` |
| Properties | snake_case (legacy) / camelCase (new) | `$patient_id` / `$patientId` |
| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
| Database Tables | snake_case | `patient`, `patient_visits`, `order_tests` |
| Database Columns | PascalCase (legacy) | `PatientID`, `NameFirst`, `Birthdate`, `CreatedAt` |
| JSON Fields | PascalCase | `"PatientID": "123"` |
## File and Directory Structure
### Controllers
- Grouped by domain in subdirectories: `app/Controllers/Patient/`, `app/Controllers/Specimen/`
- Each controller handles CRUD for its entity
- Use `ResponseTrait` for standardized responses
### Models
- Grouped by domain: `app/Models/Patient/`, `app/Models/Specimen/`
- Extend `BaseModel` for automatic UTC date handling
- Define `$table`, `$primaryKey`, `$allowedFields`
- Use `checkDbError()` for database error detection
## Code Patterns
### Controller Structure
```php
<?php
namespace App\Controllers\Patient;
use App\Traits\ResponseTrait;
use CodeIgniter\Controller;
use App\Models\Patient\PatientModel;
class PatientController extends Controller {
use ResponseTrait;
protected $db;
protected $model;
protected $rules;
public function __construct() {
$this->db = \Config\Database::connect();
$this->model = new PatientModel();
$this->rules = [ /* validation rules */ ];
}
public function index() { /* ... */ }
public function create() { /* ... */ }
public function show($id) { /* ... */ }
public function update() { /* ... */ }
public function delete() { /* ... */ }
}
```
### Model Structure
```php
<?php
namespace App\Models\Patient;
use App\Models\BaseModel;
use App\Services\AuditService;
class PatientModel extends BaseModel {
protected $table = 'patient';
protected $primaryKey = 'InternalPID';
protected $allowedFields = ['PatientID', 'NameFirst', ...];
protected $useTimestamps = true;
protected $createdField = 'CreateDate';
protected $useSoftDeletes = true;
protected $deletedField = 'DelDate';
public function getPatients($filters = []) { /* ... */ }
public function createPatient($input) { /* ... */ }
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception("{$context} failed: {$error['code']} - {$error['message']}");
}
}
}
```
### Validation Rules
- Define in controller constructor as `$this->rules`
- Use CodeIgniter validation rules: `required`, `permit_empty`, `regex_match`, `max_length`, etc.
- For nested data, override rules dynamically based on input
### Response Format
```php
// Success
return $this->respond([
'status' => 'success',
'message' => 'Operation completed',
'data' => $data
], 200);
// Error
return $this->respond([
'status' => 'failed',
'message' => 'Error description',
'data' => []
], 400);
```
**Note:** Custom `ResponseTrait` automatically converts empty strings to `null`.
### Error Handling
- Use try-catch for JWT and external calls
- Log errors: `log_message('error', $message)`
- Return structured error responses with appropriate HTTP status codes
```php
try {
$decoded = JWT::decode($token, new Key($key, 'HS256'));
} catch (\Firebase\JWT\ExpiredException $e) {
return $this->respond(['status' => 'failed', 'message' => 'Token expired'], 401);
} catch (\Exception $e) {
return $this->respond(['status' => 'failed', 'message' => 'Invalid token'], 401);
}
```
### Database Operations
- Use CodeIgniter Query Builder or Model methods
- Use `helper('utc')` for UTC date conversion
- Wrap multi-table operations in transactions
```php
$this->db->transStart();
// ... database operations
$this->db->transComplete();
if ($this->db->transStatus() === false) {
return $this->respond(['status' => 'error', 'message' => 'Transaction failed'], 500);
}
```
### Audit Logging
Use `AuditService::logData()` for tracking data changes:
```php
AuditService::logData(
'CREATE|UPDATE|DELETE',
'table_name',
(string) $recordId,
'entity_name',
null,
$previousData,
$newData,
'Action description',
['metadata' => 'value']
);
```
## Route Patterns
```php
$routes->group('api/patient', function ($routes) {
$routes->get('/', 'Patient\PatientController::index');
$routes->post('/', 'Patient\PatientController::create');
$routes->get('(:num)', 'Patient\PatientController::show/$1');
$routes->patch('/', 'Patient\PatientController::update');
$routes->delete('/', 'Patient\PatientController::delete');
});
```
## Testing Guidelines
```php
<?php
namespace Tests\Feature\Patients;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
use Faker\Factory;
class PatientCreateTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected $endpoint = 'api/patient';
public function testCreatePatientSuccess()
{
$faker = Factory::create('id_ID');
$payload = [...];
$result = $this->withBodyFormat('json')->post($this->endpoint, $payload);
$result->assertStatus(201);
}
}
```
**Test Naming:** `test<Action><Scenario><ExpectedResult>` (e.g., `testCreatePatientValidationFail`)
## Security Best Practices
- Use `auth` filter for protected routes
- Sanitize user inputs with validation rules
- Use parameterized queries (CodeIgniter Query Builder handles this)
- Store secrets in `.env`, never commit to repository
## Legacy Field Naming
Database uses PascalCase columns: `PatientID`, `NameFirst`, `Birthdate`, `CreatedAt`, `UpdatedAt`
## ValueSet/Lookup Usage
```php
use App\Libraries\Lookups;
// Get all lookups
$allLookups = Lookups::getAll();
// Get single lookup formatted for dropdowns
$gender = Lookups::get('gender');
// Get label for a specific key
$label = Lookups::getLabel('gender', '1'); // Returns 'Female'
// Transform database records with lookup text labels
$labeled = Lookups::transformLabels($data, ['Sex' => 'gender']);
```

View File

@ -0,0 +1,22 @@
# EquipmentList Seeder Merged
## Changes
- EquipmentListSeeder.php deleted (redundant)
- Equipment data now in OrganizationSeeder.php only
- Equipment seeding happens via: php spark db:seed OrganizationSeeder
## Equipment Data Location
File: app/Database/Seeds/OrganizationSeeder.php (lines 68-99)
## Seed Command
```bash
php spark db:seed OrganizationSeeder
```
This seeds:
- Account (3 records)
- Site (2 records)
- Discipline (13 records)
- Department (6 records)
- Workstation (9 records)
- EquipmentList (18 records)

View File

@ -0,0 +1,32 @@
# EquipmentList Seeder
## Summary
- Created: app/Database/Seeds/EquipmentListSeeder.php
- Total dummy records: 18 equipment
- Roles: A (Auto), B (Backup), M (Manual)
## Distribution
- Hematology (Dept 1): 5 equipment (including disabled)
- Chemistry (Dept 3): 5 equipment (including disabled)
- Immunology (Dept 4): 4 equipment
- Urinalysis (Dept 6): 4 equipment
## Equipment by Workstation
- WS 1 (Hem Auto): 2 equipment
- WS 2 (Hem Backup): 2 equipment
- WS 3 (Chem Auto): 2 equipment
- WS 4 (Chem Backup): 1 equipment
- WS 5 (Chem Manual): 1 equipment
- WS 6 (Imm Auto): 2 equipment
- WS 7 (Imm Manual): 2 equipment
- WS 8 (Uri Auto): 2 equipment
- WS 9 (Uri Manual): 2 equipment
## Equipment Roles
- A (Auto): 10 equipment
- B (Backup): 4 equipment
- M (Manual): 4 equipment
## Status
- Enabled: 16 equipment
- Disabled: 2 equipment (IDs 17, 18)

View File

@ -0,0 +1,129 @@
# CLQMS Task Completion Guidelines
## When a Task is Completed
### 1. Run Tests
Always run relevant tests after making changes:
```bash
# Run all tests
./vendor/bin/phpunit
# Run specific test file
./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php
# Run specific test method
./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php
```
### 2. Verify Code Quality
#### Check for Syntax Errors
```bash
php -l app/Controllers/Patient/PatientController.php
```
#### Check Test Results
Ensure:
- All existing tests still pass
- New tests (if any) pass
- No unexpected warnings or errors
### 3. Code Review Checklist
#### Controller Changes
- [ ] Validation rules are properly defined
- [ ] Error handling uses try-catch blocks
- [ ] Responses use standardized format with `ResponseTrait`
- [ ] Authentication filter applied where needed
- [ ] Input sanitization via validation rules
- [ ] Database operations wrapped in transactions (if multi-table)
#### Model Changes
- [ ] Extends `BaseModel` for UTC handling
- [ ] Table name, primary key, allowed fields defined
- [ ] Uses `checkDbError()` for error detection
- [ ] Audit logging via `AuditService::logData()` for data changes
- [ ] Soft delete fields configured if using soft deletes
- [ ] Nested data properly handled (extracted before filtering)
#### New Routes
- [ ] Added to `app/Config/Routes.php`
- [ ] Follows REST conventions (GET, POST, PATCH, DELETE)
- [ ] Grouped appropriately under `/api/`
- [ ] Auth filter applied if needed
#### New Tests
- [ ] Test name follows `test<Action><Scenario><ExpectedResult>` pattern
- [ ] Uses `FeatureTestTrait`
- [ ] Uses `Factory::create('id_ID')` for Indonesian test data
- [ ] Asserts correct status codes (200, 201, 400, 401, 404, 500)
- [ ] Tests both success and failure scenarios
### 4. Common Issues to Check
#### Database Operations
- Multi-table operations should use transactions
- Use parameterized queries (Query Builder handles this)
- Check for empty/null arrays before processing
#### Nested Data
- Extract nested arrays before filtering/processing
- Handle empty/null nested data appropriately
- Use transactions for multi-table operations
#### Date Handling
- All dates stored in UTC
- Use `helper('utc')` for conversions
- BaseModel extends with automatic UTC conversion
#### Security
- Input validation rules defined
- SQL injection prevention via parameterized queries
- JWT validation on protected endpoints
- No sensitive data logged or exposed
### 5. What NOT to Do
- **Do NOT commit** unless explicitly asked by the user
- **Do NOT push** to remote repository unless asked
- **Do NOT** skip running tests
- **Do NOT** add comments unless specifically requested
- **Do NOT** modify `.env` (database credentials, secrets)
- **Do NOT** include hardcoded secrets in code
### 6. Common Test Status Codes Reference
| Code | Usage | Example |
|------|-------|---------|
| 200 | GET/PATCH success | `->assertStatus(200)` |
| 201 | POST success (created) | `->assertStatus(201)` |
| 400 | Validation error | `->assertStatus(400)` |
| 401 | Unauthorized | `->assertStatus(401)` |
| 404 | Not found | `->assertStatus(404)` |
| 500 | Server error | `->assertStatus(500)` |
### 7. After Completing Tasks
Simply inform the user the task is complete. For example:
```
Task completed. The patient create endpoint now validates the identifier type dynamically.
```
Or if tests were run:
```
Task completed. All tests passing:
- testCreatePatientSuccess ✓
- testCreatePatientValidationFail ✓
```
### 8. When Something Goes Wrong
If tests fail or errors occur:
1. Check the error message carefully
2. Review the code against the patterns in this guide
3. Check database connection and configuration
4. Verify all required dependencies are installed
5. Review log files in `writable/logs/`
If unable to resolve, inform the user with details of the issue.

View File

@ -25,7 +25,7 @@ class TestDefGrpModel extends BaseModel {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$rows = $db->table('testdefgrp') $rows = $db->table('testdefgrp')
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType') ->select('testdefgrp.*, t.TestSiteID as MemberTestSiteID, t.TestSiteCode, t.TestSiteName, t.TestType')
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left') ->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
->where('testdefgrp.TestSiteID', $testSiteID) ->where('testdefgrp.TestSiteID', $testSiteID)
->where('testdefgrp.EndDate IS NULL') ->where('testdefgrp.EndDate IS NULL')

View File

@ -159,7 +159,7 @@ class TestDefSiteModel extends BaseModel {
} elseif (TestValidationService::isGroup($typeCode)) { } elseif (TestValidationService::isGroup($typeCode)) {
$row['testdefgrp'] = $db->table('testdefgrp') $row['testdefgrp'] = $db->table('testdefgrp')
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType') ->select('testdefgrp.*, t.TestSiteID as MemberTestSiteID, t.TestSiteCode, t.TestSiteName, t.TestType')
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left') ->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
->where('testdefgrp.TestSiteID', $TestSiteID) ->where('testdefgrp.TestSiteID', $TestSiteID)
->where('testdefgrp.EndDate IS NULL') ->where('testdefgrp.EndDate IS NULL')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,762 +0,0 @@
# Audit Logging Architecture Plan for CLQMS
> **Clinical Laboratory Quality Management System (CLQMS)** - Comprehensive audit trail implementation based on section 4.2.1.20 Error Management requirements, implementing 5W1H audit principles across four specialized log types.
---
## Executive Summary
This document defines the audit logging architecture for CLQMS, implementing the **5W1H audit principle** (What, When, Who, How, Where, Why) across four specialized log tables. The design supports both **manual** (user-initiated) and **automatic** (instrument/service-initiated) operations with complete traceability.
---
## 1. Requirements Analysis (Section 4.2.1.20)
### 5W1H Audit Principles
| Dimension | Description | Captured Fields |
|-----------|-------------|-----------------|
| **What** | Data changed, operation performed | `operation`, `table_name`, `field_name`, `previous_value`, `new_value` |
| **When** | Timestamp of activity | `created_at` |
| **Who** | User performing operation | `user_id` |
| **How** | Mechanism, application, session | `mechanism`, `application_id`, `web_page`, `session_id`, `event_type` |
| **Where** | Location of operation | `site_id`, `workstation_id`, `pc_name`, `ip_address` |
| **Why** | Reason for operation | `reason` |
### Four Log Types
| Log Type | Description | Examples |
|----------|-------------|----------|
| **Data Log** | Events related to data operations | Patient demographics, visits, test orders, samples, results, user data, master data, archiving, transaction errors |
| **Service Log** | Background service events | Host communication, instrument communication, printing, messaging, resource access, system errors |
| **Security Log** | Security and access events | Logins/logouts, file access, permission changes, password failures, system changes |
| **Error Log** | Error events by entity | Instrument errors, integration errors, validation errors |
### Mechanism Types
- **MANUAL**: User-initiated actions via web interface
- **AUTOMATIC**: System/instrument-initiated (duplo/repeated operations)
---
## 2. Table Architecture
### 2.1 Overview
Four separate tables optimized for different volumes and retention:
| Table | Volume | Retention | Partitioning |
|-------|--------|-----------|--------------|
| `data_audit_log` | Medium | 7 years | Monthly |
| `service_audit_log` | Very High | 2 years | Monthly |
| `security_audit_log` | Low | Permanent | No |
| `error_audit_log` | Variable | 5 years | Monthly |
---
### 2.2 Table: data_audit_log
```sql
CREATE TABLE data_audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- WHAT: Operation and data details
operation VARCHAR(50) NOT NULL, -- 'CREATE', 'UPDATE', 'DELETE', 'ARCHIVE', etc.
entity_type VARCHAR(50) NOT NULL, -- 'patient', 'visit', 'test_order', 'sample', 'user', etc.
entity_id VARCHAR(36) NOT NULL, -- ID of affected entity
table_name VARCHAR(100), -- Database table name
field_name VARCHAR(100), -- Specific field changed (NULL if multiple)
previous_value JSON, -- Value before change
new_value JSON, -- Value after change
-- HOW: Mechanism details
mechanism ENUM('MANUAL', 'AUTOMATIC') NOT NULL DEFAULT 'MANUAL',
application_id VARCHAR(50), -- Application identifier
web_page VARCHAR(500), -- URL/endpoint accessed
session_id VARCHAR(100), -- Session identifier
event_type VARCHAR(100), -- Event classification
-- WHERE: Location information
site_id VARCHAR(36), -- Site/location ID
workstation_id VARCHAR(36), -- Workstation ID
pc_name VARCHAR(100), -- Computer name
ip_address VARCHAR(45), -- IP address (IPv6 compatible)
-- WHO: User information
user_id VARCHAR(36) NOT NULL, -- User ID or 'SYSTEM' for automatic
-- WHEN: Timestamp
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- WHY: Reason
reason TEXT, -- User-provided reason
-- Context: Additional flexible data
context JSON, -- Log-type-specific extra data
-- Indexes
INDEX idx_operation_created (operation, created_at),
INDEX idx_entity (entity_type, entity_id, created_at),
INDEX idx_user_created (user_id, created_at),
INDEX idx_mechanism (mechanism, created_at),
INDEX idx_table (table_name, created_at),
INDEX idx_site (site_id, created_at),
INDEX idx_created (created_at),
INDEX idx_session (session_id, created_at)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202601 VALUES LESS THAN (202602),
PARTITION p202602 VALUES LESS THAN (202603),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
```
**Data Log Examples:**
- Patient registration: `operation='CREATE'`, `entity_type='patient'`, `mechanism='MANUAL'`
- Sample result from instrument: `operation='UPDATE'`, `entity_type='result'`, `mechanism='AUTOMATIC'`
- User profile update: `operation='UPDATE'`, `entity_type='user'`, `mechanism='MANUAL'`
---
### 2.3 Table: service_audit_log
```sql
CREATE TABLE service_audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- WHAT: Service operation details
operation VARCHAR(50) NOT NULL, -- 'COMMUNICATION', 'PRINT', 'BACKUP', 'MESSAGE', etc.
entity_type VARCHAR(50) NOT NULL, -- 'host', 'instrument', 'database', 'network', etc.
entity_id VARCHAR(36) NOT NULL, -- Service identifier
service_class VARCHAR(50), -- 'communication', 'printing', 'messaging', 'resource'
resource_type VARCHAR(100), -- 'database_access', 'backup', 'network', 'internet'
resource_details JSON, -- IP, port, connection details
previous_value JSON, -- State before
new_value JSON, -- State after
-- HOW: Mechanism and context
mechanism ENUM('MANUAL', 'AUTOMATIC') NOT NULL DEFAULT 'AUTOMATIC',
application_id VARCHAR(50), -- Service application ID
service_name VARCHAR(100), -- Background service name
session_id VARCHAR(100), -- Service session
event_type VARCHAR(100), -- Event classification
-- WHERE: Location and resources
site_id VARCHAR(36),
workstation_id VARCHAR(36),
pc_name VARCHAR(100),
ip_address VARCHAR(45),
port INT, -- Port number for network
-- WHO: System or user
user_id VARCHAR(36) NOT NULL, -- 'SYSTEM' for automatic services
-- WHEN: Timestamp
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- WHY: Reason if manual
reason TEXT,
-- Context: Service-specific data
context JSON, -- Communication details, error codes, etc.
-- Indexes
INDEX idx_operation_created (operation, created_at),
INDEX idx_entity (entity_type, entity_id, created_at),
INDEX idx_service_class (service_class, created_at),
INDEX idx_user_created (user_id, created_at),
INDEX idx_mechanism (mechanism, created_at),
INDEX idx_site (site_id, created_at),
INDEX idx_created (created_at)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202601 VALUES LESS THAN (202602),
PARTITION p202602 VALUES LESS THAN (202603),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
```
**Service Log Examples:**
- Instrument communication: `operation='COMMUNICATION'`, `entity_type='instrument'`, `service_class='communication'`
- Database backup: `operation='BACKUP'`, `entity_type='database'`, `service_class='resource'`
- Automatic print: `operation='PRINT'`, `service_class='printing'`, `mechanism='AUTOMATIC'`
---
### 2.4 Table: security_audit_log
```sql
CREATE TABLE security_audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- WHAT: Security event details
operation VARCHAR(50) NOT NULL, -- 'LOGIN', 'LOGOUT', 'ACCESS_DENIED', 'PASSWORD_FAIL', etc.
entity_type VARCHAR(50) NOT NULL, -- 'user', 'file', 'folder', 'setting', 'application'
entity_id VARCHAR(36) NOT NULL, -- Target entity ID
security_class VARCHAR(50), -- 'authentication', 'authorization', 'system_change'
resource_path VARCHAR(500), -- File/folder path accessed
previous_value JSON, -- Previous security state
new_value JSON, -- New security state
-- HOW: Access details
mechanism ENUM('MANUAL', 'AUTOMATIC') NOT NULL DEFAULT 'MANUAL',
application_id VARCHAR(50),
web_page VARCHAR(500),
session_id VARCHAR(100),
event_type VARCHAR(100), -- 'SUCCESS', 'FAILURE', 'WARNING'
-- WHERE: Access location
site_id VARCHAR(36),
workstation_id VARCHAR(36),
pc_name VARCHAR(100),
ip_address VARCHAR(45),
-- WHO: User attempting action
user_id VARCHAR(36) NOT NULL, -- User ID or 'UNKNOWN' for failed attempts
-- WHEN: Timestamp
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- WHY: Reason if provided
reason TEXT,
-- Context: Security-specific data
context JSON, -- Permission changes, failure counts, etc.
-- Indexes
INDEX idx_operation_created (operation, created_at),
INDEX idx_entity (entity_type, entity_id, created_at),
INDEX idx_security_class (security_class, created_at),
INDEX idx_user_created (user_id, created_at),
INDEX idx_event_type (event_type, created_at),
INDEX idx_site (site_id, created_at),
INDEX idx_created (created_at),
INDEX idx_session (session_id, created_at)
) ENGINE=InnoDB;
```
**Security Log Examples:**
- User login: `operation='LOGIN'`, `entity_type='user'`, `security_class='authentication'`, `event_type='SUCCESS'`
- Failed password: `operation='PASSWORD_FAIL'`, `entity_type='user'`, `security_class='authentication'`, `event_type='FAILURE'`
- Permission change: `operation='UPDATE'`, `entity_type='user'`, `security_class='authorization'`
- File access: `operation='ACCESS'`, `entity_type='file'`, `security_class='authorization'`
---
### 2.5 Table: error_audit_log
```sql
CREATE TABLE error_audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- WHAT: Error details
operation VARCHAR(50) NOT NULL, -- 'ERROR', 'WARNING', 'CRITICAL'
entity_type VARCHAR(50) NOT NULL, -- 'instrument', 'integration', 'database', 'validation'
entity_id VARCHAR(36) NOT NULL, -- Entity where error occurred
error_code VARCHAR(50), -- Specific error code
error_message TEXT, -- Error message
error_details JSON, -- Stack trace, context
previous_value JSON, -- State before error
new_value JSON, -- State after error (if recovered)
-- HOW: Error context
mechanism ENUM('MANUAL', 'AUTOMATIC') NOT NULL DEFAULT 'MANUAL',
application_id VARCHAR(50),
web_page VARCHAR(500),
session_id VARCHAR(100),
event_type VARCHAR(100), -- 'TRANSACTION_ERROR', 'SYSTEM_ERROR', 'VALIDATION_ERROR'
-- WHERE: Error location
site_id VARCHAR(36),
workstation_id VARCHAR(36),
pc_name VARCHAR(100),
ip_address VARCHAR(45),
-- WHO: User or system
user_id VARCHAR(36) NOT NULL, -- User ID or 'SYSTEM'
-- WHEN: Timestamp
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- WHY: Error context
reason TEXT, -- Why the error occurred
-- Context: Additional error data
context JSON, -- Related IDs, transaction info, etc.
-- Indexes
INDEX idx_operation_created (operation, created_at),
INDEX idx_entity (entity_type, entity_id, created_at),
INDEX idx_error_code (error_code, created_at),
INDEX idx_event_type (event_type, created_at),
INDEX idx_user_created (user_id, created_at),
INDEX idx_site (site_id, created_at),
INDEX idx_created (created_at)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202601 VALUES LESS THAN (202602),
PARTITION p202602 VALUES LESS THAN (202603),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
```
**Error Log Examples:**
- Transaction error: `operation='ERROR'`, `entity_type='database'`, `event_type='TRANSACTION_ERROR'`
- Instrument error: `operation='ERROR'`, `entity_type='instrument'`, `event_type='SYSTEM_ERROR'`
- Integration error: `operation='ERROR'`, `entity_type='integration'`, `event_type='SYSTEM_ERROR'`
- Validation error: `operation='ERROR'`, `entity_type='validation'`, `event_type='VALIDATION_ERROR'`
---
## 3. Example Audit Entries
### 3.1 Data Log Entry (Patient Update)
```json
{
"id": 15243,
"operation": "UPDATE",
"entity_type": "patient",
"entity_id": "PAT-2026-001234",
"table_name": "patients",
"field_name": null,
"previous_value": {
"NameFirst": "John",
"NameLast": "Doe",
"Phone": "+1-555-0100"
},
"new_value": {
"NameFirst": "Johnny",
"NameLast": "Doe-Smith",
"Phone": "+1-555-0199"
},
"mechanism": "MANUAL",
"application_id": "CLQMS-WEB",
"web_page": "/api/patient/PAT-2026-001234",
"session_id": "sess_abc123",
"event_type": "PATIENT_UPDATE",
"site_id": "SITE-001",
"workstation_id": "WS-001",
"pc_name": "LAB-PC-01",
"ip_address": "192.168.1.100",
"user_id": "USR-001",
"created_at": "2026-02-19T14:30:00Z",
"reason": "Patient requested name change after marriage",
"context": {
"changed_fields": ["NameFirst", "NameLast", "Phone"],
"validation_status": "PASSED"
}
}
```
### 3.2 Service Log Entry (Instrument Communication)
```json
{
"id": 89345,
"operation": "COMMUNICATION",
"entity_type": "instrument",
"entity_id": "INST-001",
"service_class": "communication",
"resource_type": "instrument_communication",
"resource_details": {
"protocol": "HL7",
"port": 2575,
"direction": "INBOUND"
},
"previous_value": { "status": "IDLE" },
"new_value": { "status": "RECEIVING" },
"mechanism": "AUTOMATIC",
"application_id": "INSTRUMENT-SERVICE",
"service_name": "instrument-listener",
"session_id": "svc_inst_001",
"event_type": "RESULT_RECEIVED",
"site_id": "SITE-001",
"workstation_id": "WS-LAB-01",
"pc_name": "LAB-SERVER-01",
"ip_address": "192.168.1.10",
"port": 2575,
"user_id": "SYSTEM",
"created_at": "2026-02-19T14:35:22Z",
"reason": null,
"context": {
"sample_id": "SMP-2026-004567",
"test_count": 5,
"bytes_received": 2048
}
}
```
### 3.3 Security Log Entry (Failed Login)
```json
{
"id": 4521,
"operation": "PASSWORD_FAIL",
"entity_type": "user",
"entity_id": "USR-999",
"security_class": "authentication",
"resource_path": "/api/auth/login",
"previous_value": { "failed_attempts": 2 },
"new_value": { "failed_attempts": 3 },
"mechanism": "MANUAL",
"application_id": "CLQMS-WEB",
"web_page": "/login",
"session_id": "sess_fail_789",
"event_type": "FAILURE",
"site_id": "SITE-002",
"workstation_id": "WS-RECEPTION",
"pc_name": "RECEPTION-PC-02",
"ip_address": "203.0.113.45",
"user_id": "USR-999",
"created_at": "2026-02-19T15:10:05Z",
"reason": null,
"context": {
"lockout_threshold": 5,
"remaining_attempts": 2,
"username_attempted": "john.doe"
}
}
```
### 3.4 Error Log Entry (Database Transaction Failure)
```json
{
"id": 1203,
"operation": "ERROR",
"entity_type": "database",
"entity_id": "DB-PRIMARY",
"error_code": "DB_TXN_001",
"error_message": "Transaction rollback due to deadlock",
"error_details": {
"sql_state": "40001",
"error_number": 1213,
"deadlock_victim": true
},
"previous_value": { "transaction_status": "ACTIVE" },
"new_value": { "transaction_status": "ROLLED_BACK" },
"mechanism": "AUTOMATIC",
"application_id": "CLQMS-WEB",
"web_page": "/api/orders/batch-update",
"session_id": "sess_xyz789",
"event_type": "TRANSACTION_ERROR",
"site_id": "SITE-001",
"workstation_id": "WS-001",
"pc_name": "LAB-PC-01",
"ip_address": "192.168.1.100",
"user_id": "USR-001",
"created_at": "2026-02-19T15:15:30Z",
"reason": "Deadlock detected during batch update",
"context": {
"affected_tables": ["orders", "order_tests"],
"retry_count": 0,
"transaction_id": "txn_20260219151530"
}
}
```
---
## 4. Implementation Strategy
### 4.1 Central Audit Service
```php
<?php
namespace App\Services;
class AuditService
{
/**
* Log a DATA audit event
*/
public static function logData(
string $operation,
string $entityType,
string $entityId,
?string $tableName = null,
?string $fieldName = null,
?array $previousValue = null,
?array $newValue = null,
?string $reason = null,
?array $context = null
): void {
self::log('data_audit_log', [
'operation' => $operation,
'entity_type' => $entityType,
'entity_id' => $entityId,
'table_name' => $tableName,
'field_name' => $fieldName,
'previous_value' => $previousValue ? json_encode($previousValue) : null,
'new_value' => $newValue ? json_encode($newValue) : null,
'mechanism' => 'MANUAL',
'application_id' => 'CLQMS-WEB',
'web_page' => $_SERVER['REQUEST_URI'] ?? null,
'session_id' => session_id(),
'event_type' => strtoupper($entityType) . '_' . strtoupper($operation),
'site_id' => session('site_id'),
'workstation_id' => session('workstation_id'),
'pc_name' => gethostname(),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_id' => auth()->id() ?? 'SYSTEM',
'reason' => $reason,
'context' => $context ? json_encode($context) : null,
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* Log a SERVICE audit event
*/
public static function logService(
string $operation,
string $entityType,
string $entityId,
string $serviceClass,
?string $resourceType = null,
?array $resourceDetails = null,
?array $previousValue = null,
?array $newValue = null,
?string $serviceName = null,
?array $context = null
): void {
self::log('service_audit_log', [
'operation' => $operation,
'entity_type' => $entityType,
'entity_id' => $entityId,
'service_class' => $serviceClass,
'resource_type' => $resourceType,
'resource_details' => $resourceDetails ? json_encode($resourceDetails) : null,
'previous_value' => $previousValue ? json_encode($previousValue) : null,
'new_value' => $newValue ? json_encode($newValue) : null,
'mechanism' => 'AUTOMATIC',
'application_id' => $serviceName ?? 'SYSTEM-SERVICE',
'service_name' => $serviceName,
'session_id' => session_id() ?: 'service_session',
'event_type' => strtoupper($serviceClass) . '_' . strtoupper($operation),
'site_id' => session('site_id'),
'workstation_id' => session('workstation_id'),
'pc_name' => gethostname(),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
'port' => $resourceDetails['port'] ?? null,
'user_id' => 'SYSTEM',
'reason' => null,
'context' => $context ? json_encode($context) : null,
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* Log a SECURITY audit event
*/
public static function logSecurity(
string $operation,
string $entityType,
string $entityId,
string $securityClass,
?string $eventType = 'SUCCESS',
?string $resourcePath = null,
?array $previousValue = null,
?array $newValue = null,
?string $reason = null,
?array $context = null
): void {
self::log('security_audit_log', [
'operation' => $operation,
'entity_type' => $entityType,
'entity_id' => $entityId,
'security_class' => $securityClass,
'resource_path' => $resourcePath,
'previous_value' => $previousValue ? json_encode($previousValue) : null,
'new_value' => $newValue ? json_encode($newValue) : null,
'mechanism' => 'MANUAL',
'application_id' => 'CLQMS-WEB',
'web_page' => $_SERVER['REQUEST_URI'] ?? null,
'session_id' => session_id(),
'event_type' => $eventType,
'site_id' => session('site_id'),
'workstation_id' => session('workstation_id'),
'pc_name' => gethostname(),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_id' => auth()->id() ?? 'UNKNOWN',
'reason' => $reason,
'context' => $context ? json_encode($context) : null,
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* Log an ERROR audit event
*/
public static function logError(
string $entityType,
string $entityId,
string $errorCode,
string $errorMessage,
string $eventType,
?array $errorDetails = null,
?array $previousValue = null,
?array $newValue = null,
?string $reason = null,
?array $context = null
): void {
self::log('error_audit_log', [
'operation' => 'ERROR',
'entity_type' => $entityType,
'entity_id' => $entityId,
'error_code' => $errorCode,
'error_message' => $errorMessage,
'error_details' => $errorDetails ? json_encode($errorDetails) : null,
'previous_value' => $previousValue ? json_encode($previousValue) : null,
'new_value' => $newValue ? json_encode($newValue) : null,
'mechanism' => 'AUTOMATIC',
'application_id' => 'CLQMS-WEB',
'web_page' => $_SERVER['REQUEST_URI'] ?? null,
'session_id' => session_id() ?: 'system',
'event_type' => $eventType,
'site_id' => session('site_id'),
'workstation_id' => session('workstation_id'),
'pc_name' => gethostname(),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_id' => auth()->id() ?? 'SYSTEM',
'reason' => $reason,
'context' => $context ? json_encode($context) : null,
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* Generic log method with async support
*/
private static function log(string $table, array $data): void
{
// For high-volume operations, dispatch to queue
if (in_array($table, ['service_audit_log', 'error_audit_log'])) {
self::dispatchAuditJob($table, $data);
} else {
// Direct insert for data and security logs
\Config\Database::connect()->table($table)->insert($data);
}
}
private static function dispatchAuditJob(string $table, array $data): void
{
// Implementation: Queue the audit entry for async processing
// This prevents blocking user operations during high-volume logging
}
}
```
---
## 5. Query Patterns
### 5.1 Common Audit Queries
```sql
-- View patient history (DATA log)
SELECT * FROM data_audit_log
WHERE entity_type = 'patient'
AND entity_id = 'PAT-2026-001234'
ORDER BY created_at DESC;
-- User activity report
SELECT operation, entity_type, COUNT(*) as count
FROM data_audit_log
WHERE user_id = 'USR-001'
AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY operation, entity_type;
-- Instrument communication history (SERVICE log)
SELECT * FROM service_audit_log
WHERE entity_type = 'instrument'
AND entity_id = 'INST-001'
AND operation = 'COMMUNICATION'
ORDER BY created_at DESC;
-- Failed login attempts (SECURITY log)
SELECT * FROM security_audit_log
WHERE operation IN ('PASSWORD_FAIL', 'ACCESS_DENIED')
AND event_type = 'FAILURE'
AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY created_at DESC;
-- Recent errors (ERROR log)
SELECT * FROM error_audit_log
WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)
AND event_type = 'CRITICAL'
ORDER BY created_at DESC;
-- Find changes to specific field
SELECT * FROM data_audit_log
WHERE table_name = 'patients'
AND field_name = 'Phone'
AND entity_id = 'PAT-2026-001234'
ORDER BY created_at DESC;
```
---
## 6. Migration Plan
### Phase 1: Foundation (Week 1)
- [ ] Drop existing unused tables (patreglog, patvisitlog, specimenlog)
- [ ] Create 4 new audit tables with partitioning
- [ ] Create AuditService class
- [ ] Add database indexes
### Phase 2: Core Implementation (Week 2)
- [ ] Integrate data_audit_log into Patient model
- [ ] Integrate data_audit_log into Order/Test models
- [ ] Integrate data_audit_log into Master data models
- [ ] Integrate security_audit_log into authentication
### Phase 3: Service & Error Logging (Week 3)
- [ ] Implement service_audit_log for instrument communication
- [ ] Implement service_audit_log for printing/messaging
- [ ] Implement error_audit_log for database errors
- [ ] Implement error_audit_log for instrument errors
- [ ] Implement error_audit_log for integration errors
### Phase 4: API & Optimization (Week 4)
- [ ] Create unified API endpoint for querying all log types
- [ ] Add filters by log_type, date range, user, entity
- [ ] Implement async logging queue
- [ ] Add log export functionality (CSV/PDF)
---
## 7. Retention Strategy (TBD)
| Table | Proposed Retention | Notes |
|-------|-------------------|-------|
| `data_audit_log` | 7 years | Patient data compliance |
| `service_audit_log` | 2 years | High volume, operational only |
| `security_audit_log` | Permanent | Compliance and forensics |
| `error_audit_log` | 5 years | Debugging and incident analysis |
---
## 8. Key Design Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| **Table Count** | 4 tables | Separates by log type, different retention needs |
| **5W1H** | All 6 dimensions captured | Complete audit trail per section 4.2.1.20 |
| **Mechanism** | MANUAL vs AUTOMATIC | Distinguishes user vs instrument operations |
| **User for AUTO** | 'SYSTEM' | Clear identification of automatic operations |
| **JSON Storage** | previous_value, new_value, context | Flexible schema evolution |
| **Partitioning** | Monthly for high-volume tables | Manage service and error log growth |
| **Async Logging** | Yes for service/error logs | Don't block user operations |
---
*Document Version: 2.0*
*Based on: Section 4.2.1.20 Error Management*
*Date: February 20, 2026*

File diff suppressed because it is too large Load Diff

View File

@ -1,640 +0,0 @@
// CLQMS Database Schema
// Generated from app/Models/ directory
// Database Markup Language (DBML) for dbdiagram.io and other tools
// ============================================
// TABLE 1: Patient Management
// ============================================
Table patient {
InternalPID int [pk, increment]
PatientID varchar(255)
AlternatePID varchar(255)
Prefix varchar(50)
NameFirst varchar(255)
NameMiddle varchar(255)
NameMaiden varchar(255)
NameLast varchar(255)
Suffix varchar(50)
NameAlias varchar(255)
Sex varchar(10)
Birthdate datetime
PlaceOfBirth varchar(255)
Street_1 varchar(255)
Street_2 varchar(255)
Street_3 varchar(255)
City varchar(100)
Province varchar(100)
ZIP varchar(20)
Country varchar(100)
EmailAddress1 varchar(255)
EmailAddress2 varchar(255)
Phone varchar(50)
MobilePhone varchar(50)
AccountNumber varchar(100)
Race varchar(50)
MaritalStatus varchar(50)
Religion varchar(50)
Ethnic varchar(50)
Citizenship varchar(100)
DeathIndicator boolean
TimeOfDeath datetime
Custodian int [ref: > patient.InternalPID]
LinkTo int [ref: > patient.InternalPID]
CreateDate datetime [not null]
DelDate datetime
}
Table patidt {
PatIdtID int [pk, increment]
InternalPID int [not null, ref: > patient.InternalPID]
IdentifierType varchar(100)
Identifier varchar(255)
EffectiveDate datetime
ExpirationDate datetime
CreateDate datetime [not null]
DelDate datetime
}
Table patcom {
PatComID int [pk, increment]
InternalPID int [not null, ref: > patient.InternalPID]
Comment text
CreateDate datetime [not null]
EndDate datetime
}
Table patatt {
PatAttID int [pk, increment]
InternalPID int [not null, ref: > patient.InternalPID]
UserID int
Address text
CreateDate datetime [not null]
DelDate datetime
}
// ============================================
// TABLE 2: Visit Management
// ============================================
Table patvisit {
InternalPVID int [pk, increment]
InternalPID int [not null, ref: > patient.InternalPID]
EpisodeID int
PVID varchar(100)
CreateDate datetime [not null]
EndDate datetime
ArchivedDate datetime
DelDate datetime
}
Table patvisitadt {
PVADTID int [pk, increment]
InternalPVID int [not null, ref: > patvisit.InternalPVID]
LocationID int [ref: > location.LocationID]
ADTCode varchar(50)
AttDoc varchar(255)
RefDoc varchar(255)
AdmDoc varchar(255)
CnsDoc varchar(255)
CreateDate datetime [not null]
EndDate datetime
ArchivedDate datetime
DelDate datetime
}
Table patdiag {
InternalPVID int [pk, increment]
InternalPVID int [not null, ref: > patvisit.InternalPVID]
InternalPID int [not null, ref: > patient.InternalPID]
DiagCode varchar(50)
Diagnosis varchar(255)
CreateDate datetime [not null]
EndDate datetime
ArchivedDate datetime
DelDate datetime
}
// ============================================
// TABLE 3: Organization Structure
// ============================================
Table account {
AccountID int [pk, increment]
AccountName varchar(255)
Initial varchar(50)
Street_1 varchar(255)
Street_2 varchar(255)
Street_3 varchar(255)
City varchar(100)
Province varchar(100)
ZIP varchar(20)
Country varchar(100)
AreaCode varchar(20)
EmailAddress1 varchar(255)
EmailAddress2 varchar(255)
Phone varchar(50)
Fax varchar(50)
Parent int [ref: > account.AccountID]
CreateDate datetime [not null]
EndDate datetime
}
Table site {
SiteID int [pk, increment]
AccountID int [not null, ref: > account.AccountID]
Parent int [ref: > site.SiteID]
SiteTypeID int
SiteClassID int
SiteCode varchar(50)
SiteName varchar(255)
ME varchar(50)
CreateDate datetime [not null]
EndDate datetime
}
Table department {
DepartmentID int [pk, increment]
DisciplineID int [ref: > discipline.DisciplineID]
SiteID int [ref: > site.SiteID]
DepartmentCode varchar(50)
DepartmentName varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
Table discipline {
DisciplineID int [pk, increment]
SiteID int [ref: > site.SiteID]
Parent int [ref: > discipline.DisciplineID]
DisciplineCode varchar(50)
DisciplineName varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
Table workstation {
WorkstationID int [pk, increment]
DepartmentID int [ref: > department.DepartmentID]
LinkTo int [ref: > workstation.WorkstationID]
EquipmentID int
WorkstationCode varchar(50)
WorkstationName varchar(255)
Type varchar(50)
Enable boolean
CreateDate datetime [not null]
EndDate datetime
}
// ============================================
// TABLE 4: Location Management
// ============================================
Table location {
LocationID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
Parent int [ref: > location.LocationID]
LocCode varchar(50)
LocFull varchar(255)
Description text
LocType varchar(50)
CreateDate datetime [not null]
EndDate datetime
}
Table locationaddress {
LocationID int [pk, ref: > location.LocationID]
Province int [ref: > areageo.AreaGeoID]
City int [ref: > areageo.AreaGeoID]
Street1 varchar(255)
Street2 varchar(255)
PostCode varchar(20)
GeoLocationSystem varchar(50)
GeoLocationData text
Phone varchar(50)
Email varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
Table areageo {
AreaGeoID int [pk, increment]
Parent int [ref: > areageo.AreaGeoID]
AreaCode varchar(50)
Class varchar(50)
AreaName varchar(255)
}
// ============================================
// TABLE 5: Test Management
// ============================================
Table testdefsite {
TestSiteID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
TestSiteCode varchar(50)
TestSiteName varchar(255)
TestType varchar(50) // TEST, PARAM, CALC, GROUP, TITLE
Description text
SeqScr int
SeqRpt int
IndentLeft int
FontStyle varchar(50)
VisibleScr boolean
VisibleRpt boolean
CountStat boolean
CreateDate datetime [not null]
StartDate datetime
EndDate datetime
}
Table testdeftech {
TestTechID int [pk, increment]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
DisciplineID int [ref: > discipline.DisciplineID]
DepartmentID int [ref: > department.DepartmentID]
VSet int
ResultType varchar(50) // NM, TX, DT, TM, VS, HL7
RefType varchar(50) // NUM, TXT, VSET
ReqQty decimal(10,4)
ReqQtyUnit varchar(20)
Unit1 varchar(50)
Factor decimal(10,6)
Unit2 varchar(50)
Decimal int
CollReq text
Method varchar(255)
ExpectedTAT int
CreateDate datetime [not null]
EndDate datetime
}
Table testdefcal {
TestCalID int [pk, increment]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
DisciplineID int [ref: > discipline.DisciplineID]
DepartmentID int [ref: > department.DepartmentID]
FormulaInput varchar(500)
FormulaCode text
RefType varchar(50)
Unit1 varchar(50)
Factor decimal(10,6)
Unit2 varchar(50)
Decimal int
Method varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
Table testdefgrp {
TestGrpID int [pk, increment]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
Member int [ref: > testdefsite.TestSiteID]
CreateDate datetime [not null]
EndDate datetime
}
Table testmap {
TestMapID int [pk, increment]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
ConDefID int [ref: > containerdef.ConDefID]
HostType varchar(50)
HostID varchar(100)
HostDataSource varchar(100)
HostTestCode varchar(100)
HostTestName varchar(255)
ClientType varchar(50)
ClientID varchar(100)
ClientDataSource varchar(100)
ClientTestCode varchar(100)
ClientTestName varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
// ============================================
// TABLE 6: Reference Ranges
// ============================================
Table refnum {
RefNumID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
SpcType varchar(50)
Sex varchar(10)
Criteria varchar(255)
AgeStart int
AgeEnd int
NumRefType varchar(50) // NR, CR
RangeType varchar(50) // LL-UL, LL, UL, ABS
LowSign varchar(5)
Low decimal(15,5)
HighSign varchar(5)
High decimal(15,5)
Display varchar(50)
Flag varchar(10)
Interpretation text
Notes text
CreateDate datetime [not null]
StartDate datetime
EndDate datetime
}
Table reftxt {
RefTxtID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
SpcType varchar(50)
Sex varchar(10)
Criteria varchar(255)
AgeStart int
AgeEnd int
TxtRefType varchar(50) // TX, VS
RefTxt text
Flag varchar(10)
Notes text
CreateDate datetime [not null]
StartDate datetime
EndDate datetime
}
Table refvset {
RefVSetID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
SpcType varchar(50)
Sex varchar(10)
AgeStart int
AgeEnd int
RefTxt varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
Table refthold {
RefTHoldID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
SpcType varchar(50)
Sex varchar(10)
AgeStart int
AgeEnd int
Threshold decimal(15,5)
BelowTxt text
AboveTxt text
GrayzoneLow decimal(15,5)
GrayzoneHigh decimal(15,5)
GrayzoneTxt text
CreateDate datetime [not null]
EndDate datetime
}
// ============================================
// TABLE 7: Specimen Management
// ============================================
Table specimen {
InternalSID int [pk, increment]
SID varchar(17) [not null, unique]
SiteID int [not null, ref: > site.SiteID]
OrderID varchar(13)
ConDefID int [ref: > containerdef.ConDefID]
Parent int [ref: > specimen.InternalSID]
Qty decimal(10,4)
Unit varchar(20)
GenerateBy varchar(100)
SchDateTime datetime
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
}
Table specimenstatus {
SpcStaID int [pk, increment]
SID varchar(17) [not null, ref: > specimen.SID]
OrderID varchar(13)
CurrSiteID int [ref: > site.SiteID]
CurrLocID int [ref: > location.LocationID]
UserID int
SpcAct varchar(50)
ActRes varchar(50)
SpcStatus varchar(50)
Qty decimal(10,4)
Unit varchar(20)
SpcCon varchar(50)
Comment text
Origin varchar(100)
GeoLocationSystem varchar(50)
GeoLocationData text
DIDType varchar(50)
DID varchar(100)
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
}
Table specimencollection {
SpcColID int [pk, increment]
SpcStaID int [not null, ref: > specimenstatus.SpcStaID]
SpRole varchar(50)
ColMethod varchar(50)
BodySite varchar(50)
CntSize varchar(50)
FastingVolume decimal(10,4)
ColStart datetime
ColEnd datetime
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
}
Table specimenprep {
SpcPrpID int [pk, increment]
SpcStaID int [not null, ref: > specimenstatus.SpcStaID]
Description text
Method varchar(255)
Additive varchar(100)
AddQty decimal(10,4)
AddUnit varchar(20)
PrepStart datetime
PrepEnd datetime
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
}
Table containerdef {
ConDefID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
ConCode varchar(50)
ConName varchar(255)
ConDesc text
Additive varchar(100)
ConClass varchar(50)
Color varchar(50)
CreateDate datetime [not null]
EndDate datetime
}
// ============================================
// TABLE 8: Order Management
// ============================================
Table ordertest {
InternalOID int [pk, increment]
OrderID varchar(13) [not null, unique]
PlacerID varchar(100)
InternalPID int [not null, ref: > patient.InternalPID]
SiteID int [ref: > site.SiteID]
PVADTID int [ref: > patvisitadt.PVADTID]
ReqApp varchar(100)
Priority varchar(50)
TrnDate datetime
EffDate datetime
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
DelDate datetime
}
Table patres {
ResultID int [pk, increment]
SiteID int [ref: > site.SiteID]
OrderID varchar(13)
InternalSID int [ref: > specimen.InternalSID]
SID varchar(17)
SampleID varchar(100)
TestSiteID int [ref: > testdefsite.TestSiteID]
WorkstationID int [ref: > workstation.WorkstationID]
EquipmentID int
RefNumID int [ref: > refnum.RefNumID]
RefTxtID int [ref: > reftxt.RefTxtID]
TestSiteCode varchar(50)
AspCnt int
Result text
SampleType varchar(50)
ResultDateTime datetime
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
DelDate datetime
}
// ============================================
// TABLE 9: Contact Management
// ============================================
Table contact {
ContactID int [pk, increment]
NameFirst varchar(255)
NameLast varchar(255)
Title varchar(100)
Initial varchar(50)
Birthdate datetime
EmailAddress1 varchar(255)
EmailAddress2 varchar(255)
Phone varchar(50)
MobilePhone1 varchar(50)
MobilePhone2 varchar(50)
Specialty varchar(100)
SubSpecialty varchar(100)
CreateDate datetime [not null]
EndDate datetime
}
Table contactdetail {
ContactDetID int [pk, increment]
ContactID int [not null, ref: > contact.ContactID]
SiteID int [ref: > site.SiteID]
OccupationID int [ref: > occupation.OccupationID]
ContactCode varchar(50)
ContactEmail varchar(255)
JobTitle varchar(100)
Department varchar(100)
ContactStartDate datetime
ContactEndDate datetime
}
Table medicalspecialty {
SpecialtyID int [pk, increment]
Parent int [ref: > medicalspecialty.SpecialtyID]
SpecialtyText varchar(255)
Title varchar(100)
CreateDate datetime [not null]
EndDate datetime
}
Table occupation {
OccupationID int [pk, increment]
OccCode varchar(50)
OccText varchar(255)
Description text
CreateDate datetime [not null]
}
// ============================================
// TABLE 10: Value Sets
// ============================================
Table valuesetdef {
VSetID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
VSName varchar(255)
VSDesc text
CreateDate datetime [not null]
EndDate datetime
}
Table valueset {
VID int [pk, increment]
SiteID int [not null, ref: > site.SiteID]
VSetID int [not null, ref: > valuesetdef.VSetID]
VCategory varchar(100)
VOrder int
VValue varchar(255)
VDesc varchar(255)
CreateDate datetime [not null]
EndDate datetime
}
// ============================================
// TABLE 11: System / Counter
// ============================================
Table counter {
CounterID int [pk, increment]
CounterValue bigint
CounterStart bigint
CounterEnd bigint
CounterReset varchar(50)
CreateDate datetime [not null]
EndDate datetime
}
Table edgeres {
EdgeResID int [pk, increment]
SiteID int [ref: > site.SiteID]
InstrumentID int
SampleID varchar(100)
PatientID varchar(100)
Payload text
Status varchar(50)
AutoProcess boolean
ProcessedAt datetime
ErrorMessage text
CreateDate datetime [not null]
EndDate datetime
ArchiveDate datetime
DelDate datetime
}
Table zones {
id int [pk, increment]
name varchar(255)
code varchar(50)
type varchar(50)
description text
status varchar(50)
created_at datetime
updated_at datetime
}

View File

@ -1,941 +0,0 @@
# Manual Result Entry Implementation Plan
## Overview
This document outlines the implementation plan for manual laboratory result entry functionality in CLQMS. The system already creates empty `patres` records when orders are placed. This plan covers the complete workflow for entering, validating, and verifying test results.
**Current State:** Empty `patres` records exist for all ordered tests
**Target State:** Full result entry with reference range validation, abnormal flag calculation, and verification workflow
---
## Phase 1: Core Result Management (Priority: HIGH)
### 1.1 Extend PatResultModel
**File:** `app/Models/PatResultModel.php`
#### New Methods to Add:
```php
/**
* Get results with filtering and pagination
*
* @param array $filters Available filters:
* - InternalPID: int - Filter by patient
* - OrderID: string - Filter by order
* - ResultStatus: string - PEN, PRE, FIN, AMD
* - TestSiteID: int - Filter by test
* - date_from: string - YYYY-MM-DD
* - date_to: string - YYYY-MM-DD
* - WorkstationID: int - Filter by workstation
* @param int $page
* @param int $perPage
* @return array
*/
public function getResults(array $filters = [], int $page = 1, int $perPage = 20): array
/**
* Get single result with full details
* Includes: patient demographics, test info, specimen info, reference ranges
*
* @param int $resultID
* @return array|null
*/
public function getResultWithDetails(int $resultID): ?array
/**
* Update result with validation
*
* @param int $resultID
* @param array $data
* - Result: string - The result value
* - Unit: string - Unit of measurement (optional)
* - AbnormalFlag: string - H, L, N, A, C (optional, auto-calculated)
* - Comment: string - Result comment (optional)
* - ResultStatus: string - Status update (optional)
* @return bool
*/
public function updateResult(int $resultID, array $data): bool
/**
* Get pending results for a workstation (worklist)
*
* @param int $workstationID
* @param array $filters Additional filters
* @return array
*/
public function getPendingByWorkstation(int $workstationID, array $filters = []): array
/**
* Get all results for an order
*
* @param string $orderID
* @return array
*/
public function getByOrder(string $orderID): array
/**
* Verify a result
*
* @param int $resultID
* @param int $userID - ID of verifying user
* @param string|null $comment - Optional verification comment
* @return bool
*/
public function verifyResult(int $resultID, int $userID, ?string $comment = null): bool
/**
* Unverify a result (amendment)
*
* @param int $resultID
* @param int $userID - ID of user amending
* @param string $reason - Required reason for amendment
* @return bool
*/
public function unverifyResult(int $resultID, int $userID, string $reason): bool
```
#### Fields to Add to `$allowedFields`:
```php
protected $allowedFields = [
'SiteID',
'OrderID',
'InternalSID',
'SID',
'SampleID',
'TestSiteID',
'TestSiteCode',
'AspCnt',
'Result',
'Unit', // NEW
'SampleType',
'ResultDateTime',
'WorkstationID',
'EquipmentID',
'RefNumID',
'RefTxtID',
'ResultStatus', // NEW: PEN, PRE, FIN, AMD
'Verified', // NEW: boolean
'VerifiedBy', // NEW: user ID
'VerifiedDate', // NEW: datetime
'EnteredBy', // NEW: user ID
'AbnormalFlag', // NEW: H, L, N, A, C
'Comment', // NEW
'CreateDate',
'EndDate',
'ArchiveDate',
'DelDate'
];
```
---
### 1.2 Create ResultEntryService
**File:** `app/Libraries/ResultEntryService.php`
This service handles all business logic for result entry.
```php
<?php
namespace App\Libraries;
use App\Models\PatResultModel;
use App\Models\Patient\PatientModel;
use App\Models\RefRange\RefNumModel;
use App\Models\RefRange\RefTxtModel;
use App\Models\Test\TestDefSiteModel;
class ResultEntryService
{
/**
* Validate result value based on test type
*
* @param string $value
* @param int $testSiteID
* @return array ['valid' => bool, 'error' => string|null]
*/
public function validateResult(string $value, int $testSiteID): array
/**
* Find applicable reference range
*
* @param int $testSiteID
* @param array $patient Demographics: age (months), sex, specimenType
* @return array|null Reference range data
*/
public function getApplicableRange(int $testSiteID, array $patient): ?array
/**
* Calculate abnormal flag based on value and range
*
* @param string|float $value
* @param array $range Reference range data
* @return string H, L, N, A, or C
*/
public function calculateAbnormalFlag($value, array $range): string
/**
* Format reference range for display
*
* @param array $range
* @return string Human-readable range (e.g., "10.0 - 20.0 mg/dL")
*/
public function formatDisplayRange(array $range): string
/**
* Check delta (compare with previous result)
*
* @param int $resultID Current result being edited
* @param string|float $newValue
* @return array ['hasPrevious' => bool, 'previousValue' => string|null, 'deltaPercent' => float|null, 'significant' => bool]
*/
public function checkDelta(int $resultID, $newValue): array
/**
* Process result entry
*
* @param int $resultID
* @param array $data
* @param int $userID User entering the result
* @return array ['success' => bool, 'result' => array|null, 'errors' => array]
*/
public function processEntry(int $resultID, array $data, int $userID): array
/**
* Update calculated tests after dependency changes
*
* @param string $orderID
* @param int $userID
* @return int Number of calculated results updated
*/
public function recalculateDependentResults(string $orderID, int $userID): int
/**
* Get worklist for workstation
*
* @param int $workstationID
* @param array $filters
* @return array
*/
public function getWorklist(int $workstationID, array $filters = []): array
}
```
---
### 1.3 Implement ResultController
**File:** `app/Controllers/ResultController.php`
Replace the placeholder controller with full implementation:
```php
<?php
namespace App\Controllers;
use App\Traits\ResponseTrait;
use App\Libraries\ResultEntryService;
use App\Models\PatResultModel;
use CodeIgniter\Controller;
class ResultController extends Controller
{
use ResponseTrait;
protected $resultModel;
protected $entryService;
public function __construct()
{
$this->resultModel = new PatResultModel();
$this->entryService = new ResultEntryService();
}
/**
* GET /api/results
* List results with filtering
*/
public function index()
/**
* GET /api/results/{id}
* Get single result with details
*/
public function show($id = null)
/**
* PATCH /api/results/{id}
* Update result value
*/
public function update($id = null)
/**
* POST /api/results/batch
* Batch update multiple results
*/
public function batchUpdate()
/**
* POST /api/results/{id}/verify
* Verify a result
*/
public function verify($id = null)
/**
* POST /api/results/{id}/unverify
* Unverify/amend a result
*/
public function unverify($id = null)
/**
* GET /api/results/worklist
* Get pending results for workstation
*/
public function worklist()
/**
* GET /api/results/order/{orderID}
* Get all results for an order
*/
public function byOrder($orderID = null)
}
```
---
## Phase 2: Database Schema (Priority: HIGH)
### 2.1 Migration for New Fields
**File:** `app/Database/Migrations/2025-03-04-000001_AddResultFields.php`
```php
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddResultFields extends Migration
{
public function up()
{
$this->forge->addColumn('patres', [
'ResultStatus' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => true,
'comment' => 'PEN=Pending, PRE=Preliminary, FIN=Final, AMD=Amended',
'after' => 'RefTxtID'
],
'Verified' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'after' => 'ResultStatus'
],
'VerifiedBy' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'Verified'
],
'VerifiedDate' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'VerifiedBy'
],
'EnteredBy' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'VerifiedDate'
],
'AbnormalFlag' => [
'type' => 'VARCHAR',
'constraint' => 1,
'null' => true,
'comment' => 'H=High, L=Low, N=Normal, A=Abnormal, C=Critical',
'after' => 'EnteredBy'
],
'Comment' => [
'type' => 'TEXT',
'null' => true,
'after' => 'AbnormalFlag'
],
'Unit' => [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
'after' => 'Result'
]
]);
}
public function down()
{
$this->forge->dropColumn('patres', [
'ResultStatus',
'Verified',
'VerifiedBy',
'VerifiedDate',
'EnteredBy',
'AbnormalFlag',
'Comment',
'Unit'
]);
}
}
```
### 2.2 Create Result History Table (Audit Trail)
**File:** `app/Database/Migrations/2025-03-04-000002_CreatePatResHistory.php`
```php
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePatResHistory extends Migration
{
public function up()
{
$this->forge->addField([
'HistoryID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true
],
'ResultID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'OrderID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'TestSiteID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'OldResult' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true
],
'NewResult' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true
],
'OldStatus' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => true
],
'NewStatus' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => true
],
'ChangedBy' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'ChangeReason' => [
'type' => 'TEXT',
'null' => true
],
'CreateDate' => [
'type' => 'DATETIME',
'null' => false
]
]);
$this->forge->addKey('HistoryID', true);
$this->forge->addKey('ResultID');
$this->forge->addKey('CreateDate');
$this->forge->createTable('patreshistory');
}
public function down()
{
$this->forge->dropTable('patreshistory');
}
}
```
---
## Phase 3: API Routes (Priority: HIGH)
### 3.1 Update Routes.php
Add to `app/Config/Routes.php` within the existing `api` group:
```php
// Results
$routes->group('results', function ($routes) {
$routes->get('/', 'ResultController::index');
$routes->get('worklist', 'ResultController::worklist');
$routes->get('order/(:any)', 'ResultController::byOrder/$1');
$routes->get('(:num)', 'ResultController::show/$1');
$routes->patch('(:num)', 'ResultController::update/$1');
$routes->post('batch', 'ResultController::batchUpdate');
$routes->post('(:num)/verify', 'ResultController::verify/$1');
$routes->post('(:num)/unverify', 'ResultController::unverify/$1');
});
```
---
## Phase 4: API Documentation (Priority: MEDIUM)
### 4.1 Create Results Schema
**File:** `public/components/schemas/results.yaml`
```yaml
Result:
type: object
properties:
ResultID:
type: integer
OrderID:
type: integer
InternalSID:
type: integer
nullable: true
TestSiteID:
type: integer
TestSiteCode:
type: string
TestSiteName:
type: string
nullable: true
SID:
type: string
SampleID:
type: string
Result:
type: string
nullable: true
Unit:
type: string
nullable: true
ResultStatus:
type: string
enum: [PEN, PRE, FIN, AMD]
nullable: true
Verified:
type: boolean
default: false
VerifiedBy:
type: integer
nullable: true
VerifiedDate:
type: string
format: date-time
nullable: true
EnteredBy:
type: integer
nullable: true
AbnormalFlag:
type: string
enum: [H, L, N, A, C]
nullable: true
Comment:
type: string
nullable: true
CreateDate:
type: string
format: date-time
ReferenceRange:
type: object
nullable: true
properties:
Low:
type: number
High:
type: number
Display:
type: string
Patient:
type: object
properties:
InternalPID:
type: integer
PatientID:
type: string
NameFirst:
type: string
NameLast:
type: string
Birthdate:
type: string
format: date
Sex:
type: string
ResultEntryRequest:
type: object
required:
- Result
properties:
Result:
type: string
description: The result value
Unit:
type: string
description: Unit override (optional)
AbnormalFlag:
type: string
enum: [H, L, N, A, C]
description: Override auto-calculated flag (optional)
Comment:
type: string
description: Result comment
ResultStatus:
type: string
enum: [PEN, PRE, FIN]
description: Set status (can't set to AMD via update)
ResultBatchRequest:
type: object
required:
- results
properties:
results:
type: array
items:
type: object
properties:
ResultID:
type: integer
Result:
type: string
Unit:
type: string
Comment:
type: string
ResultVerifyRequest:
type: object
properties:
comment:
type: string
description: Optional verification comment
ResultUnverifyRequest:
type: object
required:
- reason
properties:
reason:
type: string
description: Required reason for amendment
ResultWorklistResponse:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
type: object
properties:
ResultID:
type: integer
PatientName:
type: string
PatientID:
type: string
OrderID:
type: string
TestCode:
type: string
TestName:
type: string
ResultStatus:
type: string
Priority:
type: string
OrderDate:
type: string
format: date-time
```
### 4.2 Create API Paths Documentation
**File:** `public/paths/results.yaml`
Document all endpoints (GET /api/results, GET /api/results/{id}, PATCH /api/results/{id}, POST /api/results/batch, POST /api/results/{id}/verify, POST /api/results/{id}/unverify, GET /api/results/worklist, GET /api/results/order/{orderID}) with:
- Summary and description
- Security (bearerAuth)
- Parameters (path, query)
- RequestBody schemas
- Response schemas
- Error responses
---
## Phase 5: Reference Range Integration (Priority: MEDIUM)
### 5.1 Create RefRangeService
**File:** `app/Libraries/RefRangeService.php`
```php
<?php
namespace App\Libraries;
use App\Models\RefRange\RefNumModel;
use App\Models\RefRange\RefTxtModel;
class RefRangeService
{
/**
* Get applicable reference range for a test and patient
*
* @param int $testSiteID
* @param array $patient Contains: age (in months), sex (M/F), specimenType
* @return array|null
*/
public function getApplicableRange(int $testSiteID, array $patient): ?array
/**
* Evaluate numeric result against range
*
* @param float $value
* @param array $range
* @return string H, L, N, A, C
*/
public function evaluateNumeric(float $value, array $range): string
/**
* Get text reference options
*
* @param int $refTxtID
* @return array
*/
public function getTextOptions(int $refTxtID): array
/**
* Format range for display
*
* @param array $range
* @return string
*/
public function formatDisplay(array $range): string
}
```
---
## Phase 6: Testing (Priority: MEDIUM)
### 6.1 Create Feature Tests
**File:** `tests/feature/Results/ResultEntryTest.php`
Test scenarios:
- Get results list with filters
- Get single result with details
- Update result value
- Validation errors (invalid value)
- Auto-calculate abnormal flag
- Delta check notification
- Batch update
**File:** `tests/feature/Results/ResultVerifyTest.php`
Test scenarios:
- Verify result successfully
- Unverify with reason
- Attempt to modify verified result
- Permission checks
- Amendment workflow
**File:** `tests/feature/Results/ResultWorklistTest.php`
Test scenarios:
- Get worklist by workstation
- Filter by priority
- Sort by order date
- Pagination
### 6.2 Create Unit Tests
**File:** `tests/unit/Libraries/ResultEntryServiceTest.php`
Test scenarios:
- Result validation
- Reference range matching
- Abnormal flag calculation
- Delta calculation
- Calculated test formulas
**File:** `tests/unit/Libraries/RefRangeServiceTest.php`
Test scenarios:
- Age-based range selection
- Sex-based range selection
- Specimen type matching
- Boundary value evaluation
---
## Data Flow Diagram
```
Order Creation
|
v
Empty patres records created
|
v
GET /api/results/worklist <-- Technician sees pending results
|
v
GET /api/results/{id} <-- Load result with patient info
|
v
PATCH /api/results/{id} <-- Enter result value
| |
| v
| ResultEntryService.validateResult()
| |
| v
| RefRangeService.getApplicableRange()
| |
| v
| Auto-calculate AbnormalFlag
| |
v v
Result updated in patres
|
v
POST /api/results/{id}/verify <-- Senior tech/pathologist
|
v
ResultStatus = FIN, Verified = true
```
---
## Status Definitions
| Status | Code | Description |
|--------|------|-------------|
| Pending | PEN | Order created, awaiting result entry |
| Preliminary | PRE | Result entered but not verified |
| Final | FIN | Result verified by authorized user |
| Amended | AMD | Previously final result modified |
## Abnormal Flag Definitions
| Flag | Meaning | Action Required |
|------|---------|-----------------|
| N | Normal | None |
| H | High | Review |
| L | Low | Review |
| A | Abnormal (text) | Review |
| C | Critical | Immediate notification |
---
## Questions for Implementation
Before starting implementation, clarify:
1. **Who can verify results?**
- Option A: Any authenticated user
- Option B: Users with specific role (senior tech, pathologist)
- Option C: Configure per test/discipline
2. **Can calculated tests be manually edited?**
- Option A: No, always auto-computed
- Option B: Yes, allow override with reason
- Option C: Configurable per test
3. **Audit trail requirements:**
- Option A: Full history (every change)
- Option B: Only amendments (verified→unverify→verify)
- Option C: No audit trail needed
4. **Critical results handling:**
- Option A: Flag only
- Option B: Flag + notification system
- Option C: Flag + mandatory acknowledgment
5. **Batch entry priority:**
- Must-have or nice-to-have?
- Support for templates/predefined sets?
6. **Delta check sensitivity:**
- Fixed percentage threshold (e.g., 20%)?
- Test-specific thresholds?
- Configurable?
---
## Implementation Checklist
- [ ] Phase 1.1: Extend PatResultModel with CRUD methods
- [ ] Phase 1.2: Create ResultEntryService with business logic
- [ ] Phase 1.3: Implement ResultController methods
- [ ] Phase 2.1: Create migration for new patres fields
- [ ] Phase 2.2: Create patreshistory table
- [ ] Phase 3.1: Add routes to Routes.php
- [ ] Phase 4.1: Create results.yaml schema
- [ ] Phase 4.2: Create results.yaml paths documentation
- [ ] Phase 4.3: Run `node public/bundle-api-docs.js`
- [ ] Phase 5.1: Create RefRangeService
- [ ] Phase 6.1: Create feature tests
- [ ] Phase 6.2: Create unit tests
- [ ] Run full test suite: `./vendor/bin/phpunit`
---
## Estimated Timeline
| Phase | Duration | Priority |
|-------|----------|----------|
| Phase 1: Core Management | 2-3 days | HIGH |
| Phase 2: Database Schema | 0.5 day | HIGH |
| Phase 3: API Routes | 0.5 day | HIGH |
| Phase 4: Documentation | 1 day | MEDIUM |
| Phase 5: Reference Ranges | 1-2 days | MEDIUM |
| Phase 6: Testing | 2-3 days | MEDIUM |
| **Total** | **7-10 days** | |
---
## Notes
- All dates stored in UTC, convert to local time for display
- Use transactions for all multi-table operations
- Follow existing code style (camelCase methods, snake_case properties)
- Update AGENTS.md if adding new commands or patterns
- Consider performance: worklist queries should be fast (< 500ms)
*Last Updated: 2025-03-04*

View File

@ -1,249 +0,0 @@
# CLQMS MVP Sprint Plan
> **Scope**: Result Entry + Validation + Reporting (2-week sprint)
> **Removed**: Edge API, Worklists, Calculated Tests, QC, Instrument Integration
---
## Current State
### What's Working
- Patient CRUD + Identifiers + Addresses
- Patient Visit + ADT
- Lab Orders (create with specimen generation)
- Test Definitions + Reference Ranges (refnum)
- Authentication (JWT)
- Master Data (ValueSets, Locations, etc.)
### Critical Gap
- `ResultController` only returns JWT payload (no CRUD)
- `PatResultModel` has no validation logic
- No PDF generation capability
- Only 1 working route: `GET /api/result` (returns auth check only)
---
## Phase 1: Result CRUD + Validation (Week 1)
### Day 1-2: PatResultModel Enhancement
**Location**: `app/Models/PatResultModel.php`
Add methods:
- `validateAndFlag($resultID, $value)` - Compare result against refnum ranges
- Check patient age, sex from order
- Match refnum criteria
- Return 'L', 'H', or null
- `getByOrder($orderID)` - Fetch all results for an order with test names
- `getByPatient($internalPID)` - Get cumulative patient results
- `updateWithValidation($resultID, $data)` - Update + auto-validate
### Day 3-4: ResultController
**Location**: `app/Controllers/ResultController.php`
Replace placeholder with full CRUD:
| Method | Endpoint | Description |
|--------|----------|-------------|
| `index()` | `GET /api/results` | List results (filter by order/patient) |
| `show($id)` | `GET /api/results/{id}` | Get single result |
| `update($id)` | `PATCH /api/results/{id}` | Update result + auto-validate |
| `delete($id)` | `DELETE /api/results/{id}` | Soft delete result |
**Features**:
- Filter by `order_id` or `patient_id` query param
- Include test name from `testdefsite`
- Auto-calculate flags on update
- Return standardized ResponseTrait format
### Day 5: Routes & Testing
**Location**: `app/Config/Routes.php`
Replace line 18:
```php
// OLD
$routes->get('result', 'ResultController::index');
// NEW
$routes->group('results', function ($routes) {
$routes->get('/', 'ResultController::index');
$routes->get('(:num)', 'ResultController::show/$1');
$routes->patch('(:num)', 'ResultController::update/$1');
$routes->delete('(:num)', 'ResultController::delete/$1');
});
```
**Testing**: Manual API testing with Postman/Insomnia
---
## Phase 2: HTML Report Viewing (Week 2)
### Day 1-2: Report View & Controller
**Create Report View**: `app/Views/reports/lab_report.php`
Template sections:
- Patient header (Name, DOB, ID)
- Order info (OrderID, Date, Doctor)
- Results table (Test, Result, Units, Reference, Flag)
- Footer (Lab info, signature)
- Print-friendly CSS with `@media print` support
**Create ReportController**: `app/Controllers/ReportController.php`
| Method | Endpoint | Description |
|--------|----------|-------------|
| `view($orderID)` | `GET /api/reports/{orderID}` | Generate HTML lab report |
**Logic**:
- Fetch order with patient details
- Get all results for order (with flags)
- Include test definitions (name, units, ref range)
- Render HTML view using CodeIgniter's view() function
- Users can print/save as PDF via browser
### Day 3-4: Routes & Polish
**Routes** (add to Routes.php):
```php
$routes->get('reports/(:num)', 'ReportController::view/$1');
```
**HTML Styling**:
- Professional lab report format
- Show abnormal flags (L/H) highlighted
- Include reference ranges
- Signature line for pathologist
- Responsive design with print button
**Testing**:
- Generate report with actual data
- Verify formatting
- Test print functionality
- Test edge cases (no results, all normal, mix of flags)
**Note**: PDF generation deferred to post-MVP. Users can use browser's "Print to PDF" feature for now.
---
## Database Schema Reference
### patres (Results)
| Column | Type | Notes |
|--------|------|-------|
| ResultID | INT | PK |
| OrderID | INT | FK to ordertest |
| TestSiteID | INT | FK to testdefsite |
| Result | VARCHAR(255) | The actual value |
| ResultDateTime | DATETIME | When entered |
| RefNumID | INT | Applied reference range |
### refnum (Reference Ranges)
| Column | Type | Notes |
|--------|------|-------|
| RefNumID | INT | PK |
| TestSiteID | INT | Which test |
| Sex | VARCHAR | M/F/ALL |
| AgeStart/AgeEnd | INT | Age criteria |
| Low/High | DECIMAL | Range values |
| LowSign/HighSign | VARCHAR | <=, <, etc |
---
## API Endpoints Summary
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/api/results` | GET | Yes | List results (filter: ?order_id= or ?patient_id=) |
| `/api/results/{id}` | GET | Yes | Get single result |
| `/api/results/{id}` | PATCH | Yes | Update result + auto-flag |
| `/api/results/{id}` | DELETE | Yes | Soft delete result |
| `/api/reports/{orderID}` | GET | Yes | Generate PDF report |
---
## Flag Logic (Reference Range Validation)
```php
// Pseudo-code for validation
function validateResult($resultValue, $refNumID) {
$ref = getRefNum($refNumID);
$patient = getPatientFromOrder($orderID);
// Match criteria (sex, age)
if (!matchesCriteria($ref, $patient)) {
return null; // No flag if criteria don't match
}
$value = floatval($resultValue);
$low = floatval($ref['Low']);
$high = floatval($ref['High']);
// Check low
if ($ref['LowSign'] === '<=' && $value <= $low) return 'L';
if ($ref['LowSign'] === '<' && $value < $low) return 'L';
// Check high
if ($ref['HighSign'] === '>=' && $value >= $high) return 'H';
if ($ref['HighSign'] === '>' && $value > $high) return 'H';
return null; // Normal
}
```
---
## Out of Scope (Post-MVP)
- **Edge API**: Instrument integration (`app/Controllers/EdgeController.php`)
- **Worklist Generation**: Technician worklists
- **Calculated Tests**: Formula execution (CALC type)
- **Quality Control**: QC samples, Levy-Jennings charts
- **Calculated Test Execution**: Deferred to later
- **Delta Checking**: Result trending
- **Critical Result Alerts**: Notification system
- **Audit Trail**: Complete audit logging
---
## Success Criteria
- [ ] Can enter result values via API
- [ ] Results auto-validate against refnum ranges
- [ ] Abnormal results show L/H flags
- [ ] Can view all results for an order
- [ ] Can generate PDF lab report
- [ ] Report shows patient, order, results with flags
---
## Files to Create/Modify
### Create
1. `app/Controllers/ReportController.php` - HTML report controller
2. `app/Views/reports/lab_report.php` - HTML report template with dynamic flag calculation
3. `app/Database/Migrations/2026-03-04-073950_RemoveFlagColumnFromPatRes.php` - Remove Flag column
4. `public/paths/reports.yaml` - OpenAPI documentation for reports endpoint
### Modify
1. `app/Models/PatResultModel.php` - Add validation methods (validateAndFlag, getByOrder, getByPatient, updateWithValidation, getWithRelations, softDelete)
2. `app/Controllers/ResultController.php` - Full CRUD (index, show, update, delete)
3. `app/Config/Routes.php` - New routes for results and reports
4. `public/paths/results.yaml` - Updated OpenAPI documentation
5. `public/api-docs.yaml` - Added Reports tag
6. Regenerated `public/api-docs.bundled.yaml`
**Note**: Flag is calculated dynamically at runtime, not stored in database. This allows for:
- Real-time validation against current reference ranges
- Support for reference range updates without re-processing historical results
- Reduced storage requirements
**API Documentation**: Remember to run `node public/bundle-api-docs.js` after updating YAML files to regenerate the bundled documentation.
---
*Last Updated: 2026-03-04*
*Sprint Duration: 2 weeks*
*Team Size: 1 developer*

File diff suppressed because one or more lines are too long

View File

@ -1,434 +0,0 @@
# 📋 Test Types & Reference Types Guide
> **Quick Overview**: This guide helps you understand the different types of tests and how to display them in the frontend.
---
## 🎯 What Are Test Types?
Think of test types as "categories" that determine how a test behaves and what information it needs.
### Quick Reference Card
```
┌─────────────┬────────────────────────────┬────────────────────────┐
│ Type │ Use This For... │ Example │
├─────────────┼────────────────────────────┼────────────────────────┤
│ TEST │ Standard lab tests │ Blood Glucose, CBC │
│ PARAM │ Components of a test │ WBC count (in CBC) │
│ CALC │ Formula-based results │ BMI, eGFR │
│ GROUP │ Panels/batteries │ Lipid Panel, CMP │
│ TITLE │ Section headers │ "Hematology Results" │
└─────────────┴────────────────────────────┴────────────────────────┘
```
---
## 🧪 Detailed Test Types
### 1. TEST - Standard Laboratory Test
**Icon**: 🧫 **Color**: Blue
Use this for regular tests that have:
- Reference ranges (normal values)
- Units (mg/dL, mmol/L, etc.)
- Collection requirements
**Example**: Blood Glucose, Hemoglobin, Cholesterol
**What to Display**:
- Test code and name
- Reference range
- Units
- Collection instructions
- Expected turnaround time
---
### 2. PARAM - Parameter
**Icon**: 📊 **Color**: Light Blue
Use this for individual components within a larger test.
**Example**:
- Complete Blood Count (GROUP) contains:
- WBC (PARAM)
- RBC (PARAM)
- Hemoglobin (PARAM)
**What to Display**:
- Same as TEST, but shown indented under parent
- Often part of a GROUP
---
### 3. CALC - Calculated Test
**Icon**: 🧮 **Color**: Purple
Use this for tests computed from other test results using formulas.
**Example**:
- BMI (calculated from height & weight)
- eGFR (calculated from creatinine, age, etc.)
**What to Display**:
- Formula description
- Input parameters (which tests feed into this)
- Result value
- Reference range (if applicable)
**Special Fields**:
- `FormulaInput` - What values go in?
- `FormulaCode` - How is it calculated?
---
### 4. GROUP - Group Test (Panel/Battery)
**Icon**: 📦 **Color**: Green
Use this to bundle multiple related tests together.
**Example**:
- Lipid Panel (GROUP) contains:
- Total Cholesterol (PARAM)
- HDL (PARAM)
- LDL (PARAM)
- Triglycerides (PARAM)
**What to Display**:
- Group name
- List of member tests
- Individual results for each member
**Structure**:
```
📦 Lipid Panel (GROUP)
├── Total Cholesterol (PARAM): 180 mg/dL
├── HDL (PARAM): 55 mg/dL
├── LDL (PARAM): 110 mg/dL
└── Triglycerides (PARAM): 150 mg/dL
```
---
### 5. TITLE - Section Header
**Icon**: 📌 **Color**: Gray
Use this for organizing tests into sections on reports.
**Example**: "Hematology", "Chemistry", "Urinalysis"
**What to Display**:
- Just the title text
- No results or values
- Used for organization only
---
## 📐 Reference Types Explained
Reference types tell you **how to interpret the results** - what "normal" looks like.
### Choose Your Reference Type:
```
┌──────────────────────┐
│ Reference Type? │
└──────────┬───────────┘
┌───────────────────┼───────────────────┐
│ │ │
Numbers? Text values? Threshold?
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ NMRC │ │ TEXT │ │ THOLD │
│ (Numeric) │ │ (Text) │ │ (Threshold) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ RANGE │ │ Free │ │ Positive/ │
│ (Min-Max) │ │ Text │ │ Negative │
│ OR │ │ OR │ │ OR │
│ THOLD │ │ VSET │ │ < > = │
│ (Threshold) │ │ (Dropdown) │ │ cutoff │
└─────────────┘ └─────────────┘ └─────────────┘
```
### Reference Type Details
| Type | Visual | Example |
|------|--------|---------|
| **NMRC** - Numeric Range | `70 - 100 mg/dL` | Blood glucose: 70-100 mg/dL |
| **TEXT** - Text Value | `"Normal"` or `"Positive"` | Urinalysis: "Clear", "Cloudy" |
| **THOLD** - Threshold | `> 60` or `< 5.5` | eGFR: > 60 (normal) |
| **VSET** - Value Set | Dropdown options | Organism: [E.coli, Staph, etc.] |
---
## 🎨 Frontend Display Patterns
### Pattern 1: Test List View
```javascript
// When showing a list of tests
function renderTestCard(test) {
const typeIcon = getIcon(test.TestType);
const typeColor = getColor(test.TestType);
return `
<div class="test-card ${typeColor}">
<span class="icon">${typeIcon}</span>
<h3>${test.TestSiteName}</h3>
<span class="badge">${test.TestTypeLabel}</span>
<code>${test.TestSiteCode}</code>
</div>
`;
}
```
### Pattern 2: Reference Range Display
```javascript
// Show reference range based on type
function renderReferenceRange(test) {
switch(test.RefType) {
case 'NMRC':
return `${test.MinValue} - ${test.MaxValue} ${test.Unit}`;
case 'TEXT':
return test.ReferenceText || 'See report';
case 'THOLD':
return `${test.ThresholdOperator} ${test.ThresholdValue}`;
case 'VSET':
return 'Select from list';
}
}
```
### Pattern 3: Group Test Expansion
```javascript
// Expandable group test
function renderGroupTest(test) {
return `
<div class="group-test">
<button onclick="toggleGroup(${test.TestSiteID})">
📦 ${test.TestSiteName} (${test.testdefgrp.length} tests)
</button>
<div class="members" id="group-${test.TestSiteID}">
${test.testdefgrp.map(member => renderTestRow(member)).join('')}
</div>
</div>
`;
}
```
---
## 🗂️ Data Structure Visualization
### Test Hierarchy
```
┌─────────────────────────────────────────────────────────┐
│ TEST DEFINITION │
├─────────────────────────────────────────────────────────┤
│ TestSiteID: 12345 │
│ TestSiteCode: GLUC │
│ TestSiteName: Glucose │
│ TestType: TEST │
├─────────────────────────────────────────────────────────┤
│ 📎 Additional Info (loaded based on TestType): │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ TestDefTech (for TEST/PARAM) │ │
│ │ - DisciplineID, DepartmentID │ │
│ │ - ResultType, RefType │ │
│ │ - Unit, Method, ExpectedTAT │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ TestDefCal (for CALC) │ │
│ │ - FormulaInput, FormulaCode │ │
│ │ - RefType, Unit, Decimal │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ TestDefGrp (for GROUP) │ │
│ │ - Array of member tests │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ TestMap (for ALL types) │ │
│ │ - Mapping to external systems │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 💡 Common Scenarios
### Scenario 1: Creating a Simple Test
**Need**: Add "Hemoglobin" test
**Choose**: TEST type with NMRC reference
```javascript
const hemoglobin = {
TestSiteCode: 'HGB',
TestSiteName: 'Hemoglobin',
TestType: 'TEST',
testdeftech: {
RefType: 'NMRC',
Unit: 'g/dL',
// Reference range: 12-16 g/dL
}
};
```
### Scenario 2: Creating a Panel
**Need**: "Complete Blood Count" with multiple components
**Choose**: GROUP type with PARAM children
```javascript
const cbc = {
TestSiteCode: 'CBC',
TestSiteName: 'Complete Blood Count',
TestType: 'GROUP',
testdefgrp: [
{ TestSiteCode: 'WBC', TestType: 'PARAM' },
{ TestSiteCode: 'RBC', TestType: 'PARAM' },
{ TestSiteCode: 'HGB', TestType: 'PARAM' },
{ TestSiteCode: 'PLT', TestType: 'PARAM' }
]
};
```
### Scenario 3: Calculated Result
**Need**: BMI from height and weight
**Choose**: CALC type with formula
```javascript
const bmi = {
TestSiteCode: 'BMI',
TestSiteName: 'Body Mass Index',
TestType: 'CALC',
testdefcal: {
FormulaInput: 'HEIGHT,WEIGHT',
FormulaCode: 'WEIGHT / ((HEIGHT/100) * (HEIGHT/100))',
RefType: 'NMRC',
Unit: 'kg/m²'
}
};
```
---
## 🎨 Visual Style Guide
### Color Coding
| Type | Primary Color | Background | Usage |
|------|---------------|------------|-------|
| TEST | `#0066CC` | `#E6F2FF` | Main test cards |
| PARAM | `#3399FF` | `#F0F8FF` | Component rows |
| CALC | `#9933CC` | `#F5E6FF` | Calculated fields |
| GROUP | `#00AA44` | `#E6F9EE` | Expandable panels |
| TITLE | `#666666` | `#F5F5F5` | Section headers |
### Icons
| Type | Icon | Unicode |
|------|------|---------|
| TEST | 🧫 | `\u{1F9EB}` |
| PARAM | 📊 | `\u{1F4CA}` |
| CALC | 🧮 | `\u{1F9EE}` |
| GROUP | 📦 | `\u{1F4E6}` |
| TITLE | 📌 | `\u{1F4CC}` |
---
## 🔌 Quick API Reference
### Fetch Tests
```javascript
// Get all tests for a site
GET /api/tests?siteId=123
// Get specific test type
GET /api/tests?testType=GROUP
// Search tests
GET /api/tests?search=glucose
// Get single test with details
GET /api/tests/12345
```
### Value Set Helpers
```javascript
// Get human-readable labels
const labels = {
testType: valueSet.getLabel('test_type', test.TestType),
refType: valueSet.getLabel('reference_type', test.RefType),
resultType: valueSet.getLabel('result_type', test.ResultType)
};
// Get dropdown options
const testTypes = valueSet.get('test_type');
// Returns: [{value: "TEST", label: "Test"}, ...]
```
---
## 📚 Value Set File Locations
```
app/Libraries/Data/
├── test_type.json # TEST, PARAM, CALC, GROUP, TITLE
├── reference_type.json # NMRC, TEXT, THOLD, VSET
├── result_type.json # NMRIC, RANGE, TEXT, VSET
├── numeric_ref_type.json # RANGE, THOLD
└── text_ref_type.json # VSET, TEXT
```
---
## ✅ Checklist for Frontend Developers
- [ ] Show correct icon for each test type
- [ ] Display reference ranges based on RefType
- [ ] Handle GROUP tests as expandable panels
- [ ] Show TITLE tests as section headers
- [ ] Display CALC formulas when available
- [ ] Use ValueSet labels for all coded fields
- [ ] Respect VisibleScr/VisibleRpt flags
- [ ] Handle soft-deleted tests (EndDate check)
---
## 🆘 Need Help?
**Q: When do I use PARAM vs TEST?**
A: Use PARAM for sub-components of a GROUP. Use TEST for standalone tests.
**Q: What's the difference between NMRC and THOLD?**
A: NMRC shows a range (70-100). THOLD shows a threshold (> 60 or < 5.5).
**Q: Can a GROUP contain other GROUPs?**
A: Yes! Groups can be nested (though typically only 1-2 levels deep).
**Q: How do I know which details to fetch?**
A: Check `TestType` first, then look for the corresponding property:
- TEST/PARAM → `testdeftech`
- CALC → `testdefcal`
- GROUP → `testdefgrp`
- All types → `testmap`

View File

@ -1,514 +0,0 @@
Here is the converted Markdown format of the Use Case document. I have structured it with headers and lists to make it easily parsable by an AI agent.
```markdown
# Use Case Document
## Use Case Authentication
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-01 |
| **Use Case Name** | Authentication |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Phlebotomist Lab, Perawat, DSPK/Konsulen, Supervisor Lab, Manajer Lab, Database Administrator, System Administrator |
| **Aktor Sekunder** | - |
| **Tujuan** | Verifikasi identitas pengguna, yang mencoba mengakses sistem, memastikan bahwa mereka adalah orang yang mereka klaim. Bertindak sebagai mekanisme keamanan primer untuk mencegah akses tidak sah, melindungi data, dan mengurangi risiko seperti pencurian identitas dan pelanggaran keamanan. |
| **Prasyarat** | Data pengguna sudah terdefinisi dalam sistem sebagai User atau Contact. |
### Alur Utama
1. Aktor klik tombol Login.
2. System menampilkan login dialog yang terdiri dari User ID dan Password.
3. Aktor memasukkan email address sebagai User ID.
4. System memeriksa email address di table User, SiteStatus, Contact dan ContactDetail:
- Jika Aktor menggunakan email pribadi, maka System menampilkan pilihan sites dimana Aktor memiliki akses. Aktor, kemudian memilih salah satu site.
- Jika Aktor menggunakan email site, maka System langsung mengarahkan ke site yang bersangkutan.
5. Aktor memasukkan password.
6. System memeriksa kebenaran User ID dan password.
7. System memeriksa role dan menu apa saja yang bisa diakses Aktor.
8. System menampilkan halaman utama dengan menu sesuai role Aktor.
### Alur Alternatif
-
### Alur Pengecualian
* **Aktor tidak terdaftar:**
* System menampilkan pesan: “Unregistered user, please contact system administrator”.
* **Aktor ditemukan tetapi:**
* **Disabled:** System menampilkan pesan: “Disabled user, please contact system administrator”.
* **Password expired:** System menampilkan pesan: “Your password is expired, please contact system administrator”.
* **Password salah:**
* System menampilkan pesan: “Invalid login”.
* System menghitung jumlah percobaan login password yang gagal dan mencatat dalam Audit log (device dimana login attempt dilakukan, waktu).
* System menghentikan proses login untuk User ID tersebut selama x jam dan menampilkan pesan, ”Please try again in x hours or contact system administrator”.
### Kondisi Akhir
* Aktor masuk ke halaman utama dan mendapat akses menu-menu system yang sesuai.
* Audit mencatat User ID, waktu, device dimana Aktor melakukan login.
---
## Use Case Patient Registration
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-02a |
| **Use Case Name** | Patient Registration |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mencatatkan data demografi pasien baru ke oleh System |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi dalam System.<br>3. Pasien menunjukkan identitas yang sah (kartu identitas, atau rujukan). |
### Alur Utama
1. Aktor membuka halaman Patient Management Patient Registration.
2. Aktor memasukkan PID.
3. System memeriksa apakah PID sudah ada.
4. System meminta detail pasien (nama, tanggal lahir, jenis kelamin, informasi kontak, nomor identitas, dst).
5. Aktor memasukkan informasi demografis pasien, dengan mandatory data:
- patient.NameFirst
- patient.Gender
- patient.Birthdate
6. Jika Aktor memasukkan `patidt.IdentifierType` dan `patidt.Identifier`, maka System memeriksa apakah sudah ada record pasien yang menggunakan Identifier yang sama.
7. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A04` (Register), mengkonfirmasi registrasi berhasil dan menampilkan ringkasan pasien.
8. Aktor memberikan konfirmasi pendaftaran kepada Pasien (misalnya, slip cetak atau ID digital barcode, QRIS, dll).
### Alur Alternatif
* **Record pasien sudah ada:**
1. Aktor memasukkan PID.
2. System mengambil record yang sudah ada dan menampilkan data di halaman Patient Management Patient Search & Update.
3. Aktor memperbarui data jika diperlukan.
4. Sistem menyimpan perubahan dan membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A08` (Update patient information).
### Alur Pengecualian
* **Mandatory data tidak ada:**
* System menolak menyimpan record.
* Aktor diminta untuk melengkapi, setidaknya mandatory data.
* **Record pasien tidak ada tetapi ditemukan record yang menggunakan `patidt.IdentifierType` dan `patidt.Identifier` yang sama:**
* System menampilkan pesan Multiple IDs found”.
* System menampilkan records dengan `patidt.IdentifierType` dan `patidt.Identifier` yang sama.
* Aktor melakukan review.
* Aktor memilih salah satu dari kemungkinan berikut:
1. Melanjutkan membuat record pasien baru, mengabaikan record ganda.
2. Melanjutkan membuat record pasien baru, kemudian menggabungkan record pasien (lihat UC-02b).
3. Membatalkan pendaftaran.
### Kondisi Akhir
* Record pasien dibuat atau diperbarui di System.
* PID pasien tersedia untuk test ordering & tracing.
* Audit mencatat bahwa record dibuat/diperbarui secara manual, User ID yang mendaftarkan/memperbarui data pasien, device dimana, kapan, dan data apa yang dimasukkan.
---
## Use Case Patient Link
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-02b |
| **Use Case Name** | Patient Link |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Link (menghubungkan) satu atau beberapa record (PID) pasien (source) dengan record pasien lainnya (destination). PatientID destination adalah surviving entity, yang akan digunakan dalam semua aktivitas laboratorium. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi dalam System.<br>3. PID source dan destination telah tercatat dalam System.<br>4. PID source dan destination memiliki `patidt.IdentifierType` dan `patidt.Identifier` yang sama atau nama, alamat dan tanggal lahir yang sama. |
### Alur Utama
1. Aktor membuka halaman Patient Management - Patient Link.
2. Aktor mencari PID menggunakan:
- `patidt.IdentifierType` dan `patidt.Identifier`
- Nama, alamat dan tanggal lahir
3. System menampilkan semua PID dengan `patidt.IdentifierType` dan `patidt.Identifier` dan/atau nama, alamat dan tanggal lahir yang sama.
4. Aktor memilih dan menentukan satu PID untuk menjadi destination. (Lihat Alur Pengecualian).
5. Aktor memilih dan menentukan satu atau lebih, PID yang menjadi source. (Lihat Alur Pengecualian).
6. Aktor menghubungkan PID-PID tersebut dengan menekan tombol Link.
7. System meminta konfirmasi dari Aktor dengan menampilkan pesan,” Please confirm to link these patient records”. Disertai pilihan “Confirm” dan “Cancel”.
8. Aktor mengkonfirmasi Patient Link dengan menekan tombol Confirm.
9. System melakukan:
- Membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A24` (Link Patient Information).
- Mengkonfirmasi Patient Link berhasil.
- Menampilkan ringkasan PID yang dihubungkan.
### Alur Alternatif
-
### Alur Pengecualian
* **System menemukan bahwa suatu record pasien telah menjadi source, ditandai dengan field `patient.LinkTo` telah terisi dengan PID dari record lain (ditampilkan):**
* **Multiple link:**
* Aktor memilih dan menunjuk record tersebut sebagai source bagi PID yang berbeda.
* System menampilkan peringatan “Multiple link” di samping PID tersebut dan pilihan (check mark) tidak bisa dilakukan.
* **Multi-level Link:**
* Aktor memilih dan menunjuk record tersebut sebagai destination bagi PID yang berbeda.
* System menampilkan peringatan “Multi-level link” di samping PID tersebut dan pilihan (check mark) tidak bisa dilakukan.
* Jika semua atau satu-satunya PID mendapat peringatan tersebut maka, proses Patient Link sama sekali tidak bisa dilanjutkan.
* Jika ada PID lain yang tidak mendapat peringatan, maka proses Patient Link dilanjutkan atas PID tanpa peringatan.
### Kondisi Akhir
* PID source terhubung dengan PID destination.
* Relasi source dengan test order dan lain-lain tidak berubah sebelum dan sesudah proses Patient Link.
* Semua test order milik PID source dan destination bisa ditampilkan dalam satu cumulative view/report.
* PID destination tersedia untuk test ordering & tracing.
* PID source tetap bisa dicari tetapi tidak bisa di-edit maupun digunakan untuk test ordering.
* Audit mencatat Patient Link dilakukan secara manual, waktu, User ID yang melakukan Patient Link serta device dimana aktivitas tersebut dilakukan.
---
## Use Case Patient Unlink
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-02c |
| **Use Case Name** | Patient Unlink |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Melepaskan link antara source PID dan destination PID. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi dalam System.<br>3. Pasien sudah pernah registrasi di System, ditandai dengan adanya PID dengan `patidt.IdentifierType` dan `patidt.Identifier` yang sama. |
### Alur Utama
1. Aktor membuka halaman Patient Management Patient Unlink.
2. Aktor mencari record pasien menggunakan PID.
3. System menampilkan PID berikut data demografinya dan semua linked PID.
4. Aktor uncheck source PID(s) yang hendak dilepaskan dari destination PID.
5. Aktor melepas hubungan PID-PID tersebut dengan menekan tombol Unlink.
6. System meminta konfirmasi dari Aktor dengan menampilkan pesan,” Please confirm to unlink these patient records”. Disertai pilihan “Confirm” dan “Cancel”.
7. Aktor mengkonfirmasi Patient Unink dengan menekan tombol Confirm.
8. System melakukan:
- Mengosongkan field `patient.LinkTo` dari source PID.
- Membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A37` (Unlink Patient Information).
- Mengkonfirmasi Patient Unlink berhasil.
- Menampilkan ringkasan destination dan source PID yang unlinked.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Source PID aktif kembali, bisa diedit dan tersedia untuk test ordering & tracing.
* Unlink source terjadi bisa isi field LinkTo dikosongkan Kembali.
* Audit mencatat Patient Unlink dilakukan secara manual, waktu, User ID yang melakukan Patient Unlink dan device dimana aktivitas tersebut dilakukan.
---
## Use Case Patient Admission
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03a |
| **Use Case Name** | Patient Admission |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Menerima pasien di fasyankes untuk perawatan atau observasi. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Record pasien tersedia di System, ditandai dengan adanya PID. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Patient Admission.
2. Aktor memasukkan PID.
3. System memeriksa apakah PID ada.
4. System menampilkan data demografi pasien dan meminta data-data:
- **Mandatory data:** PVID, dokter, location.
- **Optional data:** EpisodeID, diagnosis (bisa lebih dari satu), lampiran-lampiran.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A01` (Admit), mengkonfirmasi admission berhasil dan menampilkan ringkasan admission.
6. Aktor memberikan konfirmasi admission kepada Pasien (misalnya, slip cetak atau ID digital barcode, QRIS, dll).
### Alur Alternatif
* **PID tidak ada:**
1. System menampilkan pesan, “PID does not exist. Proceed to Patient Registration?”.
2. Aktor memilih “Yes” dan System membuka halaman Patient Management Patient Registration.
3. Aktor melakukan activity patient registration dilanjutkan patient admission.
* **Pembaruan optional data:**
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System mengambil record yang sudah ada.
4. Aktor memperbarui data jika diperlukan.
5. Sistem menyimpan perubahan.
### Alur Pengecualian
* **Mandatory data tidak ada:**
* System menolak menyimpan record.
* Aktor diminta untuk melengkapi, setidaknya mandatory data.
### Kondisi Akhir
* Kunjungan pasien ke fasyankes tercatat (patvisit records) di System, ditandai dengan adanya PVID dan direlasikan dengan dokter dan ruangan di fasyankes.
* Audit mencatat admission/perubahannya dilakukan secara manual, User ID yang melakukan, device dimana, kapan, dan data apa saja yang dimasukkan.
---
## Use Case Cancel Admission
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03b |
| **Use Case Name** | Cancel Patient Admission |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Membatalkan penerimaan pasien di fasyankes. Pembatalan bisa disebabkan: Data registrasi salah atau tidak lengkap, Cakupan asuransi tidak valid, Pasien menolak rawat inap, Pasien dialihkan, Permintaan rawat inap salah, Kondisi pasien berubah, Permintaan pasien, dll. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengkonfirmasi pembatalan admission ke pihak terkait dan melakukan pembatalan.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A11` (Cancel Admit), mengkonfirmasi cancel patient admission berhasil dan menampilkan ringkasan cancel patient admission.
6. Aktor memberikan konfirmasi cancel patient admission kepada pihak terkait (misalnya, slip cetak atau ID digital barcode, QRIS, dll).
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Cancel patient admission tercatat (patvisit records) di System, ditandai dengan record di `patvisitadt` dengan `patvisitadt.Code: A11`.
* Audit mencatat cancel patient admission dilakukan secara manual, User ID yang melakukan, device dimana, kapan, dan data apa yang dimasukkan.
---
## Use Case Change Attending Doctor
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03c |
| **Use Case Name** | Change Attending Doctor |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mengganti dokter yang bertanggung jawab atas pengobatan pasien (DPJP). |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah memiliki data Attending Doctor (`patvisitadt.AttDoc`). |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengganti Attending Doctor.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A54` (Change Attending Doctor), mengkonfirmasi penggantian dokter berhasil dan menampilkan data admission yang telah diperbarui.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Penggantian Attending Doctor di System sehingga bisa dilakukan pelacakan Attending Doctor sekarang dan sebelumnya.
* Audit mencatat User ID yang melakukan perubahan Attending Doctor, device dimana perubahan dilakukan, kapan.
---
## Use Case Change Consulting Doctor
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03d |
| **Use Case Name** | Change Consulting Doctor |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mengganti dokter konsulen. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah memiliki data Consulting Doctor (`patvisitadt.CnsDoc`). |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengganti Consulting Doctor.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A61` (Change Consulting Doctor), mengkonfirmasi penggantian dokter berhasil dan menampilkan data admission yang telah diperbarui.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Penggantian Consulting Doctor di System sehingga bisa dilakukan pelacakan Consulting Doctor sekarang dan sebelumnya.
* Audit mencatat User ID yang melakukan perubahan Consulting Doctor, device dimana perubahan dilakukan, kapan.
---
## Use Case Patient Transfer
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-04 |
| **Use Case Name** | Patient Transfer |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Memindahkan pasien dari satu lokasi ke lokasi lainnya. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah memiliki data Location ID (`patvisitadt.LocationID`). |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Transfer.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengganti Location ID.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A02` (Patient Transfer), mengkonfirmasi perpindahan lokasi berhasil dan menampilkan data admission yang telah diperbarui.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Penggantian Location ID di System sehingga bisa dilakukan pelacakan Location ID sekarang dan sebelumnya.
* Audit mencatat User ID yang melakukan perubahan Location ID, device dimana perubahan dilakukan dan kapan.
---
## Use Case Patient Discharge
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-04a |
| **Use Case Name** | Patient Discharge |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mengakhiri kunjungan pasien. Close billing. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Discharge.
2. Aktor memasukkan PVID.
3. System memeriksa apakah PVID tersebut memiliki test order.
4. System memeriksa `orderstatus.OrderStatus` dari test order tsb.
5. System menampilkan data admission pasien.
6. Aktor mengisikan tanggal discharge.
7. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A03` (Discharge), mengkonfirmasi discharge/end visit berhasil dan menampilkan data admission yang telah di-discharge.
### Alur Alternatif
-
### Alur Pengecualian
* **Open test order:**
* System menolak discharge, jika menemukan `orderstatus.OrderStatus` bernilai ”A” atau “IP” atau “SC” atau “HD”.
* Aktor diminta untuk menyelesaikan test order terkait.
### Kondisi Akhir
* Discharge visit di System.
* Audit mencatat User ID yang melakukan discharge, device dimana discharge dilakukan dan kapan.
* Semua record terkait visit tersebut tidak bisa diedit/update lagi data-data pada `patvisit`, `patdiag`, `patvisitbill`. Hal-hal berikut tidak bisa dilakukan lagi:
- Perpindahan lokasi dan/atau dokter.
- Test order.
- Billing is closed.
* **Cancel discharge:**
- Bisa dilakukan atas instruksi dari HIS, misalnya berupa ADT message.
- Oleh orang tertentu saja di lab.
- Tidak meng-update existing record tetapi men-trigger tambahan `patvisitadt` record dengan Code: A13 (cancel discharge).
---
## Use Case Cancel Discharge
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-04b |
| **Use Case Name** | Cancel Patient Discharge |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Membatalkan Patient Discharge. Open billing. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah discharge. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Cancel Patient Discharge.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor membatalkan discharge dengan menekan tombol Cancel Discharge.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A13` (Cancel Discharge), mengkonfirmasi cancel discharge berhasil dan menampilkan data admission yang telah dibatalkan discharge-nya.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Pembatalan discharge di System.
* Audit mencatat cancel discharge dilakukan secara manual, User ID yang melakukan, device dimana activity dilakukan dan kapan.
* Semua record terkait visit tersebut kembali bisa diedit/update lagi data-data pada `patvisit`, `patdiag`, `patvisitbill`. Hal-hal berikut bisa dilakukan lagi:
- Perpindahan lokasi dan/atau dokter.
- Test order.
- Billing is re-open.
---
## Use Case Test Ordering
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-5a |
| **Use Case Name** | Test Ordering |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Membuat test order untuk pasien. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID. |
### Alur Utama
1. Aktor membuka halaman Test Ordering New Test Order.
2. Aktor memasukkan PVID.
3. System menampilkan data demografi, daftar PVID pasien berikut daftar OrderID (OID) yang telah dibuat sebelumnya (menghindari test order berlebihan).
4. Aktor bisa menambahkan komentar dan/atau lampiran ke test order.
5. Aktor memilih test yang diperlukan.
6. Aktor bisa memilih mencetak labels segera setelah klik tombol Save, dengan mencentang Print Patient Label, Print Order Label, Print Specimen Label check boxes.
7. Aktor menyimpan test order dengan menekan tombol Save.
8. System secara otomatis memberi OID.
9. System otomatis membuat records di table specimens.
10. System, mengkonfirmasi test ordering berhasil dan menampilkan data test order berikut daftar SID.
### Alur Alternatif
* **PVID belum ada:**
1. System mengarahkan Aktor ke halaman Patient Visit Management Patient Admission.
2. Aktor melakukan activity patient admission.
3. Aktir kembali ke test ordering.
* **Test ordering menggunakan PID:**
1. Aktor membuka halaman Test Ordering New Test Order.
2. Aktor memasukkan PID.
3. System menampilkan daftar PVID yang belum discharge.
4. Aktor memilih salah satu PVID dan melanjutkan activity test ordering.
* **Future Order:**
1. Aktor mengisi Effective Date (`ordertest.EffDate`) untuk menjadwalkan kapan test order mulai dikerjakan.
2. Aktor menyimpan test order dengan menekan tombol Save.
3. System memberikan OID yang sesuai dengan Effective Date.
* **OID sudah ada:**
1. Aktor membuka halaman Test Ordering Test Order Search & Update.
2. Aktor memasukan OID.
3. System menampilkan data-data test order.
4. Aktor melakukan update dan menyimpannya.
* **Non patient option** (Not detailed in text).
### Alur Pengecualian
-
### Kondisi Akhir
* Test order terbentuk di System ditandai dengan adanya OID dengan status (`orderstatus.OrderStatus`) ”SC” (In process, scheduled).
* SID terbentuk dan specimen label bisa dicetak atau tercetak otomatis.
* Audit mencatat test order dilakukan secara manual, User ID yang melakukan, device dimana activity dilakukan dan kapan.
---
## Use Case Update Test Order
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-5b |
| **Use Case Name** | Update Test Order |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Memperbarui test order untuk pasien. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Test order tersedia di System, ditandai dengan adanya OID. |
### Alur Utama
1. Aktor membuka halaman Test Ordering Test Order Search & Update.
2. Aktor memasukkan OID.
3. System menampilkan data-data test order.
4. Aktor melakukan update dan menyimpannya.
5. System, mengkonfirmasi update test order berhasil dan menampilkan data test order berikut daftar SID.
### Alur Alternatif
* **PVID belum ada:**
1. System mengarahkan Aktor ke halaman Patient Visit Management Patient Admission.
2. Aktor melakukan activity patient admission.
3. Aktir kembali ke test ordering.
* **Non patient option** (Not detailed in text).
### Alur Pengecualian
* **Test order tidak ada:**
* System menolak melakukan update dan menampilkan pesan,” Test order does not exists”.
* **Test order berstatus closed:** Ditandai dengan `ordertest.EndDate` memiliki value dan `orderstatus.OrderStatus` bernilai CL (Closed).
* System menolak melakukan update dan menampilkan pesan,” This test order is inaccessible”.
* **Test order berstatus archived:** Ditandai dengan `ordertest.ArchiveDate` memiliki value dan `orderstatus.OrderStatus` bernilai AC (Archived).
* System menolak melakukan update dan menampilkan pesan,” This test order is already archived”.
* **Test order berstatus deleted:** Ditandai dengan `ordertest.DelDate` memiliki value dan `orderstatus.OrderStatus` bernilai DL (Deleted).
* System menolak melakukan update dan menampilkan pesan,” This test order is already deleted”.
* **Update dilakukan dengan menghapus test yang telah ada hasilnya:**
* System menampilkan data-data test order.
* Aktor mengganti test yang telah ada hasilnya.
* System menolak melakukan update dan menampilkan pesan,” This test order is inaccessible”.
### Kondisi Akhir
* Test order terbentuk di System ditandai dengan adanya OID dengan status (`orderstatus.OrderStatus`) ”SC” (In process, scheduled).
* SID terbentuk dan specimen label bisa dicetak atau tercetak otomatis.
* Audit mencatat test order dilakukan secara manual, User ID yang melakukan, device dimana activity dilakukan dan kapan.
```

View File

@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-03-08

View File

@ -1,74 +0,0 @@
# Backend Implementation - Quick Reference
## Files to Create
1. **app/Controllers/User/UserController.php**
- Copy from: `code-templates/UserController.php`
- Creates: Full CRUD for users
2. **app/Models/User/UserModel.php**
- Copy from: `code-templates/UserModel.php`
- Creates: User database model
## Files to Modify
3. **app/Controllers/Specimen/SpecimenController.php**
- Add method from: `code-templates/SpecimenController-delete-method.php`
- Adds: Delete specimen functionality
4. **app/Config/Routes.php**
- Add routes from: `code-templates/Routes-additions.php`
- Adds: User routes + Specimen delete route
## Database Migration
Run this SQL if `users` table doesn't exist:
```sql
CREATE TABLE IF NOT EXISTS users (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Username VARCHAR(50) NOT NULL UNIQUE,
Email VARCHAR(100) NOT NULL,
Name VARCHAR(100),
Role VARCHAR(50),
Department VARCHAR(100),
IsActive BOOLEAN DEFAULT TRUE,
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
DelDate TIMESTAMP NULL,
INDEX idx_username (Username),
INDEX idx_email (Email)
);
```
## Testing Commands
```bash
# Specimen Delete
curl -X DELETE http://localhost:8000/api/specimen/1
# Users API
curl http://localhost:8000/api/users
curl http://localhost:8000/api/users/1
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-d '{"Username":"test","Email":"test@test.com","Name":"Test User"}'
curl -X PATCH http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-d '{"UserID":1,"Name":"Updated"}'
curl -X DELETE http://localhost:8000/api/users/1
```
## Timeline Estimate
- **Specimen Delete**: 15 minutes (just add route + method)
- **User API**: 1-2 hours (new controller + model + routes)
- **Testing**: 30 minutes
**Total: ~2-3 hours**
## Questions?
See the detailed specs:
- `specimen-delete.md` - Full specimen delete specification
- `user-api.md` - Complete user API specification with examples

View File

@ -1,55 +0,0 @@
# Backend API Requirements
This folder contains specifications for backend changes needed to support the frontend features.
## Quick Summary
| Feature | Status | Priority | Files to Create/Modify |
|---------|--------|----------|----------------------|
| Specimen Delete | Missing | High | Routes.php, SpecimenController.php |
| User CRUD | Not Implemented | High | UserController.php, UserModel.php, Routes.php |
## Directory Structure to Create
```
app/
├── Config/
│ └── Routes.php # MODIFY - Add new routes
├── Controllers/
│ └── Specimen/
│ └── SpecimenController.php # MODIFY - Add delete method
│ └── User/ # CREATE
│ └── UserController.php # CREATE - Full CRUD
├── Models/
│ └── User/ # CREATE
│ └── UserModel.php # CREATE - User database model
```
## Files
1. **specimen-delete.md** - Specimen delete endpoint specification
2. **user-api.md** - Complete User CRUD API specification
3. **code-templates/** - Ready-to-use code templates
- UserController.php
- UserModel.php
- Routes-additions.php
## Testing Checklist
After implementation, verify these endpoints work:
```bash
# Specimen Delete
curl -X DELETE http://localhost:8000/api/specimen/123
# User CRUD
curl http://localhost:8000/api/users
curl http://localhost:8000/api/users/1
curl -X POST http://localhost:8000/api/users -H "Content-Type: application/json" -d '{"username":"test","email":"test@test.com"}'
curl -X PATCH http://localhost:8000/api/users -H "Content-Type: application/json" -d '{"UserID":1,"username":"updated"}'
curl -X DELETE http://localhost:8000/api/users/1
```
## Questions?
Contact the frontend team if you need clarification on data structures or expected responses.

View File

@ -1,42 +0,0 @@
// ============================================================================
// ADD THESE ROUTES TO: app/Config/Routes.php
// ============================================================================
// Add this inside the '$routes->group('api', function ($routes) {' section
// Preferably after the Organization routes and before the Specimen routes
// Users Management
$routes->group('users', function ($routes) {
$routes->get('/', 'User\UserController::index');
$routes->get('(:num)', 'User\UserController::show/$1');
$routes->post('/', 'User\UserController::create');
$routes->patch('/', 'User\UserController::update');
$routes->delete('(:num)', 'User\UserController::delete/$1');
});
// ============================================================================
// SPECIMEN DELETE ROUTE
// Add this INSIDE the existing specimen group (around line 256-296)
// ============================================================================
$routes->group('specimen', function ($routes) {
// ... existing routes ...
// ADD THIS LINE:
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
// ... rest of existing routes ...
});
// ============================================================================
// COMPLETE EXAMPLE - Users route placement in context
// ============================================================================
// Master Data section (after Equipment, before Specimen)
$routes->group('users', function ($routes) {
$routes->get('/', 'User\UserController::index');
$routes->get('(:num)', 'User\UserController::show/$1');
$routes->post('/', 'User\UserController::create');
$routes->patch('/', 'User\UserController::update');
$routes->delete('(:num)', 'User\UserController::delete/$1');
});

View File

@ -1,49 +0,0 @@
// ============================================================================
// ADD THIS METHOD TO: app/Controllers/Specimen/SpecimenController.php
// ============================================================================
/**
* Delete a specimen (soft delete)
* DELETE /api/specimen/(:num)
*/
public function delete($id) {
try {
// Check if specimen exists
$specimen = $this->model->where('SID', $id)->first();
if (empty($specimen)) {
return $this->respond([
'status' => 'failed',
'message' => 'Specimen not found',
'data' => null
], 404);
}
// Perform soft delete (set DelDate)
$deleted = $this->model->update($id, [
'DelDate' => date('Y-m-d H:i:s')
]);
if (!$deleted) {
return $this->respond([
'status' => 'failed',
'message' => 'Failed to delete specimen',
'data' => null
], 500);
}
return $this->respond([
'status' => 'success',
'message' => 'Specimen deleted successfully',
'data' => ['SID' => $id]
], 200);
} catch (\Exception $e) {
log_message('error', 'SpecimenController::delete error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to delete specimen',
'data' => null
], 500);
}
}

View File

@ -1,290 +0,0 @@
<?php
namespace App\Controllers\User;
use App\Controllers\BaseController;
use App\Models\User\UserModel;
/**
* User Management Controller
* Handles CRUD operations for users
*/
class UserController extends BaseController
{
protected $model;
protected $db;
public function __construct()
{
$this->db = \Config\Database::connect();
$this->model = new UserModel();
}
/**
* List users with pagination and search
* GET /api/users?page=1&per_page=20&search=term
*/
public function index()
{
try {
$page = (int)($this->request->getGet('page') ?? 1);
$perPage = (int)($this->request->getGet('per_page') ?? 20);
$search = $this->request->getGet('search');
// Build query
$builder = $this->model->where('DelDate', null);
// Apply search if provided
if ($search) {
$builder->groupStart()
->like('Username', $search)
->orLike('Email', $search)
->orLike('Name', $search)
->groupEnd();
}
// Get total count for pagination
$total = $builder->countAllResults(false);
// Get paginated results
$users = $builder
->orderBy('UserID', 'DESC')
->limit($perPage, ($page - 1) * $perPage)
->findAll();
return $this->respond([
'status' => 'success',
'message' => 'Users retrieved successfully',
'data' => [
'users' => $users,
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => ceil($total / $perPage)
]
]
], 200);
} catch (\Exception $e) {
log_message('error', 'UserController::index error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to retrieve users',
'data' => null
], 500);
}
}
/**
* Get single user by ID
* GET /api/users/(:num)
*/
public function show($id)
{
try {
$user = $this->model->where('UserID', $id)
->where('DelDate', null)
->first();
if (empty($user)) {
return $this->respond([
'status' => 'failed',
'message' => 'User not found',
'data' => null
], 404);
}
return $this->respond([
'status' => 'success',
'message' => 'User retrieved successfully',
'data' => $user
], 200);
} catch (\Exception $e) {
log_message('error', 'UserController::show error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to retrieve user',
'data' => null
], 500);
}
}
/**
* Create new user
* POST /api/users
*/
public function create()
{
try {
$data = $this->request->getJSON(true);
// Validation rules
$rules = [
'Username' => 'required|min_length[3]|max_length[50]|is_unique[users.Username]',
'Email' => 'required|valid_email|is_unique[users.Email]',
];
if (!$this->validateData($data, $rules)) {
return $this->respond([
'status' => 'failed',
'message' => 'Validation failed',
'data' => $this->validator->getErrors()
], 400);
}
// Set default values
$data['IsActive'] = $data['IsActive'] ?? true;
$data['CreatedAt'] = date('Y-m-d H:i:s');
$userId = $this->model->insert($data);
if (!$userId) {
return $this->respond([
'status' => 'failed',
'message' => 'Failed to create user',
'data' => null
], 500);
}
return $this->respond([
'status' => 'success',
'message' => 'User created successfully',
'data' => [
'UserID' => $userId,
'Username' => $data['Username'],
'Email' => $data['Email']
]
], 201);
} catch (\Exception $e) {
log_message('error', 'UserController::create error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to create user',
'data' => null
], 500);
}
}
/**
* Update existing user
* PATCH /api/users
*/
public function update()
{
try {
$data = $this->request->getJSON(true);
if (empty($data['UserID'])) {
return $this->respond([
'status' => 'failed',
'message' => 'UserID is required',
'data' => null
], 400);
}
$userId = $data['UserID'];
// Check if user exists
$user = $this->model->where('UserID', $userId)
->where('DelDate', null)
->first();
if (empty($user)) {
return $this->respond([
'status' => 'failed',
'message' => 'User not found',
'data' => null
], 404);
}
// Remove UserID from data array
unset($data['UserID']);
// Don't allow updating these fields
unset($data['CreatedAt']);
unset($data['Username']); // Username should not change
$data['UpdatedAt'] = date('Y-m-d H:i:s');
$updated = $this->model->update($userId, $data);
if (!$updated) {
return $this->respond([
'status' => 'failed',
'message' => 'Failed to update user',
'data' => null
], 500);
}
return $this->respond([
'status' => 'success',
'message' => 'User updated successfully',
'data' => [
'UserID' => $userId,
'updated_fields' => array_keys($data)
]
], 200);
} catch (\Exception $e) {
log_message('error', 'UserController::update error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to update user',
'data' => null
], 500);
}
}
/**
* Delete user (soft delete)
* DELETE /api/users/(:num)
*/
public function delete($id)
{
try {
// Check if user exists
$user = $this->model->where('UserID', $id)
->where('DelDate', null)
->first();
if (empty($user)) {
return $this->respond([
'status' => 'failed',
'message' => 'User not found',
'data' => null
], 404);
}
// Soft delete by setting DelDate
$deleted = $this->model->update($id, [
'DelDate' => date('Y-m-d H:i:s'),
'IsActive' => false
]);
if (!$deleted) {
return $this->respond([
'status' => 'failed',
'message' => 'Failed to delete user',
'data' => null
], 500);
}
return $this->respond([
'status' => 'success',
'message' => 'User deleted successfully',
'data' => ['UserID' => $id]
], 200);
} catch (\Exception $e) {
log_message('error', 'UserController::delete error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to delete user',
'data' => null
], 500);
}
}
}

View File

@ -1,138 +0,0 @@
<?php
namespace App\Models\User;
use CodeIgniter\Model;
/**
* User Model
* Handles database operations for users
*/
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'UserID';
// Allow all fields to be mass-assigned
protected $allowedFields = [
'Username',
'Email',
'Name',
'Role',
'Department',
'IsActive',
'CreatedAt',
'UpdatedAt',
'DelDate'
];
// Use timestamps (disabled, we handle manually for consistency)
protected $useTimestamps = false;
// Validation rules
protected $validationRules = [
'Username' => 'required|min_length[3]|max_length[50]',
'Email' => 'required|valid_email|max_length[100]',
];
protected $validationMessages = [
'Username' => [
'required' => 'Username is required',
'min_length' => 'Username must be at least 3 characters',
'max_length' => 'Username cannot exceed 50 characters',
],
'Email' => [
'required' => 'Email is required',
'valid_email' => 'Please provide a valid email address',
'max_length' => 'Email cannot exceed 100 characters',
],
];
/**
* Get active users only
*/
public function getActive()
{
return $this->where('DelDate', null)
->where('IsActive', true)
->findAll();
}
/**
* Find user by username
*/
public function findByUsername($username)
{
return $this->where('Username', $username)
->where('DelDate', null)
->first();
}
/**
* Find user by email
*/
public function findByEmail($email)
{
return $this->where('Email', $email)
->where('DelDate', null)
->first();
}
/**
* Search users by name, username, or email
*/
public function search($term)
{
return $this->where('DelDate', null)
->groupStart()
->like('Username', $term)
->orLike('Email', $term)
->orLike('Name', $term)
->groupEnd()
->findAll();
}
/**
* Get users by role
*/
public function getByRole($role)
{
return $this->where('Role', $role)
->where('DelDate', null)
->where('IsActive', true)
->findAll();
}
/**
* Get users by department
*/
public function getByDepartment($department)
{
return $this->where('Department', $department)
->where('DelDate', null)
->where('IsActive', true)
->findAll();
}
/**
* Soft delete user
*/
public function softDelete($id)
{
return $this->update($id, [
'DelDate' => date('Y-m-d H:i:s'),
'IsActive' => false
]);
}
/**
* Restore soft-deleted user
*/
public function restore($id)
{
return $this->update($id, [
'DelDate' => null,
'IsActive' => true
]);
}
}

View File

@ -1,165 +0,0 @@
## Context
The CLQMS frontend currently has several incomplete features marked with TODO comments:
1. **Patient Delete**: The `deletePatient()` API exists but isn't wired up in the UI
2. **Order Detail**: Orders can only be viewed as a list item, with no detail modal
3. **Barcode Printing**: Referenced in UI but not implemented
4. **TestMap Delete**: Blocked by API limitation (missing TestMapID in list response)
Additionally, the sidebar links to two pages that don't exist:
- **Specimens**: Critical for complete lab workflow (patient → visit → order → specimen → results)
- **Users**: Essential for system administration
This change addresses all these gaps to provide a complete laboratory management experience.
## Goals / Non-Goals
**Goals:**
- Wire up patient delete functionality
- Create order detail view modal
- Implement specimen management page with full CRUD
- Implement user management page with full CRUD
- Prepare barcode printing infrastructure
- Document TestMap delete limitation
**Non-Goals:**
- Full barcode generation library integration (out of scope, preparing structure only)
- Backend API changes for TestMap (document limitation only)
- Role-based access control (reuse existing auth patterns)
- Advanced specimen tracking (batch, location) - basic CRUD only
## Backend API Requirements
### Specimen API (clqms01-be)
**Current Status:** CRU exists, missing DELETE
Required additions:
```php
// Add to app/Config/Routes.php
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
// Add to SpecimenController
public function delete($id) {
// Soft delete specimen
}
```
### User API (clqms01-be)
**Current Status:** Not implemented
Required new files:
```
app/
├── Controllers/
│ └── User/
│ └── UserController.php # CRUD operations
├── Models/
│ └── User/
│ └── UserModel.php # Database model
└── Config/Routes.php # Add user routes
```
Required endpoints:
- `GET /api/users` - List with pagination
- `GET /api/users/(:num)` - Get single user
- `POST /api/users` - Create user
- `PATCH /api/users` - Update user
- `DELETE /api/users/(:num)` - Delete user
## Decisions
### Decision: Use Existing Patterns
**Rationale**: Consistency with existing codebase reduces cognitive load and maintenance burden.
All new pages follow existing patterns:
- Search/filter pattern from Patients page
- Modal forms from Test Management
- API structure from existing `/lib/api/*.js` files
- DaisyUI + Tailwind styling consistent with current UI
### Decision: Barcode Printing - Stub First
**Rationale**: Barcode libraries (jsbarcode, qrcode) add dependencies. Better to prepare structure first, then integrate library in follow-up.
Current implementation:
- Print button triggers stub function
- Opens print dialog with formatted HTML
- Placeholder text indicates "coming soon"
Future enhancement:
- Add jsbarcode or similar library
- Generate actual barcodes
- Keep same print dialog structure
### Decision: Specimen Page Structure
**Rationale**: Specimens bridge orders and results - need to show relationships.
Page layout:
- Search/filter bar (specimen type, status, date)
- Data table with key columns
- Detail modal showing:
- Specimen attributes
- Related order info
- Collection details
- Container information
### Decision: User Page - Basic CRUD Only
**Rationale**: User management is admin function, not daily use. Complex features (permissions matrix, audit logs) can be added later.
Features:
- List users with search
- Create/edit user form
- Soft delete (disable) users
- Prevent self-deletion
Not included:
- Permission matrix UI
- User activity audit
- Password reset flow (use existing)
## Risks / Trade-offs
**[Risk] API endpoints may not exist for specimens/users**
**Status**: CONFIRMED - Specimen delete missing, User API not implemented
**Mitigation**: Coordinate with backend team to add endpoints before frontend delete features
**[Risk] TestMap delete limitation blocks feature**
**Mitigation**: Document limitation clearly, add informative error message, suggest backend improvement
**[Risk] Barcode printing may not work in all browsers**
**Mitigation**: Use standard window.print(), test in target environments, provide fallback
**[Risk] User delete without RBAC checks**
**Mitigation**: Add client-side check to prevent self-deletion, rely on backend for authorization
## Migration Plan
**Deployment**:
1. Deploy frontend changes
2. Verify new pages load correctly
3. Test patient delete on staging data
4. Verify sidebar navigation works
**Rollback**:
- Revert to previous commit
- No database changes required (frontend only)
## Open Questions
1. **What fields are available for specimens?**
Need to review SpecimenModel for data structure
2. **Are there existing barcode requirements?**
What format? Code128? QR codes? What data to encode?
3. **Should users have soft delete or hard delete?**
Check if backend supports soft delete (status field)
## Backend Coordination Checklist
- [ ] Backend: Add Specimen delete endpoint
- [ ] Backend: Create UserController
- [ ] Backend: Create UserModel
- [ ] Backend: Add user routes to Routes.php
- [ ] Frontend: Implement specimen delete once API ready
- [ ] Frontend: Implement users page once API ready

View File

@ -1,63 +0,0 @@
## Why
The CLQMS frontend has several incomplete features marked with TODO comments that block core workflows. Additionally, the sidebar links to Specimens and Users pages that don't exist yet. Completing these TODOs and adding the missing pages will provide a fully functional laboratory management system with complete patient-to-results tracking and user administration capabilities.
## What Changes
### Backend TODOs (F)
- **Patient Delete**: Wire up existing `deletePatient()` API (currently commented out)
- **Order Detail View**: Create modal component to display order details, test list, and status history
- **Barcode Printing**: Add print stubs and structure for patient wristbands and specimen labels
- **TestMap Delete**: Document API limitation and add error handling for missing TestMapID
### New Pages
- **Specimens Page** (A): Full CRUD page for specimen tracking with search, list, and detail views
- **Users Page** (B): User management interface with CRUD operations
### Backend Requirements (Coordinate with clqms01-be)
- **Specimen Delete API**: Add `DELETE /api/specimen/(:num)` endpoint to backend
- **User Management API**: Create complete CRUD API for user management:
- `GET /api/users` - List users
- `GET /api/users/(:num)` - Get user details
- `POST /api/users` - Create user
- `PATCH /api/users` - Update user
- `DELETE /api/users/(:num)` - Delete user
### Minor Improvements
- Update sidebar navigation to point to new pages
- Add proper error handling for incomplete features
## Capabilities
### New Capabilities
- `specimen-management`: Specimen tracking and management interface
- `user-management`: User administration and role management
- `barcode-printing`: Barcode generation and printing for patients and specimens
- `order-detail-view`: Detailed order inspection with test breakdown
### Modified Capabilities
- `patient-management`: Add delete functionality to complete CRUD operations
## Impact
- **Frontend**: 3 new route pages, 2 new modal components, API integrations
- **Backend**: New endpoints for specimen delete and user CRUD operations
- **User Experience**: Completes core lab workflow from patient registration to results
- **Breaking Changes**: None - additive changes only
## Dependencies
**Backend Changes Required (clqms01-be repo):**
| Feature | Backend Work | Status |
|---------|--------------|--------|
| Patient Delete | ✅ Already exists | Ready |
| Specimens | ⚠️ Add DELETE endpoint | **BLOCKS specimen delete** |
| Users | ❌ Create full CRUD API | **BLOCKS users page** |
| Order Detail | ✅ Already exists | Ready |
**Coordination Strategy:**
1. Backend team creates specimen delete endpoint
2. Backend team creates user management API
3. Frontend implements features as APIs become available
4. Parallel development possible for non-dependent features

View File

@ -1,119 +0,0 @@
# Specimen Delete Endpoint
## Overview
Add DELETE endpoint for specimens to complete CRUD operations.
## Current State
- GET /api/specimen - List specimens ✅
- GET /api/specimen/(:num) - Get single specimen ✅
- POST /api/specimen - Create specimen ✅
- PATCH /api/specimen - Update specimen ✅
- DELETE /api/specimen/(:num) - **MISSING**
## Requirements
### Route
Add to `app/Config/Routes.php` in the specimen group:
```php
$routes->group('specimen', function ($routes) {
// ... existing routes ...
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
});
```
### Controller Method
Add to `app/Controllers/Specimen/SpecimenController.php`:
```php
/**
* Delete a specimen (soft delete)
* DELETE /api/specimen/(:num)
*/
public function delete($id) {
try {
// Check if specimen exists
$specimen = $this->model->where('SID', $id)->first();
if (empty($specimen)) {
return $this->respond([
'status' => 'failed',
'message' => 'Specimen not found',
'data' => null
], 404);
}
// Perform soft delete (set DelDate)
$deleted = $this->model->update($id, [
'DelDate' => date('Y-m-d H:i:s')
]);
if (!$deleted) {
return $this->respond([
'status' => 'failed',
'message' => 'Failed to delete specimen',
'data' => null
], 500);
}
return $this->respond([
'status' => 'success',
'message' => 'Specimen deleted successfully',
'data' => ['SID' => $id]
], 200);
} catch (\Exception $e) {
log_message('error', 'SpecimenController::delete error: ' . $e->getMessage());
return $this->respond([
'status' => 'failed',
'message' => 'Failed to delete specimen',
'data' => null
], 500);
}
}
```
## Expected Request/Response
### Request
```http
DELETE /api/specimen/123 HTTP/1.1
```
### Success Response (200)
```json
{
"status": "success",
"message": "Specimen deleted successfully",
"data": {
"SID": 123
}
}
```
### Not Found Response (404)
```json
{
"status": "failed",
"message": "Specimen not found",
"data": null
}
```
### Error Response (500)
```json
{
"status": "failed",
"message": "Failed to delete specimen",
"data": null
}
```
## Implementation Notes
1. **Use soft delete**: Set `DelDate` field instead of hard delete
2. **Check existence first**: Return 404 if specimen doesn't exist
3. **Use SID as identifier**: The specimen ID field is `SID`, not `id`
4. **Follow existing patterns**: Match style of other delete methods in codebase

View File

@ -1,92 +0,0 @@
## 0. Backend Coordination (clqms01-be repo)
**These backend changes must be completed before dependent frontend tasks:**
- [x] 0.1 Add Specimen delete endpoint to Routes.php
- [x] 0.2 Implement SpecimenController::delete() method
- [x] 0.3 Create UserController with CRUD methods
- [x] 0.4 Create UserModel for database operations
- [x] 0.5 Add user routes to Routes.php
- [x] 0.6 Test backend endpoints with Postman/curl
## 1. Backend TODOs - Patient Delete
- [ ] 1.1 Import deletePatient function in patients/+page.svelte
- [ ] 1.2 Uncomment delete API call in handleDelete function
- [ ] 1.3 Add error handling for delete operation
- [ ] 1.4 Test patient delete functionality
## 2. Backend TODOs - Order Detail View
- [ ] 2.1 Create OrderDetailModal.svelte component
- [ ] 2.2 Add order header display (number, date, status, patient)
- [ ] 2.3 Add test list display with status indicators
- [ ] 2.4 Add status history timeline
- [ ] 2.5 Wire up modal to order list click handler
- [ ] 2.6 Test order detail modal
## 3. Backend TODOs - Barcode Printing Structure
- [ ] 3.1 Create barcode.js utility file with print functions
- [ ] 3.2 Add printPatientWristband stub function
- [ ] 3.3 Add printSpecimenLabel stub function
- [ ] 3.4 Wire up barcode printing to patient order list
- [ ] 3.5 Wire up barcode printing to orders page
- [ ] 3.6 Test print dialog opens correctly
## 4. Backend TODOs - TestMap Delete Documentation
- [ ] 4.1 Add informative error message for TestMap delete limitation
- [ ] 4.2 Document API limitation in code comments
- [ ] 4.3 Add console warning with explanation
## 5. Specimens Page
- [ ] 5.1 Create specimens API client file (src/lib/api/specimens.js)
- [ ] 5.2 Create specimens/+page.svelte route
- [ ] 5.3 Add specimen search/filter bar
- [ ] 5.4 Add specimen data table with columns
- [ ] 5.5 Create SpecimenFormModal component
- [ ] 5.6 Implement specimen creation
- [ ] 5.7 Implement specimen editing
- [ ] 5.8 Implement specimen deletion **[BLOCKED: Requires task 0.1-0.2]**
- [ ] 5.9 Create SpecimenDetailModal component
- [ ] 5.10 Wire up detail view
- [ ] 5.11 Test full specimen CRUD workflow
## 6. Users Page
- [ ] 6.1 Create users API client file (src/lib/api/users.js)
- [ ] 6.2 Create master-data/users/+page.svelte route **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.3 Add user search functionality **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.4 Add user data table **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.5 Create UserFormModal component **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.6 Implement user creation **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.7 Implement user editing **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.8 Implement user deletion with self-delete prevention **[BLOCKED: Requires task 0.3-0.5]**
- [ ] 6.9 Test user management workflow **[BLOCKED: Requires task 0.3-0.5]**
## 7. Navigation Updates
- [ ] 7.1 Verify specimens link in sidebar works
- [ ] 7.2 Verify users link in sidebar works **[BLOCKED: Requires task 6.x]**
- [ ] 7.3 Update any broken navigation paths
- [ ] 7.4 Test all navigation flows **[PARTIALLY BLOCKED]**
## 8. Integration & Testing
- [ ] 8.1 Run development server
- [ ] 8.2 Test patient delete end-to-end
- [ ] 8.3 Test order detail modal
- [ ] 8.4 Test barcode print dialogs
- [ ] 8.5 Test specimens page CRUD **[PARTIALLY BLOCKED: Delete pending backend]**
- [ ] 8.6 Test users page CRUD **[BLOCKED: Pending backend]**
- [ ] 8.7 Verify no console errors
- [ ] 8.8 Code review and cleanup
## 9. Deployment
- [ ] 9.1 Coordinate backend deployment
- [ ] 9.2 Deploy frontend changes
- [ ] 9.3 Verify all features in production
- [ ] 9.4 Update API documentation

View File

@ -1,243 +0,0 @@
# User Management API
## Overview
Create a complete User CRUD API for the user management page.
## Required Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/users | List all users (with pagination) |
| GET | /api/users/(:num) | Get single user by ID |
| POST | /api/users | Create new user |
| PATCH | /api/users | Update existing user |
| DELETE | /api/users/(:num) | Delete user |
## Database Schema
The User model should use the existing `users` table (or create if doesn't exist):
```sql
-- If table doesn't exist, create it:
CREATE TABLE IF NOT EXISTS users (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Username VARCHAR(50) NOT NULL UNIQUE,
Email VARCHAR(100) NOT NULL,
Name VARCHAR(100),
Role VARCHAR(50),
Department VARCHAR(100),
IsActive BOOLEAN DEFAULT TRUE,
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
DelDate TIMESTAMP NULL,
INDEX idx_username (Username),
INDEX idx_email (Email)
);
```
## User Fields
### Required Fields
- `Username` - Unique login username
- `Email` - User email address
### Optional Fields
- `Name` - Full name
- `Role` - User role (admin, technician, doctor, etc.)
- `Department` - Department name
- `IsActive` - Whether user is active
## API Specifications
### 1. List Users
**Request:**
```http
GET /api/users?page=1&per_page=20&search=john HTTP/1.1
```
**Query Parameters:**
- `page` - Page number (default: 1)
- `per_page` - Items per page (default: 20)
- `search` - Search term for username/email/name (optional)
**Success Response (200):**
```json
{
"status": "success",
"message": "Users retrieved successfully",
"data": {
"users": [
{
"UserID": 1,
"Username": "john.doe",
"Email": "john@hospital.com",
"Name": "John Doe",
"Role": "technician",
"Department": "Laboratory",
"IsActive": true,
"CreatedAt": "2024-01-15 10:30:00"
}
],
"pagination": {
"current_page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
}
```
### 2. Get Single User
**Request:**
```http
GET /api/users/1 HTTP/1.1
```
**Success Response (200):**
```json
{
"status": "success",
"message": "User retrieved successfully",
"data": {
"UserID": 1,
"Username": "john.doe",
"Email": "john@hospital.com",
"Name": "John Doe",
"Role": "technician",
"Department": "Laboratory",
"IsActive": true,
"CreatedAt": "2024-01-15 10:30:00",
"UpdatedAt": "2024-01-15 10:30:00"
}
}
```
**Not Found Response (404):**
```json
{
"status": "failed",
"message": "User not found",
"data": null
}
```
### 3. Create User
**Request:**
```http
POST /api/users HTTP/1.1
Content-Type: application/json
{
"Username": "jane.smith",
"Email": "jane@hospital.com",
"Name": "Jane Smith",
"Role": "doctor",
"Department": "Pathology"
}
```
**Success Response (201):**
```json
{
"status": "success",
"message": "User created successfully",
"data": {
"UserID": 2,
"Username": "jane.smith",
"Email": "jane@hospital.com"
}
}
```
**Validation Error Response (400):**
```json
{
"status": "failed",
"message": "Validation failed",
"data": {
"Username": "Username is required",
"Email": "Email is required"
}
}
```
### 4. Update User
**Request:**
```http
PATCH /api/users HTTP/1.1
Content-Type: application/json
{
"UserID": 1,
"Name": "John Doe Updated",
"Role": "senior_technician"
}
```
**Success Response (200):**
```json
{
"status": "success",
"message": "User updated successfully",
"data": {
"UserID": 1,
"Name": "John Doe Updated",
"Role": "senior_technician"
}
}
```
### 5. Delete User
**Request:**
```http
DELETE /api/users/1 HTTP/1.1
```
**Success Response (200):**
```json
{
"status": "success",
"message": "User deleted successfully",
"data": {
"UserID": 1
}
}
```
## Implementation Files
See `code-templates/` folder for ready-to-use code:
- `UserController.php` - Complete controller implementation
- `UserModel.php` - Database model
- `Routes-additions.php` - Routes to add to Routes.php
## Testing
After implementation, test with:
```bash
# List users
curl http://localhost:8000/api/users
# Get single user
curl http://localhost:8000/api/users/1
# Create user
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-d '{"Username":"test","Email":"test@test.com","Name":"Test User"}'
# Update user
curl -X PATCH http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-d '{"UserID":1,"Name":"Updated Name"}'
# Delete user
curl -X DELETE http://localhost:8000/api/users/1
```

View File

@ -1,20 +0,0 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours

View File

@ -5904,6 +5904,34 @@ components:
description: Group members (only for GROUP type) description: Group members (only for GROUP type)
items: items:
type: object type: object
properties:
TestGrpID:
type: integer
description: Group membership record ID
TestSiteID:
type: integer
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)
TestSiteCode:
type: string
description: Member test code
TestSiteName:
type: string
description: Member test name
TestType:
type: string
description: Member test type
CreateDate:
type: string
format: date-time
EndDate:
type: string
format: date-time
testmap: testmap:
type: array type: array
description: Test mappings description: Test mappings
@ -6163,13 +6191,19 @@ components:
CountStat: 1 CountStat: 1
testdefgrp: testdefgrp:
- TestGrpID: 1 - TestGrpID: 1
TestSiteID: 6
Member: 100 Member: 100
MemberTestSiteID: 100
TestSiteCode: CHOL TestSiteCode: CHOL
TestSiteName: Total Cholesterol TestSiteName: Total Cholesterol
TestType: TEST
- TestGrpID: 2 - TestGrpID: 2
TestSiteID: 6
Member: 101 Member: 101
MemberTestSiteID: 101
TestSiteCode: TG TestSiteCode: TG
TestSiteName: Triglycerides TestSiteName: Triglycerides
TestType: TEST
TITLE: TITLE:
summary: Section header summary: Section header
value: value:

View File

@ -147,6 +147,34 @@ TestDefinition:
description: Group members (only for GROUP type) description: Group members (only for GROUP type)
items: items:
type: object type: object
properties:
TestGrpID:
type: integer
description: Group membership record ID
TestSiteID:
type: integer
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)
TestSiteCode:
type: string
description: Member test code
TestSiteName:
type: string
description: Member test name
TestType:
type: string
description: Member test type
CreateDate:
type: string
format: date-time
EndDate:
type: string
format: date-time
testmap: testmap:
type: array type: array
description: Test mappings description: Test mappings
@ -402,13 +430,19 @@ TestDefinition:
CountStat: 1 CountStat: 1
testdefgrp: testdefgrp:
- TestGrpID: 1 - TestGrpID: 1
TestSiteID: 6
Member: 100 Member: 100
MemberTestSiteID: 100
TestSiteCode: CHOL TestSiteCode: CHOL
TestSiteName: Total Cholesterol TestSiteName: Total Cholesterol
TestType: TEST
- TestGrpID: 2 - TestGrpID: 2
TestSiteID: 6
Member: 101 Member: 101
MemberTestSiteID: 101
TestSiteCode: TG TestSiteCode: TG
TestSiteName: Triglycerides TestSiteName: Triglycerides
TestType: TEST
TITLE: TITLE:
summary: Section header summary: Section header
value: value: