package workorder import ( "errors" "time" "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5" "github.com/techxcar/backend/internal/auth" "github.com/techxcar/backend/pkg/database" ) func RegisterRoutes(app *fiber.App, db *database.DB, secret string) { ro := []fiber.Handler{ auth.RequireAuth(secret), auth.RequireRole("tenant_admin", "manager", "technician"), auth.TenantMiddleware(db), } write := []fiber.Handler{ auth.RequireAuth(secret), auth.RequireRole("tenant_admin", "manager"), auth.TenantMiddleware(db), } app.Get("/api/v1/work-orders", append(ro, listWOsH())...) app.Post("/api/v1/work-orders", append(write, createWOH())...) app.Get("/api/v1/work-orders/:id", append(ro, getWODetailH())...) app.Put("/api/v1/work-orders/:id", append(write, updateWOH())...) app.Post("/api/v1/work-orders/:id/transition", append(write, transitionWOH())...) app.Post("/api/v1/work-orders/:id/items", append(write, addItemH())...) app.Delete("/api/v1/work-orders/:id/items/:itemId", append(write, removeItemH())...) app.Post("/api/v1/work-orders/:id/staff-hours", append(write, addStaffHoursH())...) app.Delete("/api/v1/work-orders/:id/staff-hours/:shId", append(write, removeStaffHoursH())...) } func listWOsH() fiber.Handler { return func(c *fiber.Ctx) error { conn := auth.GetConn(c) list, err := ListWorkOrders(c.Context(), conn, c.Query("status")) if err != nil { return fiber.NewError(500, "erro ao listar ordens") } if list == nil { list = []*WorkOrder{} } return c.JSON(fiber.Map{"data": list, "error": nil}) } } type woBody struct { ClientID string `json:"client_id"` VehicleID string `json:"vehicle_id"` InternalNotes string `json:"internal_notes"` ClientNotes string `json:"client_notes"` ETADays int `json:"eta_days"` RealDeadline string `json:"real_deadline"` PaymentMethod string `json:"payment_method"` PaymentDate string `json:"payment_date"` } func isValidETADays(v int) bool { switch v { case 1, 2, 3, 7, 15, 30, 31: return true default: return false } } func isValidPaymentMethod(v string) bool { switch v { case "", "numerario", "multibanco", "transferencia_bancaria", "mb_way", "cartao_debito", "cartao_credito", "cheque", "outro": return true default: return false } } func createWOH() fiber.Handler { return func(c *fiber.Ctx) error { var b woBody if err := c.BodyParser(&b); err != nil { return fiber.NewError(400, "corpo inválido") } if b.ETADays == 0 { b.ETADays = 1 } if !isValidETADays(b.ETADays) { return fiber.NewError(400, "eta_days inválido") } if b.RealDeadline != "" { if _, err := time.Parse("2006-01-02", b.RealDeadline); err != nil { return fiber.NewError(400, "real_deadline inválido (YYYY-MM-DD)") } } if !isValidPaymentMethod(b.PaymentMethod) { return fiber.NewError(400, "payment_method inválido") } if b.PaymentDate != "" { if _, err := time.Parse("2006-01-02", b.PaymentDate); err != nil { return fiber.NewError(400, "payment_date inválido (YYYY-MM-DD)") } } claims, _ := c.Locals("claims").(*auth.Claims) createdBy := "" if claims != nil { createdBy = claims.UserID } conn := auth.GetConn(c) wo, err := CreateWorkOrder(c.Context(), conn, b.ClientID, b.VehicleID, b.InternalNotes, b.ClientNotes, b.ETADays, b.RealDeadline, createdBy) if err != nil { return fiber.NewError(500, "erro ao criar ordem") } return c.Status(201).JSON(fiber.Map{"data": wo, "error": nil}) } } func getWODetailH() fiber.Handler { return func(c *fiber.Ctx) error { conn := auth.GetConn(c) detail, err := GetWorkOrderDetail(c.Context(), conn, c.Params("id")) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fiber.NewError(404, "ordem não encontrada") } return fiber.NewError(500, "erro ao obter ordem") } return c.JSON(fiber.Map{"data": detail, "error": nil}) } } func updateWOH() fiber.Handler { return func(c *fiber.Ctx) error { var b woBody if err := c.BodyParser(&b); err != nil { return fiber.NewError(400, "corpo inválido") } if b.ETADays == 0 { b.ETADays = 1 } if !isValidETADays(b.ETADays) { return fiber.NewError(400, "eta_days inválido") } if b.RealDeadline != "" { if _, err := time.Parse("2006-01-02", b.RealDeadline); err != nil { return fiber.NewError(400, "real_deadline inválido (YYYY-MM-DD)") } } if !isValidPaymentMethod(b.PaymentMethod) { return fiber.NewError(400, "payment_method inválido") } if b.PaymentDate != "" { if _, err := time.Parse("2006-01-02", b.PaymentDate); err != nil { return fiber.NewError(400, "payment_date inválido (YYYY-MM-DD)") } } conn := auth.GetConn(c) wo, err := UpdateWorkOrder(c.Context(), conn, c.Params("id"), b.ClientID, b.VehicleID, b.InternalNotes, b.ClientNotes, b.ETADays, b.RealDeadline, b.PaymentMethod, b.PaymentDate) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fiber.NewError(400, "ordem não editável (cancelada/faturada) ou não encontrada") } return fiber.NewError(500, "erro ao actualizar ordem") } return c.JSON(fiber.Map{"data": wo, "error": nil}) } } func transitionWOH() fiber.Handler { return func(c *fiber.Ctx) error { var body struct { Status string `json:"status"` PaymentMethod string `json:"payment_method"` PaymentDate string `json:"payment_date"` } if err := c.BodyParser(&body); err != nil || body.Status == "" { return fiber.NewError(400, "status é obrigatório") } if !isValidPaymentMethod(body.PaymentMethod) { return fiber.NewError(400, "payment_method inválido") } if body.PaymentDate != "" { if _, err := time.Parse("2006-01-02", body.PaymentDate); err != nil { return fiber.NewError(400, "payment_date inválido (YYYY-MM-DD)") } } if body.Status == "invoiced" { if body.PaymentMethod == "" { return fiber.NewError(400, "meio de pagamento é obrigatório para faturar") } if body.PaymentDate == "" { body.PaymentDate = time.Now().Format("2006-01-02") } } claims, _ := c.Locals("claims").(*auth.Claims) changedBy := "" if claims != nil { changedBy = claims.UserID } conn := auth.GetConn(c) wo, err := TransitionStatus(c.Context(), conn, c.Params("id"), body.Status, body.PaymentMethod, body.PaymentDate, changedBy) if err != nil { return fiber.NewError(400, err.Error()) } return c.JSON(fiber.Map{"data": wo, "error": nil}) } } type woItemBody struct { CatalogItemID string `json:"catalog_item_id"` Description string `json:"description"` ChangeJustification string `json:"change_justification"` Qty float64 `json:"qty"` UnitPrice float64 `json:"unit_price"` DiscountPct float64 `json:"discount_pct"` } func addItemH() fiber.Handler { return func(c *fiber.Ctx) error { var b woItemBody if err := c.BodyParser(&b); err != nil { return fiber.NewError(400, "corpo inválido") } if b.Description == "" || b.Qty <= 0 || b.UnitPrice < 0 { return fiber.NewError(400, "descrição, quantidade e preço unitário são obrigatórios") } conn := auth.GetConn(c) var status string if err := conn.QueryRow(c.Context(), `SELECT status FROM work_orders WHERE id=$1`, c.Params("id")).Scan(&status); err != nil { if errors.Is(err, pgx.ErrNoRows) { return fiber.NewError(404, "ordem não encontrada") } return fiber.NewError(500, "erro ao validar ordem") } if status == "open" && b.ChangeJustification == "" { return fiber.NewError(400, "justificação da alteração é obrigatória em orçamento aprovado") } item, err := AddItem(c.Context(), conn, c.Params("id"), b.CatalogItemID, b.Description, b.ChangeJustification, b.Qty, b.UnitPrice, b.DiscountPct) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fiber.NewError(400, "ordem não editável (cancelada/faturada) ou dados inválidos") } return fiber.NewError(500, "erro ao adicionar item") } return c.Status(201).JSON(fiber.Map{"data": item, "error": nil}) } } func removeItemH() fiber.Handler { return func(c *fiber.Ctx) error { conn := auth.GetConn(c) if err := RemoveItem(c.Context(), conn, c.Params("id"), c.Params("itemId")); err != nil { return fiber.NewError(500, "erro ao remover item") } return c.SendStatus(204) } } type staffHoursBody struct { StaffID string `json:"staff_id"` Hours float64 `json:"hours"` CostPerHour float64 `json:"cost_per_hour"` } func addStaffHoursH() fiber.Handler { return func(c *fiber.Ctx) error { var b staffHoursBody if err := c.BodyParser(&b); err != nil { return fiber.NewError(400, "corpo inválido") } if b.StaffID == "" || b.Hours <= 0 { return fiber.NewError(400, "funcionário e horas são obrigatórios") } conn := auth.GetConn(c) sh, err := AddStaffHours(c.Context(), conn, c.Params("id"), b.StaffID, b.Hours, b.CostPerHour) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fiber.NewError(400, "ordem não editável (cancelada/faturada) ou não encontrada") } return fiber.NewError(500, "erro ao adicionar horas") } return c.Status(201).JSON(fiber.Map{"data": sh, "error": nil}) } } func removeStaffHoursH() fiber.Handler { return func(c *fiber.Ctx) error { conn := auth.GetConn(c) if err := RemoveStaffHours(c.Context(), conn, c.Params("id"), c.Params("shId")); err != nil { return fiber.NewError(500, "erro ao remover horas") } return c.SendStatus(204) } }