diff --git a/internal/api/handlers/profile.go b/internal/api/handlers/profile.go index c2af61c..883ded1 100644 --- a/internal/api/handlers/profile.go +++ b/internal/api/handlers/profile.go @@ -178,14 +178,17 @@ func (h *ProfileHandler) CreateStep(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusBadRequest, "title required") return } + if body.StepType == "" { + body.StepType = "work_step" + } var maxOrder int h.db.QueryRow(`SELECT ISNULL(MAX(step_order),0) FROM wo_profile_steps WHERE profile_id=@p1`, profileID).Scan(&maxOrder) var sid int err = h.db.QueryRow(` - INSERT INTO wo_profile_steps (profile_id, step_order, title, description, required) - OUTPUT INSERTED.id VALUES (@p1, @p2, @p3, @p4, @p5)`, - profileID, maxOrder+1, body.Title, body.Description, body.Required, + INSERT INTO wo_profile_steps (profile_id, step_order, title, description, required, step_type, type_config) + OUTPUT INSERTED.id VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7)`, + profileID, maxOrder+1, body.Title, body.Description, body.Required, body.StepType, body.TypeConfig, ).Scan(&sid) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) @@ -207,8 +210,11 @@ func (h *ProfileHandler) UpdateStep(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusBadRequest, "title required") return } - h.db.Exec(`UPDATE wo_profile_steps SET title=@p1, description=@p2, required=@p3, step_order=@p4 WHERE id=@p5`, - body.Title, body.Description, body.Required, body.StepOrder, sid) + if body.StepType == "" { + body.StepType = "work_step" + } + h.db.Exec(`UPDATE wo_profile_steps SET title=@p1, description=@p2, required=@p3, step_order=@p4, step_type=@p5, type_config=@p6 WHERE id=@p7`, + body.Title, body.Description, body.Required, body.StepOrder, body.StepType, body.TypeConfig, sid) var step model.ProfileStep h.db.Get(&step, `SELECT * FROM wo_profile_steps WHERE id=@p1`, sid) respond(w, http.StatusOK, step) @@ -262,10 +268,13 @@ func (h *ProfileHandler) Apply(w http.ResponseWriter, r *http.Request) { h.db.QueryRow(`SELECT ISNULL(MAX(step_order),0) FROM wo_steps WHERE wo_id=@p1`, woID).Scan(&maxOrder) for _, s := range steps { + if s.StepType == "" { + s.StepType = "work_step" + } h.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) + INSERT INTO wo_steps (wo_id, step_order, title, description, required, step_type, type_config) + VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7)`, + woID, maxOrder+s.StepOrder, s.Title, s.Description, s.Required, s.StepType, s.TypeConfig) } // Fill instructions if blank; update priority only on draft WOs diff --git a/internal/api/handlers/step.go b/internal/api/handlers/step.go index 8913813..0a09f21 100644 --- a/internal/api/handlers/step.go +++ b/internal/api/handlers/step.go @@ -41,11 +41,14 @@ func (h *StepHandler) Create(w http.ResponseWriter, r *http.Request) { var maxOrder int h.db.QueryRow(`SELECT ISNULL(MAX(step_order),0) FROM wo_steps WHERE wo_id=@p1`, woID).Scan(&maxOrder) + if body.StepType == "" { + body.StepType = "work_step" + } var sid int err = h.db.QueryRow(` - INSERT INTO wo_steps (wo_id,step_order,title,description,required) - OUTPUT INSERTED.id VALUES (@p1,@p2,@p3,@p4,@p5)`, - woID, maxOrder+1, body.Title, body.Description, body.Required, + INSERT INTO wo_steps (wo_id,step_order,title,description,required,step_type,type_config) + OUTPUT INSERTED.id VALUES (@p1,@p2,@p3,@p4,@p5,@p6,@p7)`, + woID, maxOrder+1, body.Title, body.Description, body.Required, body.StepType, body.TypeConfig, ).Scan(&sid) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) diff --git a/internal/model/models.go b/internal/model/models.go index af6e68e..7839fee 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -56,6 +56,8 @@ type Step struct { Title string `db:"title" json:"title"` Description string `db:"description" json:"description"` Required bool `db:"required" json:"required"` + StepType string `db:"step_type" json:"step_type"` + TypeConfig string `db:"type_config" json:"type_config"` Completed bool `db:"completed" json:"completed"` CompletedBy *string `db:"completed_by" json:"completed_by"` CompletedAt *time.Time `db:"completed_at" json:"completed_at"` @@ -166,6 +168,8 @@ type ProfileStep struct { Title string `db:"title" json:"title"` Description string `db:"description" json:"description"` Required bool `db:"required" json:"required"` + StepType string `db:"step_type" json:"step_type"` + TypeConfig string `db:"type_config" json:"type_config"` } // ── Users & Auth ────────────────────────────────────────────────────────────── diff --git a/internal/repository/migrations/007_profiles.sql b/internal/repository/migrations/007_profiles.sql new file mode 100644 index 0000000..1d562f8 --- /dev/null +++ b/internal/repository/migrations/007_profiles.sql @@ -0,0 +1,31 @@ +-- Work Order Profiles + +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'wo_profiles') +BEGIN + 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() + ); +END + +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'wo_profile_steps') +BEGIN + 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); +END diff --git a/internal/repository/migrations/008_step_types.sql b/internal/repository/migrations/008_step_types.sql new file mode 100644 index 0000000..fd946f3 --- /dev/null +++ b/internal/repository/migrations/008_step_types.sql @@ -0,0 +1,15 @@ +-- Add step_type and type_config to profile steps and WO steps + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_profile_steps') AND name = 'step_type') + ALTER TABLE wo_profile_steps ADD step_type NVARCHAR(30) NOT NULL DEFAULT 'work_step'; + -- 'work_step' | 'photo' | 'inspection' | 'note' + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_profile_steps') AND name = 'type_config') + ALTER TABLE wo_profile_steps ADD type_config NVARCHAR(MAX) NULL; + -- JSON; shape depends on step_type (see CLAUDE.md) + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_steps') AND name = 'step_type') + ALTER TABLE wo_steps ADD step_type NVARCHAR(30) NOT NULL DEFAULT 'work_step'; + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_steps') AND name = 'type_config') + ALTER TABLE wo_steps ADD type_config NVARCHAR(MAX) NULL; diff --git a/migrations/008_step_types.sql b/migrations/008_step_types.sql new file mode 100644 index 0000000..fd946f3 --- /dev/null +++ b/migrations/008_step_types.sql @@ -0,0 +1,15 @@ +-- Add step_type and type_config to profile steps and WO steps + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_profile_steps') AND name = 'step_type') + ALTER TABLE wo_profile_steps ADD step_type NVARCHAR(30) NOT NULL DEFAULT 'work_step'; + -- 'work_step' | 'photo' | 'inspection' | 'note' + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_profile_steps') AND name = 'type_config') + ALTER TABLE wo_profile_steps ADD type_config NVARCHAR(MAX) NULL; + -- JSON; shape depends on step_type (see CLAUDE.md) + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_steps') AND name = 'step_type') + ALTER TABLE wo_steps ADD step_type NVARCHAR(30) NOT NULL DEFAULT 'work_step'; + +IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_steps') AND name = 'type_config') + ALTER TABLE wo_steps ADD type_config NVARCHAR(MAX) NULL; diff --git a/web/components/registry/profile-form.mjs b/web/components/registry/profile-form.mjs index 2138a34..cf0210a 100644 --- a/web/components/registry/profile-form.mjs +++ b/web/components/registry/profile-form.mjs @@ -2,6 +2,13 @@ import { api } from '../../lib/api.mjs'; const PRIORITIES = ['low', 'normal', 'high', 'urgent']; +const STEP_TYPES = [ + { value: 'work_step', label: 'Work Step', icon: 'check-square', color: '#0A7EA4', hint: '' }, + { value: 'photo', label: 'Photo', icon: 'camera', color: '#8B5CF6', hint: 'Requires a photo to complete' }, + { value: 'inspection', label: 'Inspect', icon: 'clipboard-check', color: '#E07B39', hint: 'Pass / Fail / N/A' }, + { value: 'note', label: 'Note', icon: 'file-text', color: '#64748B', hint: 'Free-text entry' }, +]; + class ProfileForm extends HTMLElement { #onSave = null; @@ -19,7 +26,7 @@ class ProfileForm extends HTMLElement { d.innerHTML = ` ${allDone ? `