Add migration scripts, activity handler, and registry components for equipment, materials, and people
This commit is contained in:
@@ -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)
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user