Add migration scripts, activity handler, and registry components for equipment, materials, and people

This commit is contained in:
2026-05-17 10:11:56 -04:00
parent fb67c76f45
commit 17e05cb61d
28 changed files with 3777 additions and 34 deletions
+152 -1
View File
@@ -3331,4 +3331,155 @@ require (
---
*Start with Phase 1 in order. Each checkbox is one commit. Don't skip CLAUDE.md — it must exist before any code is written.*
*Start with Phase 1 in order. Each checkbox is one commit. Don't skip CLAUDE.md — it must exist before any code is written.*
---
## Work Order Profiles
### Overview
A **Work Order Profile** is a reusable template that defines the default structure and behavior for a category of work orders. When a profile is applied to a work order (at creation or post-creation), its steps, instructions, priority, and duration are loaded in — then customized as needed.
> **Key idea:** Profile = preset + flexibility. The profile defines the standard; each WO can deviate from it.
### Database
```sql
-- Profiles
CREATE TABLE wo_profiles (
id INT IDENTITY PRIMARY KEY,
name NVARCHAR(200) NOT NULL,
description NVARCHAR(MAX),
category NVARCHAR(100),
default_priority NVARCHAR(10) NOT NULL DEFAULT 'normal',
default_duration_hours INT,
default_instructions NVARCHAR(MAX),
active BIT NOT NULL DEFAULT 1,
created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
CREATE TABLE wo_profile_steps (
id INT IDENTITY PRIMARY KEY,
profile_id INT NOT NULL REFERENCES wo_profiles(id) ON DELETE CASCADE,
step_order INT NOT NULL,
title NVARCHAR(200) NOT NULL,
description NVARCHAR(MAX),
required BIT NOT NULL DEFAULT 1
);
CREATE INDEX ix_profile_steps ON wo_profile_steps (profile_id, step_order);
```
### API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/profiles` | List active profiles (search, category filter) |
| POST | `/api/profiles` | Create profile |
| GET | `/api/profiles/{id}` | Get profile with its steps |
| PUT | `/api/profiles/{id}` | Update profile fields |
| DELETE | `/api/profiles/{id}` | Soft delete (set active=0) |
| GET | `/api/profiles/{id}/steps` | List profile steps |
| POST | `/api/profiles/{id}/steps` | Add step to profile |
| PUT | `/api/profiles/{id}/steps/{sid}` | Update profile step |
| DELETE | `/api/profiles/{id}/steps/{sid}` | Remove step from profile |
| POST | `/api/work-orders/{id}/apply-profile/{profileId}` | Apply profile to WO |
#### apply-profile behavior
Request body: `{ "mode": "append" | "replace" }`
- **append** — inserts profile steps after existing steps (step_order continues from current max)
- **replace** — deletes all existing steps, then inserts profile steps starting at order 1
- Both modes update `instructions` if currently blank and `priority` if WO is still `draft`
### Frontend Components
**`web/components/registry/profile-list.mjs`**
- List page at `/registry/profiles`
- Cards: name, category badge, step count, active status
- Search + category filter, edit/deactivate actions, "New Profile" button
**`web/components/registry/profile-form.mjs`**
- Modal dialog for create/edit
- Fields: name (required), description, category, default priority, default duration hours, default instructions
- Inline step editor: ordered step list with add / edit / reorder / delete
- Active toggle in edit mode
**WO Form integration (`wo-form.mjs`)**
- "Load Profile" ghost button at top of form (shown only on new WOs or draft status)
- Searchable profile picker dialog — on confirm pre-fills priority and instructions (if blank)
- Steps applied server-side via apply-profile after the WO saves; checklist tab then refreshes
**WO Detail integration (`wo-detail.mjs`)**
- "Apply Profile" button in header actions alongside Edit / Change Status
- Two-step dialog: (1) pick profile, (2) if WO has existing steps — prompt append vs replace
- Calls `POST /api/work-orders/{id}/apply-profile/{profileId}` then refreshes Checklist tab
### Sidebar Nav
Add under Resources section:
```
Profiles /registry/profiles (layout-template icon)
```
### Go Models
```go
type Profile struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Category string `db:"category" json:"category"`
DefaultPriority string `db:"default_priority" json:"default_priority"`
DefaultDurationHours *int `db:"default_duration_hours" json:"default_duration_hours"`
DefaultInstructions string `db:"default_instructions" json:"default_instructions"`
Active bool `db:"active" json:"active"`
Steps []ProfileStep `db:"-" json:"steps,omitempty"`
}
type ProfileStep struct {
ID int `db:"id" json:"id"`
ProfileID int `db:"profile_id" json:"profile_id"`
StepOrder int `db:"step_order" json:"step_order"`
Title string `db:"title" json:"title"`
Description string `db:"description" json:"description"`
Required bool `db:"required" json:"required"`
}
```
### apply-profile Handler (pseudocode)
```go
func (h *ProfileHandler) Apply(w http.ResponseWriter, r *http.Request) {
woID, profileID := intParam(r, "id"), intParam(r, "profileId")
var body struct { Mode string `json:"mode"` }
json.NewDecoder(r.Body).Decode(&body)
profile := // load profile + steps
if body.Mode == "replace" {
db.Exec(`DELETE FROM wo_steps WHERE wo_id = @p1`, woID)
}
var maxOrder int
db.Get(&maxOrder, `SELECT ISNULL(MAX(step_order), 0) FROM wo_steps WHERE wo_id = @p1`, woID)
for _, s := range profile.Steps {
db.Exec(`INSERT INTO wo_steps (wo_id, step_order, title, description, required)
VALUES (@p1, @p2, @p3, @p4, @p5)`,
woID, maxOrder+s.StepOrder, s.Title, s.Description, s.Required)
}
db.Exec(`
UPDATE work_orders SET
instructions = CASE WHEN (instructions IS NULL OR instructions = '') THEN @p1 ELSE instructions END,
priority = CASE WHEN status = 'draft' THEN @p2 ELSE priority END,
updated_at = GETUTCDATE()
WHERE id = @p3`,
profile.DefaultInstructions, profile.DefaultPriority, woID)
}
```