package workorder import ( "context" "errors" "time" "github.com/jackc/pgx/v5/pgxpool" ) type WorkOrder struct { ID string `json:"id"` Number int `json:"number"` ClientID *string `json:"client_id"` VehicleID *string `json:"vehicle_id"` Status string `json:"status"` InternalNotes string `json:"internal_notes"` ClientNotes string `json:"client_notes"` ETADays int `json:"eta_days"` RealDeadline *time.Time `json:"real_deadline"` PaymentMethod string `json:"payment_method"` PaymentDate *time.Time `json:"payment_date"` CreatedBy *string `json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type WOItem struct { ID string `json:"id"` WorkOrderID string `json:"work_order_id"` 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"` Total float64 `json:"total"` } type WOStaffHours struct { ID string `json:"id"` WorkOrderID string `json:"work_order_id"` StaffID string `json:"staff_id"` Hours float64 `json:"hours"` CostPerHour float64 `json:"cost_per_hour"` Total float64 `json:"total"` } type WorkOrderDetail struct { WorkOrder Items []*WOItem `json:"items"` StaffHours []*WOStaffHours `json:"staff_hours"` } var allowedTransitions = map[string][]string{ "quote": {"open", "cancelled"}, "open": {"in_progress", "cancelled"}, "in_progress": {"completed", "cancelled"}, "completed": {"invoiced", "cancelled"}, "invoiced": {}, "cancelled": {}, } func ValidateTransition(from, to string) error { nexts, ok := allowedTransitions[from] if !ok { return errors.New("estado desconhecido") } for _, n := range nexts { if n == to { return nil } } return errors.New("transição inválida") } func ListWorkOrders(ctx context.Context, conn *pgxpool.Conn, status string) ([]*WorkOrder, error) { q := `SELECT id, number, client_id, vehicle_id, status, COALESCE(internal_notes,''), COALESCE(client_notes,''), COALESCE(eta_days,1), real_deadline, COALESCE(payment_method,''), payment_date, created_by, created_at, updated_at FROM work_orders` args := []any{} if status != "" { q += " WHERE status = $1" args = append(args, status) } q += " ORDER BY created_at DESC" rows, err := conn.Query(ctx, q, args...) if err != nil { return nil, err } defer rows.Close() var list []*WorkOrder for rows.Next() { var wo WorkOrder if err := rows.Scan(&wo.ID, &wo.Number, &wo.ClientID, &wo.VehicleID, &wo.Status, &wo.InternalNotes, &wo.ClientNotes, &wo.ETADays, &wo.RealDeadline, &wo.PaymentMethod, &wo.PaymentDate, &wo.CreatedBy, &wo.CreatedAt, &wo.UpdatedAt); err != nil { return nil, err } list = append(list, &wo) } return list, rows.Err() } func CreateWorkOrder(ctx context.Context, conn *pgxpool.Conn, clientID, vehicleID, internalNotes, clientNotes string, etaDays int, realDeadline, createdBy string) (*WorkOrder, error) { var wo WorkOrder err := conn.QueryRow(ctx, ` INSERT INTO work_orders (client_id, vehicle_id, internal_notes, client_notes, eta_days, real_deadline, created_by) VALUES ( NULLIF($1,'')::uuid, NULLIF($2,'')::uuid, NULLIF($3,''), NULLIF($4,''), $5, NULLIF($6,'')::date, (SELECT id FROM users WHERE id = NULLIF($7,'')::uuid LIMIT 1) ) RETURNING id, number, client_id, vehicle_id, status, COALESCE(internal_notes,''), COALESCE(client_notes,''), COALESCE(eta_days,1), real_deadline, COALESCE(payment_method,''), payment_date, created_by, created_at, updated_at`, clientID, vehicleID, internalNotes, clientNotes, etaDays, realDeadline, createdBy). Scan(&wo.ID, &wo.Number, &wo.ClientID, &wo.VehicleID, &wo.Status, &wo.InternalNotes, &wo.ClientNotes, &wo.ETADays, &wo.RealDeadline, &wo.PaymentMethod, &wo.PaymentDate, &wo.CreatedBy, &wo.CreatedAt, &wo.UpdatedAt) return &wo, err } func UpdateWorkOrder(ctx context.Context, conn *pgxpool.Conn, id, clientID, vehicleID, internalNotes, clientNotes string, etaDays int, realDeadline, paymentMethod, paymentDate string) (*WorkOrder, error) { var wo WorkOrder err := conn.QueryRow(ctx, ` UPDATE work_orders SET client_id=NULLIF($2,'')::uuid, vehicle_id=NULLIF($3,'')::uuid, internal_notes=NULLIF($4,''), client_notes=NULLIF($5,''), eta_days=$6, real_deadline=NULLIF($7,'')::date, payment_method=NULLIF($8,''), payment_date=NULLIF($9,'')::date, updated_at=NOW() WHERE id=$1 AND status NOT IN ('cancelled','invoiced') RETURNING id, number, client_id, vehicle_id, status, COALESCE(internal_notes,''), COALESCE(client_notes,''), COALESCE(eta_days,1), real_deadline, COALESCE(payment_method,''), payment_date, created_by, created_at, updated_at`, id, clientID, vehicleID, internalNotes, clientNotes, etaDays, realDeadline, paymentMethod, paymentDate). Scan(&wo.ID, &wo.Number, &wo.ClientID, &wo.VehicleID, &wo.Status, &wo.InternalNotes, &wo.ClientNotes, &wo.ETADays, &wo.RealDeadline, &wo.PaymentMethod, &wo.PaymentDate, &wo.CreatedBy, &wo.CreatedAt, &wo.UpdatedAt) return &wo, err } func TransitionStatus(ctx context.Context, conn *pgxpool.Conn, id, toStatus, paymentMethod, paymentDate, changedBy string) (*WorkOrder, error) { var fromStatus string if err := conn.QueryRow(ctx, `SELECT status FROM work_orders WHERE id=$1`, id).Scan(&fromStatus); err != nil { return nil, errors.New("ordem não encontrada") } if err := ValidateTransition(fromStatus, toStatus); err != nil { return nil, err } var wo WorkOrder err := conn.QueryRow(ctx, ` UPDATE work_orders SET status=$2, payment_method=CASE WHEN $2='invoiced' THEN NULLIF($3,'') ELSE payment_method END, payment_date=CASE WHEN $2='invoiced' THEN NULLIF($4,'')::date ELSE payment_date END, updated_at=NOW() WHERE id=$1 RETURNING id, number, client_id, vehicle_id, status, COALESCE(internal_notes,''), COALESCE(client_notes,''), COALESCE(eta_days,1), real_deadline, COALESCE(payment_method,''), payment_date, created_by, created_at, updated_at`, id, toStatus, paymentMethod, paymentDate). Scan(&wo.ID, &wo.Number, &wo.ClientID, &wo.VehicleID, &wo.Status, &wo.InternalNotes, &wo.ClientNotes, &wo.ETADays, &wo.RealDeadline, &wo.PaymentMethod, &wo.PaymentDate, &wo.CreatedBy, &wo.CreatedAt, &wo.UpdatedAt) if err != nil { return nil, err } _, _ = conn.Exec(ctx, ` INSERT INTO wo_status_log (work_order_id, from_status, to_status, changed_by) VALUES ($1, $2, $3, (SELECT id FROM users WHERE id = NULLIF($4,'')::uuid LIMIT 1))`, id, fromStatus, toStatus, changedBy) return &wo, nil } func GetWorkOrderDetail(ctx context.Context, conn *pgxpool.Conn, id string) (*WorkOrderDetail, error) { var wo WorkOrder err := conn.QueryRow(ctx, ` SELECT id, number, client_id, vehicle_id, status, COALESCE(internal_notes,''), COALESCE(client_notes,''), COALESCE(eta_days,1), real_deadline, COALESCE(payment_method,''), payment_date, created_by, created_at, updated_at FROM work_orders WHERE id=$1`, id). Scan(&wo.ID, &wo.Number, &wo.ClientID, &wo.VehicleID, &wo.Status, &wo.InternalNotes, &wo.ClientNotes, &wo.ETADays, &wo.RealDeadline, &wo.PaymentMethod, &wo.PaymentDate, &wo.CreatedBy, &wo.CreatedAt, &wo.UpdatedAt) if err != nil { return nil, err } detail := &WorkOrderDetail{WorkOrder: wo, Items: []*WOItem{}, StaffHours: []*WOStaffHours{}} rows, err := conn.Query(ctx, ` SELECT id, work_order_id, catalog_item_id, description, COALESCE(change_justification,''), qty, unit_price, discount_pct, total FROM wo_items WHERE work_order_id=$1`, id) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var i WOItem if err := rows.Scan(&i.ID, &i.WorkOrderID, &i.CatalogItemID, &i.Description, &i.ChangeJustification, &i.Qty, &i.UnitPrice, &i.DiscountPct, &i.Total); err != nil { return nil, err } detail.Items = append(detail.Items, &i) } if err := rows.Err(); err != nil { return nil, err } shRows, err := conn.Query(ctx, ` SELECT id, work_order_id, staff_id, hours, cost_per_hour, total FROM wo_staff_hours WHERE work_order_id=$1`, id) if err != nil { return nil, err } defer shRows.Close() for shRows.Next() { var sh WOStaffHours if err := shRows.Scan(&sh.ID, &sh.WorkOrderID, &sh.StaffID, &sh.Hours, &sh.CostPerHour, &sh.Total); err != nil { return nil, err } detail.StaffHours = append(detail.StaffHours, &sh) } return detail, shRows.Err() } func AddItem(ctx context.Context, conn *pgxpool.Conn, woID, catalogItemID, description, changeJustification string, qty, unitPrice, discountPct float64) (*WOItem, error) { var i WOItem err := conn.QueryRow(ctx, ` INSERT INTO wo_items (work_order_id, catalog_item_id, description, change_justification, qty, unit_price, discount_pct) SELECT id, NULLIF($2,'')::uuid, $3, NULLIF($4,''), $5, $6, $7 FROM work_orders WHERE id=$1 AND status NOT IN ('cancelled','invoiced') AND (status <> 'open' OR NULLIF($4,'') IS NOT NULL) RETURNING id, work_order_id, catalog_item_id, description, COALESCE(change_justification,''), qty, unit_price, discount_pct, total`, woID, catalogItemID, description, changeJustification, qty, unitPrice, discountPct). Scan(&i.ID, &i.WorkOrderID, &i.CatalogItemID, &i.Description, &i.ChangeJustification, &i.Qty, &i.UnitPrice, &i.DiscountPct, &i.Total) return &i, err } func RemoveItem(ctx context.Context, conn *pgxpool.Conn, woID, itemID string) error { _, err := conn.Exec(ctx, ` DELETE FROM wo_items i USING work_orders w WHERE i.id=$1 AND i.work_order_id=$2 AND w.id=i.work_order_id AND w.status NOT IN ('cancelled','invoiced')`, itemID, woID) return err } func AddStaffHours(ctx context.Context, conn *pgxpool.Conn, woID, staffID string, hours, costPerHour float64) (*WOStaffHours, error) { var sh WOStaffHours err := conn.QueryRow(ctx, ` INSERT INTO wo_staff_hours (work_order_id, staff_id, hours, cost_per_hour) SELECT id, $2, $3, $4 FROM work_orders WHERE id=$1 AND status NOT IN ('cancelled','invoiced') RETURNING id, work_order_id, staff_id, hours, cost_per_hour, total`, woID, staffID, hours, costPerHour). Scan(&sh.ID, &sh.WorkOrderID, &sh.StaffID, &sh.Hours, &sh.CostPerHour, &sh.Total) return &sh, err } func RemoveStaffHours(ctx context.Context, conn *pgxpool.Conn, woID, shID string) error { _, err := conn.Exec(ctx, ` DELETE FROM wo_staff_hours s USING work_orders w WHERE s.id=$1 AND s.work_order_id=$2 AND w.id=s.work_order_id AND w.status NOT IN ('cancelled','invoiced')`, shID, woID) return err }