295 lines
9.4 KiB
Go
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,
|
|
})
|
|
}
|