275 lines
11 KiB
Go
275 lines
11 KiB
Go
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": {"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 <> 'in_progress' 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
|
|
}
|