package handlers import ( "encoding/json" "net/http" "github.com/jmoiron/sqlx" "workorders/internal/model" ) type ProfileHandler struct{ db *sqlx.DB } func NewProfileHandler(db *sqlx.DB) *ProfileHandler { return &ProfileHandler{db: db} } const profileSelectCols = ` p.id, p.name, p.description, p.category, p.default_priority, p.default_duration_hours, p.default_instructions, p.active, p.created_at, p.updated_at, COUNT(s.id) AS step_count` const profileGroupBy = ` GROUP BY p.id, p.name, p.description, p.category, p.default_priority, p.default_duration_hours, p.default_instructions, p.active, p.created_at, p.updated_at` // ── Profile CRUD ────────────────────────────────────────────────────────────── func (h *ProfileHandler) List(w http.ResponseWriter, r *http.Request) { search := r.URL.Query().Get("search") all := r.URL.Query().Get("all") == "1" pat := "%" + search + "%" base := `SELECT ` + profileSelectCols + ` FROM wo_profiles p LEFT JOIN wo_profile_steps s ON s.profile_id = p.id` var rows []model.Profile var err error switch { case all && search != "": err = h.db.Select(&rows, base+` WHERE (p.name LIKE @p1 OR p.category LIKE @p1)`+profileGroupBy+` ORDER BY p.name`, pat) case all: err = h.db.Select(&rows, base+profileGroupBy+` ORDER BY p.name`) case search != "": err = h.db.Select(&rows, base+` WHERE p.active=1 AND (p.name LIKE @p1 OR p.category LIKE @p1)`+profileGroupBy+` ORDER BY p.name`, pat) default: err = h.db.Select(&rows, base+` WHERE p.active=1`+profileGroupBy+` ORDER BY p.name`) } if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } if rows == nil { rows = []model.Profile{} } respond(w, http.StatusOK, rows) } func (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request) { id, err := intParam(r, "id") if err != nil { respondError(w, http.StatusBadRequest, "invalid id") return } h.writeProfile(w, id) } func (h *ProfileHandler) Create(w http.ResponseWriter, r *http.Request) { var body model.Profile if err := decode(r, &body); err != nil || body.Name == "" { respondError(w, http.StatusBadRequest, "name required") return } if body.DefaultPriority == "" { body.DefaultPriority = "normal" } var id int err := h.db.QueryRow(` INSERT INTO wo_profiles (name, description, category, default_priority, default_duration_hours, default_instructions) OUTPUT INSERTED.id VALUES (@p1, @p2, @p3, @p4, @p5, @p6)`, body.Name, body.Description, body.Category, body.DefaultPriority, body.DefaultDurationHours, body.DefaultInstructions, ).Scan(&id) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } h.writeProfile(w, id) } func (h *ProfileHandler) Update(w http.ResponseWriter, r *http.Request) { id, err := intParam(r, "id") if err != nil { respondError(w, http.StatusBadRequest, "invalid id") return } var body model.Profile if err := decode(r, &body); err != nil || body.Name == "" { respondError(w, http.StatusBadRequest, "name required") return } if body.DefaultPriority == "" { body.DefaultPriority = "normal" } _, err = h.db.Exec(` UPDATE wo_profiles SET name=@p1, description=@p2, category=@p3, default_priority=@p4, default_duration_hours=@p5, default_instructions=@p6, active=@p7, updated_at=GETUTCDATE() WHERE id=@p8`, body.Name, body.Description, body.Category, body.DefaultPriority, body.DefaultDurationHours, body.DefaultInstructions, body.Active, id) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } h.writeProfile(w, id) } func (h *ProfileHandler) Delete(w http.ResponseWriter, r *http.Request) { id, err := intParam(r, "id") if err != nil { respondError(w, http.StatusBadRequest, "invalid id") return } h.db.Exec(`UPDATE wo_profiles SET active=0, updated_at=GETUTCDATE() WHERE id=@p1`, id) respond(w, http.StatusOK, map[string]any{"deactivated": id}) } // writeProfile fetches a single profile with its steps and writes it to w. func (h *ProfileHandler) writeProfile(w http.ResponseWriter, id int) { var p model.Profile err := h.db.Get(&p, `SELECT `+profileSelectCols+` FROM wo_profiles p LEFT JOIN wo_profile_steps s ON s.profile_id = p.id WHERE p.id = @p1`+profileGroupBy, id) if err != nil { respondError(w, http.StatusNotFound, "profile not found") return } if err := h.db.Select(&p.Steps, `SELECT * FROM wo_profile_steps WHERE profile_id=@p1 ORDER BY step_order`, id); err != nil { p.Steps = []model.ProfileStep{} } respond(w, http.StatusOK, p) } // ── Profile Steps ───────────────────────────────────────────────────────────── func (h *ProfileHandler) ListSteps(w http.ResponseWriter, r *http.Request) { id, err := intParam(r, "id") if err != nil { respondError(w, http.StatusBadRequest, "invalid id") return } var steps []model.ProfileStep if err := h.db.Select(&steps, `SELECT * FROM wo_profile_steps WHERE profile_id=@p1 ORDER BY step_order`, id); err != nil { steps = []model.ProfileStep{} } respond(w, http.StatusOK, steps) } func (h *ProfileHandler) CreateStep(w http.ResponseWriter, r *http.Request) { profileID, err := intParam(r, "id") if err != nil { respondError(w, http.StatusBadRequest, "invalid id") return } var body model.ProfileStep if err := decode(r, &body); err != nil || body.Title == "" { 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, 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()) return } var step model.ProfileStep h.db.Get(&step, `SELECT * FROM wo_profile_steps WHERE id=@p1`, sid) respond(w, http.StatusCreated, step) } func (h *ProfileHandler) UpdateStep(w http.ResponseWriter, r *http.Request) { sid, err := intParam(r, "sid") if err != nil { respondError(w, http.StatusBadRequest, "invalid sid") return } var body model.ProfileStep if err := decode(r, &body); err != nil || body.Title == "" { respondError(w, http.StatusBadRequest, "title required") return } 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) } func (h *ProfileHandler) DeleteStep(w http.ResponseWriter, r *http.Request) { sid, err := intParam(r, "sid") if err != nil { respondError(w, http.StatusBadRequest, "invalid sid") return } h.db.Exec(`DELETE FROM wo_profile_steps WHERE id=@p1`, sid) respond(w, http.StatusOK, map[string]any{"deleted": sid}) } // ── Apply Profile to Work Order ─────────────────────────────────────────────── func (h *ProfileHandler) Apply(w http.ResponseWriter, r *http.Request) { woID, err := intParam(r, "id") if err != nil { respondError(w, http.StatusBadRequest, "invalid wo id") return } profileID, err := intParam(r, "profileId") if err != nil { respondError(w, http.StatusBadRequest, "invalid profile id") return } var body struct { Mode string `json:"mode"` // "append" | "replace" } json.NewDecoder(r.Body).Decode(&body) if body.Mode != "replace" { body.Mode = "append" } var p model.Profile if err := h.db.Get(&p, `SELECT * FROM wo_profiles WHERE id=@p1 AND active=1`, profileID); err != nil { respondError(w, http.StatusNotFound, "profile not found") return } var steps []model.ProfileStep h.db.Select(&steps, `SELECT * FROM wo_profile_steps WHERE profile_id=@p1 ORDER BY step_order`, profileID) if body.Mode == "replace" { h.db.Exec(`DELETE FROM wo_steps WHERE wo_id=@p1`, woID) } var maxOrder int 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, 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 h.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`, p.DefaultInstructions, p.DefaultPriority, woID) respond(w, http.StatusOK, map[string]any{ "applied": profileID, "steps_added": len(steps), "mode": body.Mode, }) }