Files
workorders/internal/api/handlers/profile.go
T

295 lines
9.4 KiB
Go

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,
})
}