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:
parent
85c7e96405
commit
282c642da6
2
.gitignore
vendored
2
.gitignore
vendored
@ -125,5 +125,3 @@ _modules/*
|
||||
/results/
|
||||
/phpunit*.xml
|
||||
/public/.htaccess
|
||||
|
||||
/.serena
|
||||
149
.opencode/command/opsx-apply.md
Normal file
149
.opencode/command/opsx-apply.md
Normal 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
|
||||
154
.opencode/command/opsx-archive.md
Normal file
154
.opencode/command/opsx-archive.md
Normal 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
|
||||
170
.opencode/command/opsx-explore.md
Normal file
170
.opencode/command/opsx-explore.md
Normal 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
|
||||
103
.opencode/command/opsx-propose.md
Normal file
103
.opencode/command/opsx-propose.md
Normal 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
|
||||
156
.opencode/skills/openspec-apply-change/SKILL.md
Normal file
156
.opencode/skills/openspec-apply-change/SKILL.md
Normal 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
|
||||
114
.opencode/skills/openspec-archive-change/SKILL.md
Normal file
114
.opencode/skills/openspec-archive-change/SKILL.md
Normal 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
|
||||
288
.opencode/skills/openspec-explore/SKILL.md
Normal file
288
.opencode/skills/openspec-explore/SKILL.md
Normal 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
|
||||
110
.opencode/skills/openspec-propose/SKILL.md
Normal file
110
.opencode/skills/openspec-propose/SKILL.md
Normal 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
1
.serena/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/cache
|
||||
419
.serena/memories/architecture_structure.md
Normal file
419
.serena/memories/architecture_structure.md
Normal 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
|
||||
481
.serena/memories/code_style_conventions.md
Normal file
481
.serena/memories/code_style_conventions.md
Normal 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]';
|
||||
}
|
||||
```
|
||||
669
.serena/memories/important_patterns.md
Normal file
669
.serena/memories/important_patterns.md
Normal 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.
|
||||
```
|
||||
55
.serena/memories/project_overview.md
Normal file
55
.serena/memories/project_overview.md
Normal 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)
|
||||
393
.serena/memories/suggested_commands.md
Normal file
393
.serena/memories/suggested_commands.md
Normal 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
|
||||
227
.serena/memories/task_completion_checklist.md
Normal file
227
.serena/memories/task_completion_checklist.md
Normal 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
127
.serena/project.yml
Normal 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 read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
334
TODO.md
334
TODO.md
@ -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
|
||||
@ -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
|
||||
@ -293,6 +302,7 @@ $routes->group('api', function ($routes) {
|
||||
$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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
293
app/Controllers/User/UserController.php
Normal file
293
app/Controllers/User/UserController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
138
app/Models/User/UserModel.php
Normal file
138
app/Models/User/UserModel.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-08
|
||||
@ -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
|
||||
55
openspec/changes/archive/2026-03-08-backend-specs/README.md
Normal file
55
openspec/changes/archive/2026-03-08-backend-specs/README.md
Normal 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.
|
||||
@ -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');
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
165
openspec/changes/archive/2026-03-08-backend-specs/design.md
Normal file
165
openspec/changes/archive/2026-03-08-backend-specs/design.md
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
92
openspec/changes/archive/2026-03-08-backend-specs/tasks.md
Normal file
92
openspec/changes/archive/2026-03-08-backend-specs/tasks.md
Normal 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
|
||||
243
openspec/changes/archive/2026-03-08-backend-specs/user-api.md
Normal file
243
openspec/changes/archive/2026-03-08-backend-specs/user-api.md
Normal 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
20
openspec/config.yaml
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
121
public/components/schemas/user.yaml
Normal file
121
public/components/schemas/user.yaml
Normal 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
|
||||
@ -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]
|
||||
|
||||
@ -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
212
public/paths/users.yaml
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user