feat: add OpenSpec workflow, Serena integration, User API, and Specimen delete endpoint

- Add OpenSpec experimental workflow with commands (opsx-apply, opsx-archive, opsx-explore, opsx-propose)
- Add Serena memory system for project context
- Implement User API (UserController, UserModel, routes)
- Add Specimen delete endpoint
- Update Test definitions and Routes
- Sync API documentation (OpenAPI)
- Archive completed 2026-03-08-backend-specs change
This commit is contained in:
mahdahar 2026-03-09 07:00:12 +07:00
parent 85c7e96405
commit 282c642da6
47 changed files with 6280 additions and 375 deletions

2
.gitignore vendored
View File

@ -125,5 +125,3 @@ _modules/*
/results/
/phpunit*.xml
/public/.htaccess
/.serena

View File

@ -0,0 +1,149 @@
---
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

@ -0,0 +1,154 @@
---
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

@ -0,0 +1,170 @@
---
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

@ -0,0 +1,103 @@
---
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

@ -0,0 +1,156 @@
---
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

@ -0,0 +1,114 @@
---
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

@ -0,0 +1,288 @@
---
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

@ -0,0 +1,110 @@
---
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

1
.serena/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/cache

View File

@ -0,0 +1,419 @@
# CLQMS Architecture & Codebase Structure
## High-Level Architecture
CLQMS follows a **clean architecture pattern** with clear separation of concerns:
```
┌─────────────────────────────────────────────────────────────┐
│ API Consumers │
│ (Web Apps, Mobile Apps, Desktop Clients, Instruments) │
└────────────────────┬────────────────────────────────────────┘
│ HTTP/HTTPS (JSON)
┌────────────────────┴────────────────────────────────────────┐
│ REST API Layer │
│ (Controllers: Patient, Order, Specimen, Result, etc.) │
│ - JWT Authentication Filter │
│ - Request Validation │
│ - Response Formatting │
└────────────────────┬────────────────────────────────────────┘
┌────────────────────┴────────────────────────────────────────┐
│ Business Logic Layer │
│ (Models + Libraries + Services) │
│ - ValueSet Library (JSON-based lookups) │
│ - Base Model (UTC normalization) │
│ - Edge Processing Service │
└────────────────────┬────────────────────────────────────────┘
┌────────────────────┴────────────────────────────────────────┐
│ Data Access Layer │
│ (CodeIgniter Query Builder) │
└────────────────────┬────────────────────────────────────────┘
┌────────────────────┴────────────────────────────────────────┐
│ MySQL Database │
│ (Migration-managed schema) │
└─────────────────────────────────────────────────────────────┘
```
## Directory Structure Overview
### Root Directory Files
```
clqms01-be/
├── .env # Environment configuration
├── .gitignore # Git ignore rules
├── AGENTS.md # AI agent instructions (THIS FILE)
├── README.md # Project documentation
├── PRD.md # Product Requirements Document
├── TODO.md # Implementation tasks
├── USER_STORIES.md # User stories
├── composer.json # PHP dependencies
├── composer.lock # Locked dependency versions
├── phpunit.xml.dist # PHPUnit configuration
├── spark # CodeIgniter CLI tool
└── preload.php # PHP preloader
```
### app/ - Application Core
```
app/
├── Controllers/ # API endpoint handlers
│ ├── BaseController.php # Base controller class
│ ├── AuthController.php # Authentication endpoints
│ ├── AuthV2Controller.php # V2 auth endpoints
│ ├── DashboardController.php # Dashboard data
│ ├── EdgeController.php # Instrument integration
│ ├── Patient/ # Patient management
│ │ └── PatientController.php
│ ├── Organization/ # Organization structure
│ │ ├── AccountController.php
│ │ ├── SiteController.php
│ │ ├── DisciplineController.php
│ │ ├── DepartmentController.php
│ │ └── WorkstationController.php
│ ├── Specimen/ # Specimen management
│ │ ├── SpecimenController.php
│ │ ├── SpecimenCollectionController.php
│ │ ├── SpecimenPrepController.php
│ │ ├── SpecimenStatusController.php
│ │ └── ContainerDefController.php
│ ├── OrderTest/ # Order management
│ │ └── OrderTestController.php
│ ├── Result/ # Result management
│ │ └── ResultController.php
│ ├── Test/ # Test definitions
│ │ └── TestsController.php
│ ├── Contact/ # Contact management
│ │ ├── ContactController.php
│ │ ├── OccupationController.php
│ │ └── MedicalSpecialtyController.php
│ ├── ValueSetController.php # ValueSet API endpoints
│ ├── ValueSetDefController.php # ValueSet definitions
│ ├── LocationController.php # Location management
│ ├── CounterController.php # Counter management
│ ├── PatVisitController.php # Patient visit management
│ └── SampleController.php # Sample management
├── Models/ # Data access layer
│ ├── BaseModel.php # Base model with UTC normalization
│ ├── Patient/ # Patient models
│ │ ├── PatientModel.php
│ │ ├── PatAttModel.php # Patient address
│ │ ├── PatComModel.php # Patient comments
│ │ └── PatIdtModel.php # Patient identifiers
│ ├── Organization/ # Organization models
│ │ ├── AccountModel.php
│ │ ├── SiteModel.php
│ │ ├── DisciplineModel.php
│ │ ├── DepartmentModel.php
│ │ └── WorkstationModel.php
│ ├── Specimen/ # Specimen models
│ │ ├── SpecimenModel.php
│ │ ├── SpecimenCollectionModel.php
│ │ ├── SpecimenPrepModel.php
│ │ ├── SpecimenStatusModel.php
│ │ └── ContainerDefModel.php
│ ├── OrderTest/ # Order models
│ │ ├── OrderTestModel.php
│ │ ├── OrderTestDetModel.php
│ │ └── OrderTestMapModel.php
│ ├── Result/ # Result models
│ │ ├── PatResultModel.php
│ │ └── ResultValueSetModel.php
│ ├── Test/ # Test models
│ │ ├── TestDefSiteModel.php
│ │ ├── TestDefTechModel.php
│ │ ├── TestDefCalModel.php
│ │ ├── TestDefGrpModel.php
│ │ └── RefNumModel.php
│ ├── Contact/ # Contact models
│ │ ├── ContactModel.php
│ │ ├── OccupationModel.php
│ │ └── MedicalSpecialtyModel.php
│ ├── ValueSet/ # ValueSet models (DB-based)
│ │ └── ValueSetModel.php
│ ├── EdgeResModel.php # Edge results
│ ├── CounterModel.php # Counter management
│ ├── PatVisitModel.php # Patient visits
│ └── ...
├── Libraries/ # Reusable libraries
│ ├── ValueSet.php # JSON-based lookup system
│ └── Data/ # ValueSet JSON files
│ ├── valuesets/
│ │ ├── sex.json
│ │ ├── marital_status.json
│ │ ├── race.json
│ │ ├── order_priority.json
│ │ ├── order_status.json
│ │ ├── specimen_type.json
│ │ ├── specimen_status.json
│ │ ├── result_status.json
│ │ ├── test_type.json
│ │ └── ... (many more)
├── Database/ # Database operations
│ ├── Migrations/ # Database schema migrations
│ │ ├── Format: YYYY-MM-DD-NNNNNN_Description.php
│ │ ├── Define up() and down() methods
│ │ └── Use $this->forge methods
│ └── Seeds/ # Database seeders
├── Config/ # Configuration files
│ ├── App.php # App configuration
│ ├── Database.php # Database configuration
│ ├── Routes.php # API route definitions
│ ├── Filters.php # Request filters (auth, etc.)
│ └── ...
├── Filters/ # Request/response filters
│ └── AuthFilter.php # JWT authentication filter
└── Helpers/ # Helper functions
└── utc_helper.php # UTC date conversion helpers
```
### public/ - Public Web Root
```
public/
├── index.php # Front controller (entry point)
├── api-docs.yaml # OpenAPI/Swagger documentation (CRITICAL!)
├── docs.html # API documentation HTML
├── .htaccess # Apache rewrite rules
└── robots.txt # SEO robots file
```
### tests/ - Test Suite
```
tests/
├── feature/ # Integration/API tests
│ ├── ContactControllerTest.php
│ ├── OrganizationControllerTest.php
│ ├── TestsControllerTest.php
│ ├── UniformShowTest.php # Tests show endpoint format
│ └── Patients/
│ └── PatientCreateTest.php
├── unit/ # Unit tests
├── _support/ # Test support utilities
└── README.md # Test documentation
```
### vendor/ - Composer Dependencies
```
vendor/
├── codeigniter4/ # CodeIgniter framework
├── firebase/ # JWT library
├── phpunit/ # PHPUnit testing framework
└── ... # Other dependencies
```
### writable/ - Writable Directory
```
writable/
├── cache/ # Application cache
├── logs/ # Application logs
├── session/ # Session files
└── uploads/ # File uploads
```
## API Route Structure
Routes are defined in `app/Config/Routes.php`:
### Public Routes (No Authentication)
```php
/api/v2/auth/login # User login
/api/v2/auth/register # User registration
/api/demo/order # Create demo order
```
### Authenticated Routes (JWT Required)
```php
/api/patient # Patient CRUD
/api/patvisit # Patient visit CRUD
/api/organization/* # Organization management
/api/specimen/* # Specimen management
/api/ordertest # Order management
/api/tests # Test definitions
/api/valueset/* # ValueSet management
/api/result/* # Result management
```
### Edge API (Instrument Integration)
```php
POST /api/edge/results # Receive results
GET /api/edge/orders # Fetch pending orders
POST /api/edge/orders/:id/ack # Acknowledge order
POST /api/edge/status # Log instrument status
```
## Core Design Patterns
### 1. BaseController Pattern
All controllers extend `BaseController`:
- Provides access to `$this->request`, `$this->response`
- Uses `ResponseTrait` for JSON responses
- Centralizes common functionality
### 2. BaseModel Pattern
All models extend `BaseModel`:
- **UTC Date Normalization**: Automatically converts dates to UTC before insert/update
- **ISO 8601 Output**: Automatically converts dates to ISO format on retrieval
- **Soft Deletes**: Automatic soft delete support via `DelDate` field
- **Hooks**: Uses `beforeInsert`, `beforeUpdate`, `afterFind`, etc.
### 3. ValueSet Pattern
JSON-based static lookup system:
- Fast, cached lookups for static values
- Easy maintenance via JSON files
- Automatic label transformation for API responses
- Clear cache after updates
### 4. Controller-Model-Database Flow
```
HTTP Request
Controller (Validation, Auth)
Model (Business Logic, Data Access)
Database (MySQL via Query Builder)
Model (Transform, Add Labels)
Controller (Format Response)
JSON Response
```
## Key Integration Points
### 1. JWT Authentication
- Filter: `AuthFilter` in `app/Filters/`
- Middleware checks JWT token from Cookie header
- Routes grouped with `'filter' => 'auth'`
### 2. Edge API Integration
- Controller: `EdgeController.php`
- Models: `EdgeResModel`, `EdgeStatusModel`, `EdgeAckModel`
- Staging: `edgeres` table for raw results
- Processing: Auto or manual to `patresult` table
### 3. ValueSet Integration
- Library: `ValueSet.php` in `app/Libraries/`
- Data: JSON files in `app/Libraries/Data/valuesets/`
- Usage: `ValueSet::get('name')`, `ValueSet::transformLabels()`
- Cache: Application-level caching
### 4. UTC Date Handling
- Model: `BaseModel.php` handles conversion
- Helper: `utc_helper.php` provides conversion functions
- Normalization: Local → UTC before DB operations
- Output: UTC → ISO 8601 for API responses
## Database Schema Organization
### Transactional Tables
- `patient` - Patient registry
- `porder` - Laboratory orders
- `specimen` - Specimens
- `patresult` - Patient results
- `patresultdetail` - Result details
- `patvisit` - Patient visits
- `edgeres` - Raw instrument results
### Master Data Tables
- `valueset` - Value set values
- `valuesetdef` - Value set definitions
- `testdefsite` - Test definitions
- `testdeftech` - Technical specs
- `testdefcal` - Calculated tests
- `testdefgrp` - Test groups
- `refnum` - Numeric reference ranges
- `reftxt` - Text reference ranges
### Organization Tables
- `account` - Accounts
- `site` - Sites
- `discipline` - Disciplines
- `department` - Departments
- `workstation` - Workstations
### Integration Tables
- `edgestatus` - Instrument status
- `edgeack` - Order acknowledgment
- `testmap` - Instrument test mapping
## Important Architectural Decisions
### 1. API-Only Design
- No view layer, no HTML rendering
- All responses are JSON
- Frontend-agnostic for maximum flexibility
### 2. JWT Authentication
- Stateless authentication
- Token stored in HTTP-only cookie
- 1-hour expiration (configurable)
### 3. Soft Deletes
- All transactional tables use `DelDate`
- Data preserved for audit trails
- Automatic filtering via BaseModel
### 4. UTC Timezone
- All database dates in UTC
- Automatic conversion via BaseModel
- ISO 8601 format for API responses
### 5. JSON-Based ValueSets
- Static lookups in JSON files
- Fast, cached access
- Easy to maintain and version control
### 6. Migration-Based Schema
- Database changes via migrations
- Version-controlled schema history
- Easy rollback capability
## Critical Files to Know
| File | Purpose | Importance |
|------|---------|------------|
| `AGENTS.md` | AI agent instructions | **Critical** - Always read first |
| `app/Config/Routes.php` | API route definitions | **Critical** - Defines all endpoints |
| `public/api-docs.yaml` | OpenAPI documentation | **Critical** - MUST update after changes |
| `app/Libraries/ValueSet.php` | Lookup system | High - Used throughout |
| `app/Models/BaseModel.php` | Base model with UTC | High - All models extend this |
| `app/Filters/AuthFilter.php` | JWT authentication | High - Secures endpoints |
| `phpunit.xml.dist` | Test configuration | Medium - Configure database for tests |
| `.env` | Environment config | High - Contains secrets (JWT_SECRET, DB creds) |
## Common Patterns for Code Navigation
### Finding Controller for an Endpoint
1. Check `app/Config/Routes.php` for route
2. Find controller class in `app/Controllers/`
3. View controller method implementation
### Finding Model for a Table
1. Table name: `patient` → Model: `PatientModel.php`
2. Look in `app/Models/` or subdirectories
3. Check `$table`, `$primaryKey`, `$allowedFields`
### Understanding a Feature
1. Start with controller method
2. Follow to model methods
3. Check related models via joins
4. Refer to migrations for table structure
5. Check API documentation in `public/api-docs.yaml`
### Adding a New Endpoint
1. Create controller method
2. Create/update model if needed
3. Add route in `app/Config/Routes.php`
4. Write tests in `tests/feature/`
5. Update `public/api-docs.yaml`
6. Run tests to verify

View File

@ -0,0 +1,481 @@
# CLQMS Code Style & Conventions
## File Organization
### Directory Structure
```
app/
├── Controllers/ # API endpoint handlers
│ ├── Patient/ # Patient-related controllers
│ ├── Organization/ # Organization-related controllers
│ ├── Test/ # Test-related controllers
│ └── ...
├── Models/ # Data access layer
│ ├── BaseModel.php # Base model with UTC normalization
│ └── ...
├── Libraries/ # Reusable libraries
│ ├── ValueSet.php # JSON-based lookup system
│ └── Data/ # ValueSet JSON files
├── Database/
│ └── Migrations/ # Database schema migrations
└── Config/ # Configuration files
tests/
├── feature/ # Integration/API tests
├── unit/ # Unit tests
└── _support/ # Test utilities
public/
├── api-docs.yaml # OpenAPI/Swagger documentation (CRITICAL to update!)
└── index.php # Front controller
```
## Naming Conventions
| Type | Convention | Examples |
|------|------------|----------|
| **Classes** | PascalCase | `PatientController`, `PatientModel`, `ValueSet` |
| **Methods** | camelCase | `getPatient`, `createPatient`, `updatePatient` |
| **Variables** | camelCase | `$patientId`, `$rows`, `$input` |
| **Database Fields** | PascalCase with underscores | `PatientID`, `NameFirst`, `Street_1`, `InternalPID` |
| **Constants** | UPPER_SNAKE_CASE | `MAX_ATTEMPTS`, `DEFAULT_PRIORITY` |
| **Private Methods** | Prefix with underscore if needed | `_validatePatient` |
| **Files** | PascalCase | `PatientController.php`, `PatientModel.php` |
## Formatting & Style
### Indentation & Braces
- **2-space indentation** for all code
- **Same-line opening braces**: `public function index() {`
- No trailing whitespace
- Closing braces on new line
### Imports & Namespaces
```php
<?php
namespace App\Controllers\Patient;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Controller;
use App\Libraries\ValueSet;
use App\Models\Patient\PatientModel;
```
- Namespace at top: `namespace App\Controllers\...`
- Organize use statements: Framework first, then App libraries/models
- Group imports: `use CodeIgniter\...` then `use App\...`
### Type Hints (PHP 8.1+)
```php
public function getPatient(?int $id): ?array
{
// Method body
}
```
- Type hints on method parameters where appropriate
- Return types required for all methods
- Use `?type` for nullable types
- Use `array<string, mixed>` for complex arrays when clear
## Controllers Pattern
### Standard Controller Structure
```php
<?php
namespace App\Controllers\Patient;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Controller;
use App\Libraries\ValueSet;
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 = [
'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
// More validation rules...
];
}
public function index() {
$filters = [
'InternalPID' => $this->request->getVar('InternalPID'),
'PatientID' => $this->request->getVar('PatientID'),
];
try {
$rows = $this->model->getPatients($filters);
$rows = ValueSet::transformLabels($rows, [
'Sex' => 'sex',
]);
return $this->respond([
'status' => 'success',
'message' => 'data fetched successfully',
'data' => $rows
], 200);
} catch (\Exception $e) {
return $this->failServerError('Exception : ' . $e->getMessage());
}
}
public function create() {
$input = $this->request->getJSON(true);
if (!$this->validateData($input, $this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
try {
$InternalPID = $this->model->createPatient($input);
return $this->respondCreated([
'status' => 'success',
'message' => "data $InternalPID created successfully"
]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
}
```
### Controller Rules
- Extend `BaseController`
- Use `ResponseTrait`
- Define `$this->rules` array for validation
- Return JSON: `['status' => 'success', 'message' => '...', 'data' => ...]`
- Use try-catch with `$this->failServerError()`
- Input: `$this->request->getJSON(true)` or `$this->request->getVar()`
### Response Formats
```php
// Success
return $this->respond([
'status' => 'success',
'message' => 'Operation completed',
'data' => $data
], 200);
// Created
return $this->respondCreated([
'status' => 'success',
'message' => 'Resource created'
]);
// Validation Error
return $this->failValidationErrors($errors);
// Not Found
return $this->failNotFound('Resource not found');
// Server Error
return $this->failServerError('Error message');
```
## Models Pattern
### Standard Model Structure
```php
<?php
namespace App\Models\Patient;
use App\Models\BaseModel;
use App\Libraries\ValueSet;
class PatientModel extends BaseModel {
protected $table = 'patient';
protected $primaryKey = 'InternalPID';
protected $allowedFields = ['PatientID', 'NameFirst', 'NameLast', 'Sex', 'Birthdate', /*...*/];
protected $useTimestamps = true;
protected $createdField = 'CreateDate';
protected $updatedField = '';
protected $useSoftDeletes = true;
protected $deletedField = 'DelDate';
public function getPatients(array $filters = []) {
$this->select('InternalPID, PatientID, NameFirst, NameLast, Sex');
if (!empty($filters['PatientID'])) {
$this->like('PatientID', $filters['PatientID'], 'both');
}
$rows = $this->findAll();
$rows = ValueSet::transformLabels($rows, [
'Sex' => 'sex',
]);
return $rows;
}
public function createPatient(array $input) {
$db = \Config\Database::connect();
$db->transBegin();
try {
$this->insert($input);
$newInternalPID = $this->getInsertID();
$this->checkDbError($db, 'Insert patient');
// Additional operations...
$db->transCommit();
return $newInternalPID;
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}
}
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception(
"{$context} failed: {$error['code']} - {$error['message']}"
);
}
}
}
```
### Model Rules
- Extend `BaseModel` (auto UTC date normalization)
- Define `$table`, `$primaryKey`, `$allowedFields`
- Use soft deletes: `$useSoftDeletes = true`, `$deletedField = 'DelDate'`
- Wrap multi-table operations in transactions
- Use `$this->checkDbError($db, 'context')` after DB operations
## Database Operations
### Query Builder (Preferred)
```php
// Select with joins
$this->select('patient.*, patcom.Comment')
->join('patcom', 'patcom.InternalPID = patient.InternalPID', 'left')
->where('patient.InternalPID', (int) $InternalPID)
->findAll();
// Insert
$this->insert($data);
// Update
$this->where('InternalPID', $id)->set($data)->update();
// Delete (soft)
$this->where('InternalPID', $id)->delete(); // Sets DelDate
```
### Escape Inputs
```php
// Escape for raw queries (rarely used)
$this->db->escape($value)
// Better: Use parameter binding
$this->where('PatientID', $patientId)->get();
```
## ValueSet Usage
### For Static Lookups
```php
use App\Libraries\ValueSet;
// Get all values for a lookup (formatted for dropdowns)
$gender = ValueSet::get('sex');
// Returns: [{"value"=>"1","label":"Female"},{"value"=>"2","label":"Male"},...]
// Get single label by key
$label = ValueSet::getLabel('sex', '1'); // Returns 'Female'
// Transform database results to add text labels
$patients = [
['ID' => 1, 'Sex' => '1'],
['ID' => 2, 'Sex' => '2'],
];
$labeled = ValueSet::transformLabels($patients, [
'Sex' => 'sex'
]);
// Result: [['ID'=>1, 'Sex'=>'1', 'SexLabel'=>'Female'], ...]
// Clear cache after modifying valueset JSON files
ValueSet::clearCache();
```
### ValueSet JSON File Format
```json
{
"name": "sex",
"description": "Patient gender",
"values": [
{"key": "1", "value": "Female"},
{"key": "2", "value": "Male"},
{"key": "3", "value": "Unknown"}
]
}
```
## Error Handling
### Validation
```php
if (!$this->validateData($input, $rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
```
### Try-Catch Pattern
```php
try {
$result = $this->model->createPatient($input);
return $this->respondCreated([
'status' => 'success',
'message' => 'Created successfully'
]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
```
### HTTP Status Codes
- `200` - Success
- `201` - Created
- `400` - Bad Request (validation errors)
- `401` - Unauthorized
- `404` - Not Found
- `500` - Internal Server Error
## Testing Pattern
### Feature Test
```php
<?php
namespace Tests\Feature;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
class PatientCreateTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected $token;
protected function setUp(): void
{
parent::setUp();
// Generate JWT token
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
$payload = [
'iss' => 'localhost',
'aud' => 'localhost',
'iat' => time(),
'exp' => time() + 3600,
'uid' => 1,
'email' => 'admin@admin.com'
];
$this->token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256');
}
public function testCanCreatePatient() {
$this->withHeaders(['Cookie' => 'token=' . $this->token])
->post('api/patient', [
'PatientID' => 'PT001',
'NameFirst' => 'John',
'NameLast' => 'Doe',
'Sex' => '2',
'Birthdate' => '1990-05-15'
])
->assertStatus(200)
->assertJSONFragment(['status' => 'success']);
}
}
```
### Test Commands
```bash
# Run all tests
vendor/bin/phpunit
# Run specific test file
vendor/bin/phpunit tests/feature/Patient/PatientCreateTest.php
# Run with coverage
vendor/bin/phpunit --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
```
## Special Considerations
### UTC Date Handling
- BaseModel automatically normalizes dates to UTC
- All dates in database are in UTC format
- API responses return dates in ISO 8601 format: `Y-m-d\TH:i:s\Z`
- Date conversions handled automatically via BaseModel hooks
### Soft Deletes
- All transactional tables use soft deletes via `DelDate` field
- Soft delete sets `DelDate` to current timestamp
- Query Builder automatically filters out deleted records
### Input Validation
- Use CodeIgniter's validation rules in controllers
- Custom regex patterns for specific formats (e.g., KTP, Passport)
- Validate before processing in controllers
### API Documentation (CRITICAL)
- After modifying ANY controller, you MUST update `public/api-docs.yaml`
- Update OpenAPI schema definitions for new/changed endpoints
- Update field names, types, and response formats
- Ensure schemas match actual controller responses
### Security
- Never log or commit secrets (JWT_SECRET, passwords)
- Escape user inputs before DB operations
- Use JWT authentication for API endpoints
- Validate all inputs before processing
## Common Patterns
### Nested Data Handling
```php
// Extract nested data before filtering
$patIdt = $input['PatIdt'] ?? null;
$patCom = $input['PatCom'] ?? null;
// Remove nested arrays that don't belong to parent table
unset($input['PatIdt'], $input['PatCom']);
// Process nested data separately
if (!empty($patIdt)) {
$modelPatIdt->createPatIdt($patIdt, $newInternalPID);
}
```
### Foreign Key Handling
```php
// Handle array-based foreign keys
if (!empty($input['LinkTo']) && is_array($input['LinkTo'])) {
$internalPids = array_column($input['LinkTo'], 'InternalPID');
$input['LinkTo'] = implode(',', $internalPids);
}
$input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo'];
```
### Dynamic Validation Rules
```php
// Override validation rules based on input type
$type = $input['PatIdt']['IdentifierType'] ?? null;
$identifierRulesMap = [
'KTP' => 'required|regex_match[/^[0-9]{16}$/]',
'PASS' => 'required|regex_match[/^[A-Za-z0-9]{1,9}$/]',
];
if ($type) {
$this->rules['PatIdt.Identifier'] = $identifierRulesMap[$type] ?? 'permit_empty|max_length[255]';
}
```

View File

@ -0,0 +1,669 @@
# CLQMS Important Patterns & Special Considerations
## JWT Authentication Pattern
### How Authentication Works
1. User logs in via `/api/v2/auth/login` or `/api/login`
2. Server generates JWT token with payload containing user info
3. Token stored in HTTP-only cookie named `token`
4. `AuthFilter` checks for token on protected routes
5. Token decoded using `JWT_SECRET` from environment
6. If invalid/missing, returns 401 Unauthorized
### AuthFilter Location
- File: `app/Filters/AuthFilter.php`
- Registered in: `app/Config/Filters.php` as `'auth'` alias
### Protected Routes
Routes are protected by adding `'filter' => 'auth'` to route group:
```php
$routes->group('api', ['filter' => 'auth'], function ($routes) {
$routes->get('patient', 'Patient\PatientController::index');
$routes->post('patient', 'Patient\PatientController::create');
});
```
### JWT Token Structure
```php
$payload = [
'iss' => 'localhost', // Issuer
'aud' => 'localhost', // Audience
'iat' => time(), // Issued at
'nbf' => time(), // Not before
'exp' => time() + 3600, // Expiration (1 hour)
'uid' => 1, // User ID
'email' => 'admin@admin.com' // User email
];
```
### Generating JWT Token (for Tests)
```php
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
$payload = [
'iss' => 'localhost',
'aud' => 'localhost',
'iat' => time(),
'exp' => time() + 3600,
'uid' => 1,
'email' => 'admin@admin.com'
];
$token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256');
```
### Injecting Token in Tests
```php
protected $token;
protected function setUp(): void {
parent::setUp();
// Generate token as shown above
$this->token = $encodedToken;
}
public function testEndpoint() {
$this->withHeaders(['Cookie' => 'token=' . $this->token])
->get('api/patient')
->assertStatus(200);
}
```
### Public Routes (No Authentication)
```php
$routes->group('api', function ($routes) {
$routes->post('login', 'AuthController::login'); // No auth filter
});
$routes->group('api/demo', function ($routes) {
$routes->post('order', 'Test\DemoOrderController::createDemoOrder'); // No auth
});
```
## UTC Date Handling Pattern
### BaseModel Date Normalization
`BaseModel` automatically handles UTC date conversion:
**Before Insert/Update:**
```php
protected $beforeInsert = ['normalizeDatesToUTC'];
protected $beforeUpdate = ['normalizeDatesToUTC'];
```
- Converts local dates to UTC before database operations
- Uses helper: `convert_array_to_utc($data)`
**After Find/Insert/Update:**
```php
protected $afterFind = ['convertDatesToUTCISO'];
protected $afterInsert = ['convertDatesToUTCISO'];
protected $afterUpdate = ['convertDatesToUTCISO'];
```
- Converts UTC dates to ISO 8601 format for API responses
- Uses helper: `convert_array_to_utc_iso($data)`
### Date Formats
**Database Format (UTC):**
- Format: `Y-m-d H:i:s`
- Timezone: UTC
- Example: `2026-02-11 23:55:08`
**API Response Format (ISO 8601):**
- Format: `Y-m-d\TH:i:s\Z`
- Example: `2026-02-11T23:55:08Z`
### Manual Date Conversion
```php
// Convert to UTC for storage
$birthdate = new \DateTime('1990-05-15', new \DateTimeZone('Asia/Jakarta'));
$utcDate = $birthdate->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
// Format for display (ISO 8601)
$displayDate = $utcDate->format('Y-m-d\TH:i:s\Z');
```
### Date Fields in Database
- `CreateDate` - Record creation timestamp
- `DelDate` - Soft delete timestamp (null if not deleted)
- `TimeOfDeath` - Death timestamp (patient)
- Other date fields vary by table
## Soft Delete Pattern
### How Soft Deletes Work
- All transactional tables use `DelDate` field for soft deletes
- Setting `DelDate` to current timestamp marks record as deleted
- Queries automatically exclude records where `DelDate` is not null
- Data remains in database for audit trail
### BaseModel Soft Delete Configuration
```php
protected $useSoftDeletes = true;
protected $deletedField = 'DelDate';
```
### Manual Soft Delete
```php
// In controller
$this->db->table('patient')
->where('InternalPID', $InternalPID)
->update(['DelDate' => date('Y-m-d H:i:s')]);
// Or using model
$this->where('InternalPID', $id)->delete(); // BaseModel handles this
```
### Querying Deleted Records
If you need to include soft-deleted records:
```php
$this->withDeleted()->findAll();
```
### Including Deleted Records in Join
When joining with tables that might have soft-deleted records:
```php
->join('patatt', 'patatt.InternalPID = patient.InternalPID and patatt.DelDate is null', 'left')
```
## ValueSet Pattern
### ValueSet Library Location
- File: `app/Libraries/ValueSet.php`
- Data directory: `app/Libraries/Data/valuesets/`
### Getting ValueSet Data
```php
use App\Libraries\ValueSet;
// Get all values for a lookup (formatted for dropdowns)
$gender = ValueSet::get('sex');
// Returns: [{"value"=>"1","label":"Female"},{"value"=>"2","label":"Male"},...]
// Get raw data without formatting
$raw = ValueSet::getRaw('sex');
// Returns: [{"key":"1","value":"Female"},{"key":"2","value":"Male"},...]
// Get single label by key
$label = ValueSet::getLabel('sex', '1'); // Returns 'Female'
// Get key/value pairs for select inputs
$options = ValueSet::getOptions('sex');
// Returns: [["key"=>"1","value"=>"Female"],...]
```
### Transforming Database Results
```php
$patients = $this->model->getPatients();
$patients = ValueSet::transformLabels($patients, [
'Sex' => 'sex',
'Priority' => 'order_priority',
'MaritalStatus' => 'marital_status',
]);
// Adds fields: SexLabel, PriorityLabel, MaritalStatusLabel
```
### ValueSet JSON File Format
```json
{
"name": "sex",
"description": "Patient gender",
"values": [
{"key": "1", "value": "Female"},
{"key": "2", "value": "Male"},
{"key": "3", "value": "Unknown"}
]
}
```
### Clearing ValueSet Cache
After modifying ValueSet JSON files:
```php
ValueSet::clearCache();
```
### Common ValueSets
| Name | Description | Example Values |
|------|-------------|----------------|
| `sex` | Patient gender | Female, Male, Unknown |
| `marital_status` | Marital status | Single, Married, Divorced |
| `race` | Ethnicity | Jawa, Sunda, Batak, etc. |
| `order_priority` | Order priority | Stat, ASAP, Routine, Preop |
| `order_status` | Order lifecycle | STC, SCtd, SArrv, SRcvd |
| `specimen_type` | Specimen types | BLD, SER, PLAS, UR, CSF |
| `specimen_status` | Specimen status | Ordered, Collected, Received |
| `result_status` | Result validation | Preliminary, Final, Corrected |
| `test_type` | Test definition types | TEST, PARAM, CALC, GROUP |
## Error Handling Pattern
### Controller Error Handling
```php
public function create() {
$input = $this->request->getJSON(true);
if (!$this->validateData($input, $this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
try {
$result = $this->model->createPatient($input);
return $this->respondCreated([
'status' => 'success',
'message' => "data {$result} created successfully"
]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
```
### Model Error Handling
```php
public function createPatient(array $input) {
$db = \Config\Database::connect();
$db->transBegin();
try {
$this->insert($input);
$newId = $this->getInsertID();
$this->checkDbError($db, 'Insert patient');
$db->transCommit();
return $newId;
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}
}
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception(
"{$context} failed: {$error['code']} - {$error['message']}"
);
}
}
```
### ResponseTrait Methods
```php
// Success (200)
return $this->respond(['status' => 'success', 'data' => $data], 200);
// Created (201)
return $this->respondCreated(['status' => 'success', 'message' => 'Created']);
// Validation Error (400)
return $this->failValidationErrors($errors);
// Not Found (404)
return $this->failNotFound('Resource not found');
// Server Error (500)
return $this->failServerError('Error message');
// Unauthorized (401) - use in AuthFilter
return Services::response()
->setStatusCode(401)
->setJSON(['status' => 'failed', 'message' => 'Unauthorized']);
```
## Database Transaction Pattern
### Standard Transaction Pattern
```php
$db = \Config\Database::connect();
$db->transBegin();
try {
// Insert/Update main record
$this->insert($data);
$id = $this->getInsertID();
$this->checkDbError($db, 'Insert main');
// Insert related records
$relatedModel->insert($relatedData);
$this->checkDbError($db, 'Insert related');
$db->transCommit();
return $id;
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}
```
### When to Use Transactions
- Multi-table operations (main record + related records)
- Operations that must be atomic
- When you need to rollback all changes if any operation fails
## Nested Data Handling Pattern
### Extracting Nested Data
```php
// Extract nested data before filtering
$patIdt = $input['PatIdt'] ?? null;
$patCom = $input['PatCom'] ?? null;
$patAtt = $input['PatAtt'] ?? null;
// Remove nested arrays that don't belong to parent table
unset($input['PatIdt'], $input['PatCom'], $input['PatAtt']);
// Now $input only contains fields for main table
```
### Processing Nested Data
```php
// Insert main record
$this->insert($input);
$mainId = $this->getInsertID();
// Process related records
if (!empty($patIdt)) {
$modelPatIdt->createPatIdt($patIdt, $mainId);
}
if (!empty($patCom)) {
$modelPatCom->createPatCom($patCom, $mainId);
}
if (!empty($patAtt) && is_array($patAtt)) {
foreach ($patAtt as $address) {
$modelPatAtt->createPatAtt($address, $mainId);
}
}
```
## Foreign Key Handling Pattern
### Array-Based Foreign Keys
```php
// Handle array of related records
if (!empty($input['LinkTo']) && is_array($input['LinkTo'])) {
$internalPids = array_column($input['LinkTo'], 'InternalPID');
$input['LinkTo'] = implode(',', $internalPids);
}
$input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo'];
```
### Single Record Foreign Key
```php
// Handle single related record
if (!empty($input['Custodian']) && is_array($input['Custodian'])) {
$input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
if ($input['Custodian'] !== null) {
$input['Custodian'] = (int) $input['Custodian'];
}
}
```
## Validation Rules Pattern
### Dynamic Validation Rules
```php
// Override validation rules based on input type
$type = $input['PatIdt']['IdentifierType'] ?? null;
$identifierRulesMap = [
'KTP' => 'required|regex_match[/^[0-9]{16}$/]', // 16 digits
'PASS' => 'required|regex_match[/^[A-Za-z0-9]{1,9}$/]', // alphanumeric max 9
'SSN' => 'required|regex_match[/^[0-9]{9}$/]', // numeric 9 digits
'SIM' => 'required|regex_match[/^[0-9]{19,20}$/]', // numeric 19-20 digits
'KTAS' => 'required|regex_match[/^[0-9]{11}$/]', // numeric 11 digits
];
if ($type && is_string($type)) {
$identifierRule = $identifierRulesMap[$type] ?? 'permit_empty|max_length[255]';
$this->rules['PatIdt.IdentifierType'] = 'required';
$this->rules['PatIdt.Identifier'] = $identifierRule;
} else {
$this->rules['PatIdt.IdentifierType'] = 'permit_empty';
$this->rules['PatIdt.Identifier'] = 'permit_empty|max_length[255]';
}
```
### Common Validation Rules
```php
// Required field
'NameFirst' => 'required'
// String with letters, apostrophes, spaces
'NameFirst' => 'regex_match[/^[A-Za-z\'\. ]+$/]'
// Alphanumeric
'PatientID' => 'regex_match[/^[A-Za-z0-9]+$/]'
// Numeric
'InternalPID' => 'is_natural'
// Email
'EmailAddress1' => 'valid_email'
// Phone number with optional + and 8-15 digits
'Phone' => 'regex_match[/^\\+?[0-9]{8,15}$/]'
// Date
'Birthdate' => 'required'
// Permit empty
'AlternatePID' => 'permit_empty'
```
## Edge API Pattern
### Edge API Purpose
Integration with laboratory instruments via `tiny-edge` middleware for:
- Receiving instrument results
- Sending pending orders to instruments
- Acknowledging order delivery
- Logging instrument status
### Edge API Endpoints
```php
// Receive instrument results
POST /api/edge/results
// Fetch pending orders for instrument
GET /api/edge/orders?instrument=coulter_counter
// Acknowledge order delivery
POST /api/edge/orders/:id/ack
// Log instrument status
POST /api/edge/status
```
### Edge API Workflow
```
Instrument → tiny-edge → POST /api/edge/results → edgeres table
[Manual/Auto Processing]
patres table (patient results)
```
### Staging Table Pattern
- Raw results stored in `edgeres` table first
- Allows validation before processing to main tables
- Rerun handling via `AspCnt` field (attempt count)
## Security Considerations
### Environment Variables (NEVER COMMIT)
- `JWT_SECRET` - JWT signing key
- Database credentials (username, password)
- API keys for external services
### Input Validation
- Always validate user input with `$this->validateData($input, $rules)`
- Use CodeIgniter validation rules
- Custom regex patterns for specific formats
### SQL Injection Prevention
- Use Query Builder (parameter binding)
- Never concatenate user input into raw SQL
- If using raw SQL, escape inputs: `$this->db->escape($value)`
### Output Escaping
- ResponseTrait automatically handles JSON encoding
- For HTML output (if needed), use `esc()` helper
## API Documentation Pattern
### Critical Requirement
After modifying ANY controller, **MUST** update `public/api-docs.yaml`:
- Add new endpoints
- Update existing endpoint schemas
- Document request/response formats
- Include field names, types, and validation rules
- Add example requests/responses
### API Documentation Format
```yaml
paths:
/api/patient:
get:
summary: List patients
parameters:
- name: InternalPID
in: query
schema:
type: integer
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '#/components/schemas/Patient'
```
## Testing Pattern
### Feature Test Structure
```php
<?php
namespace Tests\Feature;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
class PatientCreateTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected $token;
protected function setUp(): void {
parent::setUp();
// Generate JWT token
$this->token = $this->generateToken();
}
public function testCanCreatePatient() {
$this->withHeaders(['Cookie' => 'token=' . $this->token])
->post('api/patient', [
'PatientID' => 'PT001',
'NameFirst' => 'John',
'NameLast' => 'Doe',
'Sex' => '2',
'Birthdate' => '1990-05-15'
])
->assertStatus(200)
->assertJSONFragment(['status' => 'success']);
}
}
```
### Test Commands
```bash
# Run all tests
vendor/bin/phpunit
# Run specific test file (IMPORTANT for debugging)
vendor/bin/phpunit tests/feature/Patient/PatientCreateTest.php
# Run with coverage
vendor/bin/phpunit --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
```
## Common Pitfalls
### Forgetting to Clear ValueSet Cache
**Problem:** Modified ValueSet JSON file but changes not reflected
**Solution:** Call `ValueSet::clearCache()` after modifications
### Not Updating api-docs.yaml
**Problem:** API documentation out of sync with implementation
**Solution:** Always update `public/api-docs.yaml` after controller changes
### Missing Soft Delete Filter in Joins
**Problem:** Query returns soft-deleted records from joined tables
**Solution:** Add `and table.DelDate is null` to join condition
### Incorrect Date Format
**Problem:** Dates not in UTC format causing issues
**Solution:** BaseModel handles this, but manual dates must be in `Y-m-d H:i:s` UTC
### Validation Rule Not Applied
**Problem:** Input not validated, invalid data inserted
**Solution:** Always call `$this->validateData($input, $rules)` before processing
### Transaction Not Rolled Back on Error
**Problem:** Partial data left in database on error
**Solution:** Always use try-catch with `$db->transRollback()`
### Not Using BaseModel for Date Handling
**Problem:** Dates not normalized to UTC
**Solution:** All models must extend `BaseModel`
## Debugging Tips
### Enable Detailed Error Messages
In development environment (`.env`):
```env
CI_ENVIRONMENT = development
```
### Log SQL Queries
Add to database config or temporarily enable:
```php
$db->setQueryLog(true);
$log = $db->getQueryLog();
```
### Check Application Logs
```bash
# Windows
type writable\logs\log-*.log
# Unix/Linux
tail -f writable/logs/log-*.log
```
### Add Temporary Debug Output
```php
var_dump($variable); die();
// or
log_message('debug', 'Debug info: ' . json_encode($data));
```
### Run Specific Test for Debugging
```bash
vendor/bin/phpunit tests/feature/SpecificTest.php --filter testMethodName
```
### Check Database State
```bash
php spark db:table patient
# or use MySQL Workbench, phpMyAdmin, etc.
```

View File

@ -0,0 +1,55 @@
# CLQMS Project Overview
## Project Purpose
CLQMS (Clinical Laboratory Quality Management System) is a **headless REST API backend** designed for modern clinical laboratory workflows. This API-only system provides comprehensive JSON endpoints for laboratory operations without any view layer. Frontend applications (web, mobile, desktop) consume these REST endpoints to build laboratory information systems.
### Core Features
- Patient registration and management
- Laboratory test ordering and tracking
- Specimen lifecycle management (collection → transport → reception → prep → analysis)
- Result entry with reference range validation
- Multi-level result verification (Technical → Clinical → Reporting)
- Instrument integration via Edge API (tiny-edge middleware)
- Master data management (value sets, test definitions, reference ranges)
- Quality control and calibration tracking
### Product Vision
To provide a headless, API-first laboratory information system serving as the backend for any frontend client while streamlining laboratory operations, ensuring regulatory compliance, and integrating seamlessly with laboratory instruments.
## Technology Stack
| Component | Specification |
|-----------|---------------|
| **Language** | PHP 8.1+ (PSR-compliant) |
| **Framework** | CodeIgniter 4 (API-only mode) |
| **Database** | MySQL 8.0+ |
| **Security** | JWT (JSON Web Tokens) via firebase/php-jwt |
| **Testing** | PHPUnit 10.5+ |
| **API Format** | RESTful JSON |
| **Architecture** | Clean architecture, API-first design |
### Key Dependencies
- `codeigniter4/framework` - Main framework
- `firebase/php-jwt` - JWT authentication
- `fakerphp/faker` - Test data generation
- `phpunit/phpunit` - Unit/feature testing
## Strategic Pillars
- **Precision & Accuracy**: Strict validation for all laboratory parameters and reference ranges
- **Scalability**: Optimized for high-volume diagnostic environments
- **Compliance**: Built-in audit trails and status history for full traceability
- **Interoperability**: Modular architecture designed for LIS, HIS, and analyzer integrations
## Key Characteristics
- **API-Only**: No view layer, no server-side rendering
- **Frontend Agnostic**: Any client can consume these APIs
- **JSON-First**: All requests/responses use JSON format
- **Stateless**: Each API request is independent with JWT authentication
- **UTC Normalization**: Automatic UTC timestamp handling via BaseModel
## Current Architecture Status
The system is undergoing an **Architectural Redesign** to consolidate legacy structures into a high-performance, maintainable schema focusing on:
- Unified Test Definitions (consolidating technical, calculated, and site-specific test data)
- Reference Range Centralization (unified engine for numeric, threshold, text, and coded results)
- Ordered Workflow Management (precise tracking of orders from collection to verification)

View File

@ -0,0 +1,393 @@
# CLQMS Suggested Commands
## Database Commands
### Migrations
```bash
# Run all pending migrations
php spark migrate
# Run specific migration
php spark migrate [migration_file]
# Rollback last batch of migrations
php spark migrate:rollback
# Rollback and re-run migrations
php spark migrate:refresh
# Refresh and seed database
php spark migrate:refresh --seed
# Check migration status
php spark migrate:status
```
### Database Seeding
```bash
# Run all seeders
php spark db:seed
# Run specific seeder
php spark db:seed DBSeeder
# Create new database
php spark db:create
```
### Database Query
```bash
# Get table information
php spark db:table [table_name]
```
## Testing Commands
### PHPUnit Tests
```bash
# Run all tests
vendor/bin/phpunit
# Run specific test file (IMPORTANT for debugging)
vendor/bin/phpunit tests/feature/UniformShowTest.php
vendor/bin/phpunit tests/feature/Patient/PatientCreateTest.php
# Run tests in specific directory
vendor/bin/phpunit tests/feature/Patient/
# Run tests with coverage (text output)
vendor/bin/phpunit --coverage-text=tests/coverage.txt -d memory_limit=1024m
# Run tests with coverage (HTML report)
vendor/bin/phpunit --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
# Run with verbose output
vendor/bin/phpunit --verbose
# Run specific test method
vendor/bin/phpunit --filter testCanCreatePatient
```
## Code Generation Commands
### Generate Code Files
```bash
# Generate new model
php spark make:model ModelName
# Generate new controller
php spark make:controller ControllerName
# Generate new migration
php spark make:migration MigrationName
# Generate new seeder
php spark make:seeder SeederName
# Generate new test
php spark make:test TestName
# Generate scaffold (complete set)
php spark make:scaffold ModelName
```
## Development Server
### Start Development Server
```bash
# Start CodeIgniter dev server (default port 8080)
php spark serve
# Start on specific port
php spark serve --port=8080
# Start with specific host
php spark serve --host=0.0.0.0
```
## Cache Management
### Clear Caches
```bash
# Clear application cache
php spark cache:clear
# Show cache information
php spark cache:info
# Clear debug bar JSON files
php spark debugbar:clear
# Clear log files
php spark logs:clear
```
## System Utilities
### Configuration & Environment
```bash
# Get current environment
php spark env
# Set environment
php spark env development
# Check configuration values
php spark config:check
# Check php.ini values (for production)
php spark phpini:check
# Verify namespaces
php spark namespaces
```
### Routes
```bash
# Display all routes
php spark routes
# Check filters for a route
php spark filter:check
```
### Optimization
```bash
# Optimize for production
php spark optimize
```
## Utility Commands
### Encryption
```bash
# Generate new encryption key (writes to .env)
php spark key:generate
```
### Publish
```bash
# Publish predefined resources
php spark publish
```
## General Utility Commands (Windows)
### File & Directory Operations
```bash
# List files in current directory
dir
# List files with details
dir /a
# Change directory
cd path\to\directory
# Go to parent directory
cd ..
# Create directory
mkdir directory_name
# Remove directory (empty)
rmdir directory_name
# Remove directory with contents
rmdir /s /q directory_name
# Copy file
copy source_file destination
# Move/Rename file
move source destination
# Delete file
del filename
# Search for file
dir /s filename
```
### File Content Operations
```bash
# Display file content
type filename
# Display file content page by page
more filename
# Search for text in file
findstr "pattern" filename
# Search recursively
findstr /s /i "pattern" *.php
```
### Git Commands
```bash
# Check git status
git status
# View changes
git diff
# View staged changes
git diff --staged
# Add files to staging
git add .
# Add specific file
git add path/to/file
# Commit changes
git commit -m "commit message"
# Push to remote
git push
# Pull from remote
git pull
# View commit history
git log
# View commit history with graph
git log --graph --oneline
# Create new branch
git checkout -b branch_name
# Switch branch
git checkout branch_name
# Merge branch
git merge branch_name
# View branches
git branch
```
### Process Management
```bash
# List running processes
tasklist
# Kill process by PID
taskkill /PID pid_number
# Kill process by name
taskkill /IM process_name.exe
# Kill process forcefully
taskkill /F /PID pid_number
```
### Network Operations
```bash
# Check port usage
netstat -an | findstr :port
# Test network connectivity
ping hostname
# View active connections
netstat -an
```
## After Task Completion Checklist
When completing a task, run the following commands to ensure code quality:
### 1. Run Tests
```bash
# Run all tests
vendor/bin/phpunit
# If tests fail, run specific test file for debugging
vendor/bin/phpunit tests/feature/[SpecificTestFile].php
```
### 2. Check for Linting Issues (if configured)
```bash
# Check for PHP syntax errors
php -l app/Controllers/YourController.php
php -l app/Models/YourModel.php
# Run any custom linting tools if configured in composer.json
composer test # if 'test' script includes linting
```
### 3. Type Checking (if configured)
```bash
# Run static analysis tools if configured
vendor/bin/phpstan analyse # if phpstan is installed
vendor/bin/psalm # if psalm is installed
```
### 4. Database Verification
```bash
# Check migration status
php spark migrate:status
# If you created a migration, run it
php spark migrate
```
### 5. API Documentation Update (CRITICAL)
```bash
# After modifying ANY controller, MUST update api-docs.yaml
# This is a manual process - edit public/api-docs.yaml
# Verify YAML syntax (optional, if yamllint is available)
yamllint public/api-docs.yaml
```
### 6. Code Review Checklist
- [ ] All tests passing
- [ ] No PHP syntax errors
- [ ] Database migrations applied (if any)
- [ ] API documentation updated (`public/api-docs.yaml`)
- [ ] Code follows style conventions (see `code_style_conventions.md`)
- [ ] Proper error handling in place
- [ ] Input validation implemented
- [ ] JWT authentication required where needed
- [ ] UTC date handling via BaseModel
- [ ] Soft delete using `DelDate` where applicable
- [ ] ValueSet lookups properly used and cached cleared if modified
### 7. Verify API Endpoints (if applicable)
```bash
# If you have curl or a REST client, test the endpoints
# Example using curl:
curl -X GET http://localhost:8080/api/patient -H "Cookie: token=your_jwt_token"
```
## Common Workflows
### Creating a New API Endpoint
1. Create controller: `php spark make:controller ControllerName`
2. Create model: `php spark make:model ModelName`
3. Create migration (if new table): `php spark make:migration MigrationName`
4. Run migration: `php spark migrate`
5. Add routes in `app/Config/Routes.php`
6. Implement controller methods following pattern
7. Create tests: `php spark make:test Feature/EndpointNameTest`
8. Run tests: `vendor/bin/phpunit tests/feature/EndpointNameTest.php`
9. Update `public/api-docs.yaml`
10. Verify with test client
### Debugging a Failing Test
1. Run specific test file: `vendor/bin/phpunit tests/feature/SpecificTest.php`
2. Add debug output: `var_dump($result); die();` temporarily
3. Check database state: View with database client
4. Check logs: `writable/logs/` directory
5. Run with verbose: `vendor/bin/phpunit --verbose`
6. Isolate specific test: `vendor/bin/phpunit --filter testMethodName`
### Working with ValueSets
1. Create/edit JSON file: `app/Libraries/Data/valuesets/{name}.json`
2. Clear cache: `ValueSet::clearCache();` (in code) or via code
3. Verify via API: GET `/api/valueset/{name}`
4. Test lookup: `ValueSet::get('name')` in controller/model

View File

@ -0,0 +1,227 @@
# Task Completion Checklist
This checklist should be followed after completing any development task to ensure code quality and consistency.
## Immediate Post-Task Actions
### 1. Run Tests
```bash
# Run all tests to ensure nothing is broken
vendor/bin/phpunit
# If tests fail, run specific test file for debugging
vendor/bin/phpunit tests/feature/[SpecificTestFile].php
```
### 2. Check for PHP Syntax Errors
```bash
# Check syntax of modified files
php -l app/Controllers/YourController.php
php -l app/Models/YourModel.php
```
### 3. Verify Database Changes
```bash
# If you created a migration, verify it was applied
php spark migrate:status
# If you created a migration, run it
php spark migrate
```
### 4. Update API Documentation (CRITICAL)
- **MUST** update `public/api-docs.yaml` after modifying ANY controller
- Update OpenAPI schema definitions for new/changed endpoints
- Update field names, types, and response formats
- Ensure schemas match actual controller responses
## Code Quality Verification
### Controllers Checklist
- [ ] Extends `BaseController`
- [ ] Uses `ResponseTrait`
- [ ] Defines `$this->rules` array for validation
- [ ] Validates input: `$this->validateData($input, $rules)`
- [ ] Uses try-catch with `$this->failServerError()`
- [ ] Returns consistent JSON format: `['status' => 'success', 'message' => '...', 'data' => ...]`
- [ ] Uses `$this->request->getJSON(true)` for POST/PATCH
- [ ] Uses `$this->request->getVar()` for GET parameters
- [ ] Proper HTTP status codes: 200 (success), 201 (created), 400 (validation), 404 (not found), 500 (error)
### Models Checklist
- [ ] Extends `BaseModel` (for UTC normalization)
- [ ] Defines `$table`, `$primaryKey`, `$allowedFields`
- [ ] Uses soft deletes: `$useSoftDeletes = true`, `$deletedField = 'DelDate'`
- [ ] Wraps multi-table operations in transactions
- [ ] Uses `$this->checkDbError($db, 'context')` after DB operations
- [ ] Uses Query Builder, not raw SQL
- [ ] Escapes inputs properly via parameter binding
### Code Style Checklist
- [ ] 2-space indentation
- [ ] Same-line opening braces: `public function index() {`
- [ ] No trailing whitespace
- [ ] Namespace at top: `namespace App\Controllers\...`
- [ ] Organized imports: Framework first, then App libraries/models
- [ ] Classes: PascalCase (`PatientController`)
- [ ] Methods: camelCase (`getPatient`)
- [ ] Variables: camelCase (`$patientId`)
- [ ] Database fields: PascalCase with underscores (`PatientID`, `NameFirst`)
### Security Checklist
- [ ] No secrets logged or committed (JWT_SECRET, passwords)
- [ ] User inputs validated before processing
- [ ] JWT authentication required for protected endpoints
- [ ] SQL injection prevention via Query Builder
- [ ] XSS prevention via proper escaping
### Data Integrity Checklist
- [ ] UTC date normalization via BaseModel
- [ ] Soft delete using `DelDate` field
- [ ] Referential integrity maintained
- [ ] Transactional data consistency via `$db->transBegin()`, `$db->transCommit()`, `$db->transRollback()`
### Testing Checklist
- [ ] Tests written for new functionality
- [ ] Tests extend `CIUnitTestCase`
- [ ] Feature tests use `FeatureTestTrait`
- [ ] JWT token injected via `withHeaders(['Cookie' => 'token=' . $this->token])`
- [ ] Assert JSON structure: `$this->assertIsArray($body['data'])`
- [ ] All tests passing: `vendor/bin/phpunit`
### ValueSet Checklist
- [ ] If using static lookups, use `ValueSet::get('name')`
- [ ] Transform labels: `ValueSet::transformLabels($data, ['Field' => 'valueset'])`
- [ ] If modifying valueset JSON files, call `ValueSet::clearCache()`
- [ ] Valueset JSON files in `app/Libraries/Data/valuesets/`
### Database Checklist
- [ ] Migrations follow naming convention: `YYYY-MM-DD-NNNNNN_Description.php`
- [ ] Migrations define both `up()` and `down()` methods
- [ ] Foreign keys properly defined
- [ ] Indexes added for performance
- [ ] Tables dropped in reverse order in `down()` method
### API Documentation Checklist
- [ ] `public/api-docs.yaml` updated with new/changed endpoints
- [ ] Schemas match actual controller responses
- [ ] Field names and types documented
- [ ] Request/response examples provided
- [ ] Authentication requirements documented
## Verification Steps
### 1. Verify API Endpoints (If Applicable)
```bash
# If you have curl, test the endpoints
curl -X GET http://localhost:8080/api/patient \
-H "Cookie: token=your_jwt_token"
# Or use a REST client like Postman
```
### 2. Check Logs
```bash
# View recent application logs
type writable\logs\log-*.log # Windows
# or
tail -f writable/logs/log-*.log # Unix/Linux
```
### 3. Check Database
```bash
# Verify database state
php spark db:table [table_name]
# Or use your database client (MySQL Workbench, phpMyAdmin, etc.)
```
## Common Issues to Check
### Potential Issues
1. **Tests failing**: Check error messages, add debug output, verify database state
2. **Syntax errors**: Run `php -l` on modified files
3. **Validation errors**: Check `$this->rules` array, ensure input matches expected format
4. **Database errors**: Check migration status, verify table structure
5. **Authentication issues**: Verify JWT token, check `AuthFilter`, ensure route has auth filter
6. **Date issues**: BaseModel handles UTC normalization, verify date formats
7. **Soft deletes**: Check if `DelDate` field is being set, verify queries filter deleted records
### Performance Considerations
- [ ] Database queries optimized (use indexes, avoid SELECT *)
- [ ] Caching used appropriately (ValueSet, query caching)
- [ ] N+1 query problem avoided (use eager loading)
- [ ] Large datasets paginated
## Final Steps
### Before Marking Task Complete
1. **Run all tests**: `vendor/bin/phpunit`
2. **Check syntax**: `php -l` on modified files
3. **Update documentation**: `public/api-docs.yaml` (CRITICAL)
4. **Verify manually**: Test API endpoints if applicable
5. **Clean up**: Remove debug code, comments (unless requested)
### Git Commit (If Requested)
Only commit when explicitly requested by user. If committing:
1. Review changes: `git diff` and `git status`
2. Stage relevant files: `git add .`
3. Create meaningful commit message
4. Commit: `git commit -m "message"`
5. Do NOT push unless explicitly requested
## Example Task Completion Workflow
### Adding a New Controller Method
1. Implement controller method following pattern
2. Add model methods if needed
3. Add route in `app/Config/Routes.php`
4. Create test: `php spark make:test Feature/NewEndpointTest`
5. Implement tests
6. Run tests: `vendor/bin/phpunit`
7. Update `public/api-docs.yaml`
8. Verify with curl or REST client
9. Check syntax: `php -l app/Controllers/YourController.php`
10. Done!
### Modifying Existing Endpoint
1. Modify controller/model code
2. Run tests: `vendor/bin/phpunit`
3. Update `public/api-docs.yaml`
4. Test endpoint manually
5. Check syntax: `php -l` on modified files
6. Done!
### Creating a Database Migration
1. Create migration: `php spark make:migration Description`
2. Define `up()` and `down()` methods
3. Run migration: `php spark migrate`
4. Verify: `php spark migrate:status`
5. Create seeder if needed
6. Run tests to verify no regressions
7. Done!
## Quick Reference Commands
```bash
# Run all tests
vendor/bin/phpunit
# Run specific test
vendor/bin/phpunit tests/feature/SpecificTest.php
# Check PHP syntax
php -l path/to/file.php
# Run migrations
php spark migrate
# Check migration status
php spark migrate:status
# Start dev server
php spark serve
# Clear cache
php spark cache:clear
```

127
.serena/project.yml Normal file
View File

@ -0,0 +1,127 @@
# the name by which the project can be referenced within Serena
project_name: "clqms01-be"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- php
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in all projects
# same syntax as gitignore, so you can use * and **
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used.
symbol_info_budget:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []

334
TODO.md
View File

@ -1,334 +0,0 @@
# CLQMS REST API Development Checklist
**Note**: This checklist covers ONLY the REST API backend. UI/UX implementation is handled separately in the frontend project (clqms01-fe).
## Legend
- [x] **DONE** - Implemented and functional
- [ ] **TODO** - Not yet implemented
- [~] **PARTIAL** - Partially implemented
---
## Phase 1: Core Infrastructure ✅ DONE
### 1.1 Base Framework
- [x] BaseController with ResponseTrait
- [x] BaseModel with auto UTC normalization
- [x] Validation system with custom rules
- [x] JWT authentication system
- [x] CORS and security headers configuration
- [x] Database migration system
### 1.2 Authentication & Security
- [x] User login/logout (AuthController, AuthV2Controller)
- [x] JWT token generation and validation
- [x] Password change functionality
- [x] Auth check endpoints
- [x] Role-based access control (RBAC) preparation
---
## Phase 2: Organization Management ✅ DONE
### 2.1 Account Management
- [x] Account CRUD API (`/api/organization/account`)
- [x] Account listing and search
- [x] Parent-child Account relationships
### 2.2 Site Management
- [x] Site CRUD API (`/api/organization/site`)
- [x] SiteCode generation support
- [x] Site type management (PHL, GL, PL)
### 2.3 Department Management
- [x] Department CRUD API (`/api/organization/department`)
- [x] Department-Discipline linking
### 2.4 Discipline Management
- [x] Discipline CRUD API (`/api/organization/discipline`)
- [x] Discipline hierarchy support
### 2.5 Workstation Management
- [x] Workstation CRUD API (`/api/organization/workstation`)
- [x] Workstation type management (primary/secondary)
- [x] Workstation linking support
---
## Phase 3: Personnel/Contact Management ✅ DONE
### 3.1 Contact Management
- [x] Contact CRUD API (`/api/contact`)
- [x] ContactDetail model (for site-specific roles)
- [x] Patient-Contact association
### 3.2 Occupation Management
- [x] Occupation CRUD API (`/api/occupation`)
### 3.3 Medical Specialty
- [x] MedicalSpecialty CRUD API (`/api/medicalspecialty`)
---
## Phase 4: Patient Management ✅ DONE
### 4.1 Patient Core
- [x] Patient CRUD API (`/api/patient`)
- [x] Patient search and filtering
- [x] Patient identifier management (PatIdtModel)
- [x] Patient comments (PatComModel)
- [x] Patient attachments (PatAttModel)
- [x] Patient check endpoint
### 4.2 Patient Visit (ADT)
- [x] PatientVisit CRUD API (`/api/patvisit`)
- [x] Get visits by patient
- [x] PatVisitADT CRUD (`/api/patvisitadt`)
- [x] Get ADT by visit
- [x] Admission workflow (A01)
- [x] Transfer workflow (A02)
- [x] Discharge workflow (A03)
- [x] Cancel admission/discharge
---
## Phase 5: Location Management ✅ DONE
### 5.1 Location
- [x] Location CRUD API (`/api/location`)
- [x] Location hierarchy support
### 5.2 Area/Geography
- [x] AreaGeo API (`/api/areageo`)
- [x] Province listing
- [x] City listing
---
## Phase 6: Specimen Management ✅ DONE
### 6.1 Specimen Core
- [x] Specimen CRUD API (`/api/specimen`)
- [x] SID generation support
### 6.2 Container Management
- [x] ContainerDef CRUD API (`/api/specimen/container`, `/api/specimen/containerdef`)
### 6.3 Specimen Workflow
- [x] SpecimenPrep CRUD API (`/api/specimen/prep`)
- [x] SpecimenStatus CRUD API (`/api/specimen/status`)
- [x] SpecimenCollection CRUD API (`/api/specimen/collection`)
---
## Phase 7: Test Management ✅ DONE
### 7.1 Test Definitions
- [x] Tests CRUD API (`/api/tests`)
- [x] Test mapping API (`/api/test/testmap`)
- [x] **Reference Range nested in Tests** (RefNum, RefTxt via POST/PUT body)
- [x] Test profile/group management
- [x] Test discipline mapping
### 7.2 Test Ordering
- [x] OrderTest CRUD API (`/api/ordertest`)
- [x] Order status update endpoint
- [x] Demo order creation (`/api/demo/order`)
### 7.3 Reference Range (Nested in Tests)
- [x] RefNum model and controller integration (`saveRefNumRanges()`)
- [x] RefTxt model and controller integration (`saveRefTxtRanges()`)
- [x] Age range validation
- [x] Gender-based reference ranges
- [x] Text-based result interpretation
- [x] Threshold (RefTHold) support
- [x] ValueSet (RefVSet) support
**Note**: Reference ranges are managed as nested data within Tests API. POST/PUT `/api/tests` includes `refRanges` array in request body.
---
## Phase 8: Result Management ✅ PARTIAL
### 8.1 Result Core
- [x] Result listing endpoint (`/api/result`)
- [~] Result entry (manual)
- [ ] Result validation workflow
- [ ] Result flagging (H/L/A)
- [ ] Result correction workflow
### 8.2 Result ValueSets
- [x] ResultValueSet CRUD API (`/api/result/valueset`)
---
## Phase 9: ValueSet Management ✅ DONE
### 9.1 File-based ValueSets
- [x] ValueSet listing (`/api/valueset`)
- [x] Specific value set retrieval (`/api/valueset/{type}`)
- [x] ValueSet refresh endpoint
### 9.2 Database ValueSets
- [x] ValueSet items CRUD (`/api/valueset/user/items`)
- [x] ValueSet definitions CRUD (`/api/valueset/user/def`)
---
## Phase 10: Infrastructure & Support ✅ DONE
### 10.1 Counter Management
- [x] Counter CRUD API (`/api/counter`)
### 10.2 Edge Integration (Instrument)
- [x] Edge results endpoint (`/api/edge/results`)
- [x] Edge orders retrieval (`/api/edge/orders`)
- [x] Order acknowledgment (`/api/edge/orders/{id}/ack`)
- [x] Status update endpoint (`/api/edge/status`)
---
## Phase 11: Missing Features ⏳ TODO
### 11.1 Equipment Management
- [ ] ProductCatalog CRUD
- [ ] ProductCatalogExt CRUD
- [ ] ProductExt (Equipment) CRUD
- [ ] Equipment lifecycle tracking (installation, maintenance, decommissioning)
- [ ] Equipment-instrument mapping
### 11.2 Calibration Management
- [ ] Calibrator CRUD
- [ ] Calibration results API
- [ ] Calibration factor tracking
- [ ] Calibration validation endpoints
- [ ] Cumulative calibration view
### 11.3 Quality Control (QC) Management
- [ ] QC material CRUD
- [ ] QC results API
- [ ] Levey-Jennings data endpoints
- [ ] QC validation workflow
- [ ] Westgard rules implementation
### 11.4 Inventory Management
- [ ] Inventory receiving API
- [ ] Consumables tracking endpoints
- [ ] Stock monitoring API
- [ ] Expiry date alerts
### 11.6 Audit Trail
- [ ] Patient audit log API (patreglog)
- [ ] Visit audit log API (patvisitlog)
- [ ] Specimen audit log API (specimenlog)
- [ ] Security event logging API
### 11.7 Advanced Features
- [ ] Multi-site patient linking API
- [ ] Patient merge/unlink endpoints
- [ ] Archiving API
- [ ] Data warehouse integration endpoints
- [ ] Report generation API (PDF/JSON)
- [ ] HL7 message generation endpoints
- [ ] Instrument test mapping API
- [ ] Rerun management API
- [ ] Critical value alerts API
### 11.8 SMCRM Integration
- [ ] CRM sync webhooks
- [ ] Bidirectional data sync endpoints
- [ ] Equipment activity sync
---
## Phase 12: Documentation & Testing ⏳ TODO
### 12.1 API Documentation
- [x] Swagger/OpenAPI documentation endpoint (`/swagger`)
- [ ] Complete API documentation for all endpoints
- [ ] Request/response examples
- [ ] Authentication documentation
### 12.2 Testing
- [ ] PHPUnit test suite setup
- [ ] Unit tests for models
- [ ] Integration tests for controllers
- [ ] API endpoint tests
- [ ] HL7 message tests
---
## Summary
| Category | Done | Partial | Todo | Total |
|----------|------|---------|------|-------|
| Core Infrastructure | 5 | 0 | 0 | 5 |
| Organization | 5 | 0 | 0 | 5 |
| Personnel | 3 | 0 | 0 | 3 |
| Patient | 8 | 0 | 0 | 8 |
| Location | 2 | 0 | 0 | 2 |
| Specimen | 4 | 0 | 0 | 4 |
| Test | 9 | 0 | 0 | 9 |
| Result | 2 | 1 | 5 | 8 |
| ValueSet | 2 | 0 | 0 | 2 |
| Infrastructure | 2 | 0 | 0 | 2 |
| **Missing Features** | **0** | **0** | **32** | **32** |
| **Documentation** | **1** | **0** | **3** | **4** |
| **TOTAL** | **43** | **1** | **40** | **84** |
**Completion: ~51% (43/84 items done)**
---
## Priority Next Steps
### 🔴 CRITICAL - Blockers for Production
1. [ ] Result validation workflow
- Flag generation based on reference ranges (H/L/A)
- Result interpretation logic
- Must be completed before production deployment
### High Priority (Core Lab Functions)
2. [ ] Equipment Management (ProductExt, Equipment lifecycle)
3. [ ] Calibration Management (Calibrator, Calibration results)
4. [ ] QC Management (QC material, QC results)
5. [ ] Result entry and correction workflow
### Medium Priority (Important)
6. [ ] Audit Trail (Patient, Visit, Specimen logs)
7. [ ] HL7 message generation
8. [ ] Report generation API
9. [ ] Inventory Management
10. [ ] Rerun management API
### Low Priority (Future Enhancement)
11. [ ] SMCRM Integration
12. [ ] Data warehouse integration
13. [ ] Archiving API
14. [ ] Critical value alerts
15. [ ] Multi-site patient linking
---
**Last Updated**: 2026-02-15 (Revised: RefRange is DONE - nested in Tests API)
**Project**: CLQMS Backend REST API
**Version**: 1.2.0
## Architecture Notes
### Nested Data Pattern
Some entities are managed as nested data within parent APIs rather than standalone endpoints:
- **Reference Range** → Nested in `/api/tests` (POST/PUT body includes `refRanges` array)
- RefNum (numeric ranges with age/gender filters)
- RefTxt (text interpretations)
- RefTHold (threshold/cut-off values)
- RefVSet (ValueSet-based ranges)
- **Test Mapping** → Nested in `/api/tests`
- Instrument-to-LIS test code mapping
- Many-to-one test relationships
- **Test Groups/Profiles** → Nested in `/api/tests`
- Group definitions
- Panel structures

View File

@ -252,6 +252,15 @@ $routes->group('api', function ($routes) {
$routes->delete('/', 'Infrastructure\EquipmentListController::delete');
});
// Users
$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
$routes->group('specimen', function ($routes) {
// Container aliases - 'container' and 'containerdef' both point to ContainerDefController
@ -289,11 +298,12 @@ $routes->group('api', function ($routes) {
$routes->patch('/', 'Specimen\SpecimenCollectionController::update');
});
$routes->get('/', 'Specimen\SpecimenController::index');
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
$routes->post('/', 'Specimen\SpecimenController::create');
$routes->patch('/', 'Specimen\SpecimenController::update');
});
$routes->get('/', 'Specimen\SpecimenController::index');
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
$routes->post('/', 'Specimen\SpecimenController::create');
$routes->patch('/', 'Specimen\SpecimenController::update');
$routes->delete('(:num)', 'Specimen\SpecimenController::delete/$1');
});
// Test Mapping
$routes->group('test', function ($routes) {

View File

@ -77,4 +77,50 @@ class SpecimenController extends BaseController {
}
}
/**
* 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

@ -42,6 +42,8 @@ class TestsController extends BaseController
public function index()
{
$search = $this->request->getGet('search');
$filters = [
'SiteID' => $this->request->getGet('SiteID'),
'TestType' => $this->request->getGet('TestType'),
@ -49,6 +51,7 @@ class TestsController extends BaseController
'VisibleRpt' => $this->request->getGet('VisibleRpt'),
'TestSiteName' => $this->request->getGet('TestSiteName'),
'TestSiteCode' => $this->request->getGet('TestSiteCode'),
'search' => $search,
];
$rows = $this->model->getTestsWithRelations($filters);

View File

@ -0,0 +1,293 @@
<?php
namespace App\Controllers\User;
use App\Controllers\BaseController;
use App\Models\User\UserModel;
use App\Traits\ResponseTrait;
/**
* User Management Controller
* Handles CRUD operations for users
*/
class UserController extends BaseController
{
use ResponseTrait;
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

@ -13,7 +13,6 @@ class CreateTestDefinitions extends Migration {
'TestSiteName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => false],
'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
// Technical details merged from testdeftech
'DisciplineID' => ['type' => 'INT', 'null' => true],
'DepartmentID' => ['type' => 'INT', 'null' => true],
'ResultType' => ['type' => 'VARCHAR', 'constraint'=> 20, 'null' => true],
@ -36,6 +35,7 @@ class CreateTestDefinitions extends Migration {
'VisibleRpt' => ['type' => 'int', 'null' => true, 'default' => 1],
'CountStat' => ['type' => 'int', 'null' => true, 'default' => 1],
'Level' => ['type' => 'int', 'null' => true],
'Requestable' => [ 'type' => 'TINYINT', 'constraint' => 1, 'null' => true, 'default' => 1, 'comment' => 'Flag indicating if test can be requested (1=yes, 0=no)' ],
'CreateDate' => ['type' => 'Datetime', 'null' => true],
'StartDate' => ['type' => 'Datetime', 'null' => true],
'EndDate' => ['type' => 'Datetime', 'null' => true],

View File

@ -1,28 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddRequestableToTestDefSite extends Migration
{
public function up()
{
$fields = [
'Requestable' => [
'type' => 'TINYINT',
'constraint' => 1,
'null' => true,
'default' => 1,
'comment' => 'Flag indicating if test can be requested (1=yes, 0=no)'
]
];
$this->forge->addColumn('testdefsite', $fields);
}
public function down()
{
$this->forge->dropColumn('testdefsite', 'Requestable');
}
}

View File

@ -9,6 +9,7 @@ class DBSeeder extends Seeder
public function run()
{
$this->call('OrganizationSeeder');
$this->call('HostAppCodingSysSeeder');
$this->call('CounterSeeder');
$this->call('ContactSeeder');
$this->call('LocationSeeder');

View File

@ -120,6 +120,13 @@ class TestDefSiteModel extends BaseModel {
$builder->like('testdefsite.TestSiteCode', $filters['TestSiteCode']);
}
if (!empty($filters['search'])) {
$builder->groupStart()
->like('testdefsite.TestSiteCode', $filters['search'])
->orLike('testdefsite.TestSiteName', $filters['search'])
->groupEnd();
}
return $builder->orderBy('testdefsite.SeqScr', 'ASC')->get()->getResultArray();
}

View File

@ -0,0 +1,138 @@
<?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
]);
}
}

1
issues.md Normal file
View File

@ -0,0 +1 @@
[ ] account initial must be unique

View File

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

View File

@ -0,0 +1,74 @@
# 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

@ -0,0 +1,55 @@
# 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

@ -0,0 +1,42 @@
// ============================================================================
// 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

@ -0,0 +1,49 @@
// ============================================================================
// 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

@ -0,0 +1,290 @@
<?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

@ -0,0 +1,138 @@
<?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

@ -0,0 +1,165 @@
## 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

@ -0,0 +1,63 @@
## 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

@ -0,0 +1,119 @@
# 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

@ -0,0 +1,92 @@
## 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

@ -0,0 +1,243 @@
# 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
```

20
openspec/config.yaml Normal file
View File

@ -0,0 +1,20 @@
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

@ -51,6 +51,8 @@ tags:
description: Demo/test endpoints (no authentication)
- name: EquipmentList
description: Laboratory equipment and instrument management
- name: Users
description: User management and administration
paths:
/api/auth/login:
post:
@ -3104,6 +3106,68 @@ paths:
responses:
'200':
description: Specimen details
delete:
tags:
- Specimen
summary: Delete specimen (soft delete)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Specimen ID (SID)
responses:
'200':
description: Specimen deleted successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Specimen deleted successfully
data:
type: object
properties:
SID:
type: integer
'404':
description: Specimen not found
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: Specimen not found
data:
type: null
'500':
description: Server error
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: Failed to delete specimen
data:
type: null
/api/specimen/container:
get:
tags:
@ -3845,7 +3909,7 @@ paths:
responses:
'200':
description: Batch delete results
/api/tests:
/api/test:
get:
tags:
- Tests
@ -4170,7 +4234,7 @@ paths:
properties:
TestSiteId:
type: integer
/api/tests/{id}:
/api/test/{id}:
get:
tags:
- Tests
@ -4247,6 +4311,219 @@ paths:
description: Test not found
'422':
description: Test already disabled
/api/users:
get:
tags:
- Users
summary: List users with pagination and search
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
description: Page number
- name: per_page
in: query
schema:
type: integer
default: 20
description: Items per page
- name: search
in: query
schema:
type: string
description: Search term for username, email, or name
responses:
'200':
description: List of users with pagination
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Users retrieved successfully
data:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
type: object
properties:
current_page:
type: integer
per_page:
type: integer
total:
type: integer
total_pages:
type: integer
'500':
description: Server error
post:
tags:
- Users
summary: Create new user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserCreate'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: User created successfully
data:
type: object
properties:
UserID:
type: integer
Username:
type: string
Email:
type: string
'400':
description: Validation failed
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: Validation failed
data:
type: object
'500':
description: Server error
patch:
tags:
- Users
summary: Update existing user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdate'
responses:
'200':
description: User updated successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: User updated successfully
data:
type: object
properties:
UserID:
type: integer
updated_fields:
type: array
items:
type: string
'400':
description: UserID is required
'404':
description: User not found
'500':
description: Server error
/api/users/{id}:
get:
tags:
- Users
summary: Get user by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: User ID
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
'500':
description: Server error
delete:
tags:
- Users
summary: Delete user (soft delete)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: User ID
responses:
'200':
description: User deleted successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: User deleted successfully
data:
type: object
properties:
UserID:
type: integer
'404':
description: User not found
'500':
description: Server error
/api/valueset:
get:
tags:
@ -6268,6 +6545,124 @@ components:
WorkstationName:
type: string
description: Joined workstation name
User:
type: object
properties:
UserID:
type: integer
description: Unique user identifier
Username:
type: string
description: Unique login username
Email:
type: string
format: email
description: User email address
Name:
type: string
description: Full name of the user
Role:
type: string
description: User role (admin, technician, doctor, etc.)
Department:
type: string
description: Department name
IsActive:
type: boolean
description: Whether the user account is active
CreatedAt:
type: string
format: date-time
description: Creation timestamp
UpdatedAt:
type: string
format: date-time
description: Last update timestamp
DelDate:
type: string
format: date-time
nullable: true
description: Soft delete timestamp (null if active)
UserCreate:
type: object
required:
- Username
- Email
properties:
Username:
type: string
minLength: 3
maxLength: 50
description: Unique login username
Email:
type: string
format: email
maxLength: 100
description: User email address
Name:
type: string
description: Full name of the user
Role:
type: string
description: User role
Department:
type: string
description: Department name
IsActive:
type: boolean
default: true
description: Whether the user account is active
UserUpdate:
type: object
required:
- UserID
properties:
UserID:
type: integer
description: User ID to update
Email:
type: string
format: email
description: User email address
Name:
type: string
description: Full name of the user
Role:
type: string
description: User role
Department:
type: string
description: Department name
IsActive:
type: boolean
description: Whether the user account is active
UserListResponse:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Users retrieved successfully
data:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
type: object
properties:
current_page:
type: integer
per_page:
type: integer
total:
type: integer
total_pages:
type: integer
Contact:
type: object
properties:

View File

@ -53,6 +53,8 @@ tags:
description: Demo/test endpoints (no authentication)
- name: EquipmentList
description: Laboratory equipment and instrument management
- name: Users
description: User management and administration
components:
securitySchemes:
@ -170,6 +172,16 @@ components:
EquipmentList:
$ref: './components/schemas/equipmentlist.yaml#/EquipmentList'
# User schemas
User:
$ref: './components/schemas/user.yaml#/User'
UserCreate:
$ref: './components/schemas/user.yaml#/UserCreate'
UserUpdate:
$ref: './components/schemas/user.yaml#/UserUpdate'
UserListResponse:
$ref: './components/schemas/user.yaml#/UserListResponse'
# Paths are in separate files in the paths/ directory
# To view the complete API with all paths, use: api-docs.bundled.yaml
# To rebuild the bundle after changes: python bundle-api-docs.py

View File

@ -0,0 +1,121 @@
User:
type: object
properties:
UserID:
type: integer
description: Unique user identifier
Username:
type: string
description: Unique login username
Email:
type: string
format: email
description: User email address
Name:
type: string
description: Full name of the user
Role:
type: string
description: User role (admin, technician, doctor, etc.)
Department:
type: string
description: Department name
IsActive:
type: boolean
description: Whether the user account is active
CreatedAt:
type: string
format: date-time
description: Creation timestamp
UpdatedAt:
type: string
format: date-time
description: Last update timestamp
DelDate:
type: string
format: date-time
nullable: true
description: Soft delete timestamp (null if active)
UserCreate:
type: object
required:
- Username
- Email
properties:
Username:
type: string
minLength: 3
maxLength: 50
description: Unique login username
Email:
type: string
format: email
maxLength: 100
description: User email address
Name:
type: string
description: Full name of the user
Role:
type: string
description: User role
Department:
type: string
description: Department name
IsActive:
type: boolean
default: true
description: Whether the user account is active
UserUpdate:
type: object
required:
- UserID
properties:
UserID:
type: integer
description: User ID to update
Email:
type: string
format: email
description: User email address
Name:
type: string
description: Full name of the user
Role:
type: string
description: User role
Department:
type: string
description: Department name
IsActive:
type: boolean
description: Whether the user account is active
UserListResponse:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Users retrieved successfully
data:
type: object
properties:
users:
type: array
items:
$ref: '#/User'
pagination:
type: object
properties:
current_page:
type: integer
per_page:
type: integer
total:
type: integer
total_pages:
type: integer

View File

@ -54,6 +54,68 @@
'200':
description: Specimen details
delete:
tags: [Specimen]
summary: Delete specimen (soft delete)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Specimen ID (SID)
responses:
'200':
description: Specimen deleted successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Specimen deleted successfully
data:
type: object
properties:
SID:
type: integer
'404':
description: Specimen not found
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: Specimen not found
data:
type: null
'500':
description: Server error
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: Failed to delete specimen
data:
type: null
/api/specimen/container:
get:
tags: [Specimen]

View File

@ -1,4 +1,4 @@
/api/tests:
/api/test:
get:
tags: [Tests]
summary: List test definitions
@ -284,7 +284,7 @@
TestSiteId:
type: integer
/api/tests/{id}:
/api/test/{id}:
get:
tags: [Tests]
summary: Get test definition by ID

212
public/paths/users.yaml Normal file
View File

@ -0,0 +1,212 @@
/api/users:
get:
tags: [Users]
summary: List users with pagination and search
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
description: Page number
- name: per_page
in: query
schema:
type: integer
default: 20
description: Items per page
- name: search
in: query
schema:
type: string
description: Search term for username, email, or name
responses:
'200':
description: List of users with pagination
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Users retrieved successfully
data:
type: object
properties:
users:
type: array
items:
$ref: '../components/schemas/user.yaml#/User'
pagination:
type: object
properties:
current_page:
type: integer
per_page:
type: integer
total:
type: integer
total_pages:
type: integer
'500':
description: Server error
post:
tags: [Users]
summary: Create new user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/user.yaml#/UserCreate'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: User created successfully
data:
type: object
properties:
UserID:
type: integer
Username:
type: string
Email:
type: string
'400':
description: Validation failed
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: Validation failed
data:
type: object
'500':
description: Server error
patch:
tags: [Users]
summary: Update existing user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/user.yaml#/UserUpdate'
responses:
'200':
description: User updated successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: User updated successfully
data:
type: object
properties:
UserID:
type: integer
updated_fields:
type: array
items:
type: string
'400':
description: UserID is required
'404':
description: User not found
'500':
description: Server error
/api/users/{id}:
get:
tags: [Users]
summary: Get user by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: User ID
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: '../components/schemas/user.yaml#/User'
'404':
description: User not found
'500':
description: Server error
delete:
tags: [Users]
summary: Delete user (soft delete)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: User ID
responses:
'200':
description: User deleted successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: User deleted successfully
data:
type: object
properties:
UserID:
type: integer
'404':
description: User not found
'500':
description: Server error