Add migration scripts, activity handler, and registry components for equipment, materials, and people
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"workorders/internal/model"
|
||||
)
|
||||
|
||||
type ActivityHandler struct{ db *sqlx.DB }
|
||||
|
||||
func NewActivityHandler(db *sqlx.DB) *ActivityHandler { return &ActivityHandler{db: db} }
|
||||
|
||||
func (h *ActivityHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
woID, err := intParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var rows []model.AuditEntry
|
||||
err = h.db.Select(&rows, `
|
||||
SELECT
|
||||
a.id,
|
||||
a.action,
|
||||
ISNULL(a.old_value,'') AS old_value,
|
||||
ISNULL(a.new_value,'') AS new_value,
|
||||
ISNULL(u.display_name, ISNULL(u.username, CAST(a.performed_by AS NVARCHAR(100)))) AS performed_by,
|
||||
a.performed_at
|
||||
FROM wo_audit_log a
|
||||
LEFT JOIN users u ON u.id = a.performed_by
|
||||
WHERE a.wo_id = @p1
|
||||
ORDER BY a.performed_at DESC`,
|
||||
woID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []model.AuditEntry{}
|
||||
}
|
||||
respond(w, http.StatusOK, rows)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
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
|
||||
}
|
||||
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,
|
||||
).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
|
||||
}
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
@@ -11,26 +11,298 @@ type RegistryHandler struct{ db *sqlx.DB }
|
||||
|
||||
func NewRegistryHandler(db *sqlx.DB) *RegistryHandler { return &RegistryHandler{db: db} }
|
||||
|
||||
func (h *RegistryHandler) People(w http.ResponseWriter, r *http.Request) {
|
||||
// ── People ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *RegistryHandler) ListPeople(w http.ResponseWriter, r *http.Request) {
|
||||
search := r.URL.Query().Get("search")
|
||||
all := r.URL.Query().Get("all") == "1"
|
||||
|
||||
var rows []model.RegistryPerson
|
||||
h.db.Select(&rows, `SELECT id,name,role,email,phone FROM resource_people WHERE active=1 ORDER BY name`)
|
||||
var err error
|
||||
pat := "%" + search + "%"
|
||||
if all && search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,name,role,email,phone,active FROM resource_people WHERE (name LIKE @p1 OR email LIKE @p1 OR role LIKE @p1) ORDER BY name`, pat)
|
||||
} else if all {
|
||||
err = h.db.Select(&rows, `SELECT id,name,role,email,phone,active FROM resource_people ORDER BY name`)
|
||||
} else if search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,name,role,email,phone,active FROM resource_people WHERE active=1 AND (name LIKE @p1 OR email LIKE @p1 OR role LIKE @p1) ORDER BY name`, pat)
|
||||
} else {
|
||||
err = h.db.Select(&rows, `SELECT id,name,role,email,phone,active FROM resource_people WHERE active=1 ORDER BY name`)
|
||||
}
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []model.RegistryPerson{}
|
||||
}
|
||||
respond(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) Vehicles(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *RegistryHandler) CreatePerson(w http.ResponseWriter, r *http.Request) {
|
||||
var body model.RegistryPerson
|
||||
if err := decode(r, &body); err != nil || body.Name == "" {
|
||||
respondError(w, http.StatusBadRequest, "name required")
|
||||
return
|
||||
}
|
||||
var id int
|
||||
err := h.db.QueryRow(`INSERT INTO resource_people (name,role,email,phone) OUTPUT INSERTED.id VALUES (@p1,@p2,@p3,@p4)`,
|
||||
body.Name, body.Role, body.Email, body.Phone).Scan(&id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
var person model.RegistryPerson
|
||||
h.db.Get(&person, `SELECT id,name,role,email,phone,active FROM resource_people WHERE id=@p1`, id)
|
||||
respond(w, http.StatusCreated, person)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) UpdatePerson(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := intParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var body model.RegistryPerson
|
||||
if err := decode(r, &body); err != nil || body.Name == "" {
|
||||
respondError(w, http.StatusBadRequest, "name required")
|
||||
return
|
||||
}
|
||||
h.db.Exec(`UPDATE resource_people SET name=@p1,role=@p2,email=@p3,phone=@p4,active=@p5 WHERE id=@p6`,
|
||||
body.Name, body.Role, body.Email, body.Phone, body.Active, id)
|
||||
var person model.RegistryPerson
|
||||
h.db.Get(&person, `SELECT id,name,role,email,phone,active FROM resource_people WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, person)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) DeletePerson(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 resource_people SET active=0 WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, map[string]any{"deactivated": id})
|
||||
}
|
||||
|
||||
// ── Vehicles ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *RegistryHandler) ListVehicles(w http.ResponseWriter, r *http.Request) {
|
||||
search := r.URL.Query().Get("search")
|
||||
all := r.URL.Query().Get("all") == "1"
|
||||
|
||||
var rows []model.RegistryVehicle
|
||||
h.db.Select(&rows, `SELECT id,unit_number,description,vehicle_type FROM resource_vehicles WHERE active=1 ORDER BY unit_number`)
|
||||
var err error
|
||||
pat := "%" + search + "%"
|
||||
if all && search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,unit_number,description,vehicle_type,active FROM resource_vehicles WHERE (unit_number LIKE @p1 OR description LIKE @p1 OR vehicle_type LIKE @p1) ORDER BY unit_number`, pat)
|
||||
} else if all {
|
||||
err = h.db.Select(&rows, `SELECT id,unit_number,description,vehicle_type,active FROM resource_vehicles ORDER BY unit_number`)
|
||||
} else if search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,unit_number,description,vehicle_type,active FROM resource_vehicles WHERE active=1 AND (unit_number LIKE @p1 OR description LIKE @p1) ORDER BY unit_number`, pat)
|
||||
} else {
|
||||
err = h.db.Select(&rows, `SELECT id,unit_number,description,vehicle_type,active FROM resource_vehicles WHERE active=1 ORDER BY unit_number`)
|
||||
}
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []model.RegistryVehicle{}
|
||||
}
|
||||
respond(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) Equipment(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *RegistryHandler) CreateVehicle(w http.ResponseWriter, r *http.Request) {
|
||||
var body model.RegistryVehicle
|
||||
if err := decode(r, &body); err != nil || body.UnitNumber == "" {
|
||||
respondError(w, http.StatusBadRequest, "unit_number required")
|
||||
return
|
||||
}
|
||||
var id int
|
||||
err := h.db.QueryRow(`INSERT INTO resource_vehicles (unit_number,description,vehicle_type) OUTPUT INSERTED.id VALUES (@p1,@p2,@p3)`,
|
||||
body.UnitNumber, body.Description, body.VehicleType).Scan(&id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
var v model.RegistryVehicle
|
||||
h.db.Get(&v, `SELECT id,unit_number,description,vehicle_type,active FROM resource_vehicles WHERE id=@p1`, id)
|
||||
respond(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) UpdateVehicle(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := intParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var body model.RegistryVehicle
|
||||
if err := decode(r, &body); err != nil || body.UnitNumber == "" {
|
||||
respondError(w, http.StatusBadRequest, "unit_number required")
|
||||
return
|
||||
}
|
||||
h.db.Exec(`UPDATE resource_vehicles SET unit_number=@p1,description=@p2,vehicle_type=@p3,active=@p4 WHERE id=@p5`,
|
||||
body.UnitNumber, body.Description, body.VehicleType, body.Active, id)
|
||||
var v model.RegistryVehicle
|
||||
h.db.Get(&v, `SELECT id,unit_number,description,vehicle_type,active FROM resource_vehicles WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) DeleteVehicle(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 resource_vehicles SET active=0 WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, map[string]any{"deactivated": id})
|
||||
}
|
||||
|
||||
// ── Equipment ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *RegistryHandler) ListEquipment(w http.ResponseWriter, r *http.Request) {
|
||||
search := r.URL.Query().Get("search")
|
||||
all := r.URL.Query().Get("all") == "1"
|
||||
|
||||
var rows []model.RegistryEquipment
|
||||
h.db.Select(&rows, `SELECT id,name,asset_tag,category FROM resource_equipment WHERE active=1 ORDER BY name`)
|
||||
var err error
|
||||
pat := "%" + search + "%"
|
||||
if all && search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,name,asset_tag,category,active FROM resource_equipment WHERE (name LIKE @p1 OR asset_tag LIKE @p1 OR category LIKE @p1) ORDER BY name`, pat)
|
||||
} else if all {
|
||||
err = h.db.Select(&rows, `SELECT id,name,asset_tag,category,active FROM resource_equipment ORDER BY name`)
|
||||
} else if search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,name,asset_tag,category,active FROM resource_equipment WHERE active=1 AND (name LIKE @p1 OR asset_tag LIKE @p1) ORDER BY name`, pat)
|
||||
} else {
|
||||
err = h.db.Select(&rows, `SELECT id,name,asset_tag,category,active FROM resource_equipment WHERE active=1 ORDER BY name`)
|
||||
}
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []model.RegistryEquipment{}
|
||||
}
|
||||
respond(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) Materials(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *RegistryHandler) CreateEquipment(w http.ResponseWriter, r *http.Request) {
|
||||
var body model.RegistryEquipment
|
||||
if err := decode(r, &body); err != nil || body.Name == "" {
|
||||
respondError(w, http.StatusBadRequest, "name required")
|
||||
return
|
||||
}
|
||||
var id int
|
||||
err := h.db.QueryRow(`INSERT INTO resource_equipment (name,asset_tag,category) OUTPUT INSERTED.id VALUES (@p1,@p2,@p3)`,
|
||||
body.Name, body.AssetTag, body.Category).Scan(&id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
var eq model.RegistryEquipment
|
||||
h.db.Get(&eq, `SELECT id,name,asset_tag,category,active FROM resource_equipment WHERE id=@p1`, id)
|
||||
respond(w, http.StatusCreated, eq)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) UpdateEquipment(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := intParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var body model.RegistryEquipment
|
||||
if err := decode(r, &body); err != nil || body.Name == "" {
|
||||
respondError(w, http.StatusBadRequest, "name required")
|
||||
return
|
||||
}
|
||||
h.db.Exec(`UPDATE resource_equipment SET name=@p1,asset_tag=@p2,category=@p3,active=@p4 WHERE id=@p5`,
|
||||
body.Name, body.AssetTag, body.Category, body.Active, id)
|
||||
var eq model.RegistryEquipment
|
||||
h.db.Get(&eq, `SELECT id,name,asset_tag,category,active FROM resource_equipment WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, eq)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) DeleteEquipment(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 resource_equipment SET active=0 WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, map[string]any{"deactivated": id})
|
||||
}
|
||||
|
||||
// ── Materials ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *RegistryHandler) ListMaterials(w http.ResponseWriter, r *http.Request) {
|
||||
search := r.URL.Query().Get("search")
|
||||
all := r.URL.Query().Get("all") == "1"
|
||||
|
||||
var rows []model.RegistryMaterial
|
||||
h.db.Select(&rows, `SELECT id,name,unit,part_number FROM resource_materials WHERE active=1 ORDER BY name`)
|
||||
var err error
|
||||
pat := "%" + search + "%"
|
||||
if all && search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,name,unit,part_number,active FROM resource_materials WHERE (name LIKE @p1 OR part_number LIKE @p1) ORDER BY name`, pat)
|
||||
} else if all {
|
||||
err = h.db.Select(&rows, `SELECT id,name,unit,part_number,active FROM resource_materials ORDER BY name`)
|
||||
} else if search != "" {
|
||||
err = h.db.Select(&rows, `SELECT id,name,unit,part_number,active FROM resource_materials WHERE active=1 AND (name LIKE @p1 OR part_number LIKE @p1) ORDER BY name`, pat)
|
||||
} else {
|
||||
err = h.db.Select(&rows, `SELECT id,name,unit,part_number,active FROM resource_materials WHERE active=1 ORDER BY name`)
|
||||
}
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []model.RegistryMaterial{}
|
||||
}
|
||||
respond(w, http.StatusOK, rows)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) CreateMaterial(w http.ResponseWriter, r *http.Request) {
|
||||
var body model.RegistryMaterial
|
||||
if err := decode(r, &body); err != nil || body.Name == "" {
|
||||
respondError(w, http.StatusBadRequest, "name required")
|
||||
return
|
||||
}
|
||||
var id int
|
||||
err := h.db.QueryRow(`INSERT INTO resource_materials (name,unit,part_number) OUTPUT INSERTED.id VALUES (@p1,@p2,@p3)`,
|
||||
body.Name, body.Unit, body.PartNumber).Scan(&id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
var m model.RegistryMaterial
|
||||
h.db.Get(&m, `SELECT id,name,unit,part_number,active FROM resource_materials WHERE id=@p1`, id)
|
||||
respond(w, http.StatusCreated, m)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) UpdateMaterial(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := intParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var body model.RegistryMaterial
|
||||
if err := decode(r, &body); err != nil || body.Name == "" {
|
||||
respondError(w, http.StatusBadRequest, "name required")
|
||||
return
|
||||
}
|
||||
h.db.Exec(`UPDATE resource_materials SET name=@p1,unit=@p2,part_number=@p3,active=@p4 WHERE id=@p5`,
|
||||
body.Name, body.Unit, body.PartNumber, body.Active, id)
|
||||
var m model.RegistryMaterial
|
||||
h.db.Get(&m, `SELECT id,name,unit,part_number,active FROM resource_materials WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, m)
|
||||
}
|
||||
|
||||
func (h *RegistryHandler) DeleteMaterial(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 resource_materials SET active=0 WHERE id=@p1`, id)
|
||||
respond(w, http.StatusOK, map[string]any{"deactivated": id})
|
||||
}
|
||||
|
||||
@@ -93,6 +93,18 @@ func (h *StepHandler) Complete(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, http.StatusOK, step)
|
||||
}
|
||||
|
||||
func (h *StepHandler) Uncomplete(w http.ResponseWriter, r *http.Request) {
|
||||
sid, err := intParam(r, "sid")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid sid")
|
||||
return
|
||||
}
|
||||
h.db.Exec(`UPDATE wo_steps SET completed=0,completed_by=NULL,completed_at=NULL,notes='' WHERE id=@p1`, sid)
|
||||
var step model.Step
|
||||
h.db.Get(&step, `SELECT * FROM wo_steps WHERE id=@p1`, sid)
|
||||
respond(w, http.StatusOK, step)
|
||||
}
|
||||
|
||||
func (h *StepHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
sid, err := intParam(r, "sid")
|
||||
if err != nil {
|
||||
|
||||
+38
-5
@@ -20,6 +20,9 @@ func NewRouter(cfg *config.Config, db *sqlx.DB) http.Handler {
|
||||
// Serve frontend static files
|
||||
r.Handle("/*", http.FileServer(http.Dir("./web")))
|
||||
|
||||
// Public routes — uploads served without auth (paths are UUIDs, not guessable)
|
||||
r.Handle("/uploads/*", http.StripPrefix("/uploads/", http.FileServer(http.Dir(cfg.UploadPath))))
|
||||
|
||||
// Public auth routes
|
||||
auth := handlers.NewAuthHandler(db, cfg)
|
||||
r.Post("/api/auth/login", auth.Login)
|
||||
@@ -44,6 +47,7 @@ func NewRouter(cfg *config.Config, db *sqlx.DB) http.Handler {
|
||||
r.Post("/api/work-orders/{id}/steps", step.Create)
|
||||
r.Put("/api/work-orders/{id}/steps/{sid}", step.Update)
|
||||
r.Post("/api/work-orders/{id}/steps/{sid}/complete", step.Complete)
|
||||
r.Post("/api/work-orders/{id}/steps/{sid}/uncomplete", step.Uncomplete)
|
||||
r.Delete("/api/work-orders/{id}/steps/{sid}", step.Delete)
|
||||
|
||||
res := handlers.NewResourceHandler(db)
|
||||
@@ -55,17 +59,46 @@ func NewRouter(cfg *config.Config, db *sqlx.DB) http.Handler {
|
||||
r.Get("/api/work-orders/{id}/attachments", att.List)
|
||||
r.Post("/api/work-orders/{id}/attachments", att.Upload)
|
||||
r.Delete("/api/work-orders/{id}/attachments/{aid}", att.Delete)
|
||||
r.Handle("/uploads/*", http.StripPrefix("/uploads/", http.FileServer(http.Dir(cfg.UploadPath))))
|
||||
|
||||
acc := handlers.NewAccountingHandler(db)
|
||||
r.Get("/api/work-orders/{id}/accounting", acc.Get)
|
||||
r.Put("/api/work-orders/{id}/accounting", acc.Upsert)
|
||||
|
||||
act := handlers.NewActivityHandler(db)
|
||||
r.Get("/api/work-orders/{id}/activity", act.List)
|
||||
|
||||
prof := handlers.NewProfileHandler(db)
|
||||
r.Get("/api/profiles", prof.List)
|
||||
r.Post("/api/profiles", prof.Create)
|
||||
r.Get("/api/profiles/{id}", prof.Get)
|
||||
r.Put("/api/profiles/{id}", prof.Update)
|
||||
r.Delete("/api/profiles/{id}", prof.Delete)
|
||||
r.Get("/api/profiles/{id}/steps", prof.ListSteps)
|
||||
r.Post("/api/profiles/{id}/steps", prof.CreateStep)
|
||||
r.Put("/api/profiles/{id}/steps/{sid}", prof.UpdateStep)
|
||||
r.Delete("/api/profiles/{id}/steps/{sid}", prof.DeleteStep)
|
||||
r.Post("/api/work-orders/{id}/apply-profile/{profileId}", prof.Apply)
|
||||
|
||||
reg := handlers.NewRegistryHandler(db)
|
||||
r.Get("/api/registry/people", reg.People)
|
||||
r.Get("/api/registry/vehicles", reg.Vehicles)
|
||||
r.Get("/api/registry/equipment", reg.Equipment)
|
||||
r.Get("/api/registry/materials", reg.Materials)
|
||||
r.Get("/api/registry/people", reg.ListPeople)
|
||||
r.Post("/api/registry/people", reg.CreatePerson)
|
||||
r.Put("/api/registry/people/{id}", reg.UpdatePerson)
|
||||
r.Delete("/api/registry/people/{id}", reg.DeletePerson)
|
||||
|
||||
r.Get("/api/registry/vehicles", reg.ListVehicles)
|
||||
r.Post("/api/registry/vehicles", reg.CreateVehicle)
|
||||
r.Put("/api/registry/vehicles/{id}", reg.UpdateVehicle)
|
||||
r.Delete("/api/registry/vehicles/{id}", reg.DeleteVehicle)
|
||||
|
||||
r.Get("/api/registry/equipment", reg.ListEquipment)
|
||||
r.Post("/api/registry/equipment", reg.CreateEquipment)
|
||||
r.Put("/api/registry/equipment/{id}", reg.UpdateEquipment)
|
||||
r.Delete("/api/registry/equipment/{id}", reg.DeleteEquipment)
|
||||
|
||||
r.Get("/api/registry/materials", reg.ListMaterials)
|
||||
r.Post("/api/registry/materials", reg.CreateMaterial)
|
||||
r.Put("/api/registry/materials/{id}", reg.UpdateMaterial)
|
||||
r.Delete("/api/registry/materials/{id}", reg.DeleteMaterial)
|
||||
})
|
||||
|
||||
return r
|
||||
|
||||
@@ -101,11 +101,12 @@ type AccountingCode struct {
|
||||
// ── Resource Registry ─────────────────────────────────────────────────────────
|
||||
|
||||
type RegistryPerson struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Role string `db:"role" json:"role"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Phone string `db:"phone" json:"phone"`
|
||||
ID int `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Role string `db:"role" json:"role"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Phone string `db:"phone" json:"phone"`
|
||||
Active bool `db:"active" json:"active"`
|
||||
}
|
||||
|
||||
type RegistryVehicle struct {
|
||||
@@ -113,6 +114,7 @@ type RegistryVehicle struct {
|
||||
UnitNumber string `db:"unit_number" json:"unit_number"`
|
||||
Description string `db:"description" json:"description"`
|
||||
VehicleType string `db:"vehicle_type" json:"vehicle_type"`
|
||||
Active bool `db:"active" json:"active"`
|
||||
}
|
||||
|
||||
type RegistryEquipment struct {
|
||||
@@ -120,6 +122,7 @@ type RegistryEquipment struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
AssetTag string `db:"asset_tag" json:"asset_tag"`
|
||||
Category string `db:"category" json:"category"`
|
||||
Active bool `db:"active" json:"active"`
|
||||
}
|
||||
|
||||
type RegistryMaterial struct {
|
||||
@@ -127,6 +130,42 @@ type RegistryMaterial struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Unit string `db:"unit" json:"unit"`
|
||||
PartNumber string `db:"part_number" json:"part_number"`
|
||||
Active bool `db:"active" json:"active"`
|
||||
}
|
||||
|
||||
type AuditEntry struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Action string `db:"action" json:"action"`
|
||||
OldValue string `db:"old_value" json:"old_value"`
|
||||
NewValue string `db:"new_value" json:"new_value"`
|
||||
PerformedBy string `db:"performed_by" json:"performed_by"`
|
||||
PerformedAt time.Time `db:"performed_at" json:"performed_at"`
|
||||
}
|
||||
|
||||
// ── Work Order Profiles ───────────────────────────────────────────────────────
|
||||
|
||||
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"`
|
||||
StepCount int `db:"step_count" json:"step_count"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// ── Users & Auth ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user