package delivery import ( "errors" "log" "net/http" "time" sqlc "github.com/chedius/delivery-tracker/internal/db/sqlc" "github.com/chedius/delivery-tracker/internal/ws" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) type Handler struct { queries *sqlc.Queries hub *ws.Hub } // DeliveryRequest represents the request body for creating or updating a delivery type DeliveryRequest struct { Date string `json:"date" binding:"required"` // DD-MM-YYYY PickupLocation string `json:"pickup_location" binding:"required,oneof=warehouse symbat nursaya galaktika"` PickupLocation2 *string `json:"pickup_location_2" binding:"omitempty,oneof=warehouse symbat nursaya galaktika"` WarehouseRequestSource *string `json:"warehouse_request_source" binding:"omitempty,oneof=symbat nursaya galaktika"` WarehouseRequestSource2 *string `json:"warehouse_request_source_2" binding:"omitempty,oneof=symbat nursaya galaktika"` ProductName string `json:"product_name" binding:"required"` ProductName2 *string `json:"product_name_2"` CustomerName string `json:"customer_name" binding:"required"` Address string `json:"address" binding:"required"` Street string `json:"street" binding:"required"` House string `json:"house" binding:"required"` Apartment *string `json:"apartment"` Entrance *string `json:"entrance"` Floor *string `json:"floor"` Phone string `json:"phone" binding:"required"` AdditionalPhone *string `json:"additional_phone"` HasElevator bool `json:"has_elevator"` ServiceInfo *string `json:"service_info"` Comment string `json:"comment"` } func NewHandler(queries *sqlc.Queries, hub *ws.Hub) *Handler { return &Handler{queries: queries, hub: hub} } // GET /api/deliveries/:id func (h *Handler) GetDeliveryByID(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"}) return } parsedID, err := uuid.Parse(id) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()}) return } delivery, err := h.queries.GetDeliveryByID(c.Request.Context(), pgtype.UUID{Bytes: parsedID, Valid: true}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get delivery", "details": err.Error(), "id": id, "bytes": [16]byte([]byte(id))}) return } c.JSON(http.StatusOK, gin.H{"delivery": delivery}) } // GET /api/deliveries?date=DD-MM-YYYY func (h *Handler) GetDeliveries(c *gin.Context) { t, err := parseDate(c.Query("date")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format", "details": err.Error()}) return } date := pgtype.Date{Time: t, Valid: true} deliveries, err := h.queries.GetDeliveriesByDate(c.Request.Context(), date) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get deliveries", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deliveries": deliveries}) } // GET /api/deliveries/count func (h *Handler) GetDeliveryCount(c *gin.Context) { counts, err := h.queries.GetDeliveryCount(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get delivery count", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"counts": counts}) } // POST /api/deliveries func (h *Handler) CreateDelivery(c *gin.Context) { var req DeliveryRequest = DeliveryRequest{} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } if err := normalizeWarehouseRequestSources(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Parse date from DD-MM-YYYY t, err := parseDate(req.Date) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format", "details": err.Error()}) return } params := sqlc.CreateDeliveryParams{ Date: pgtype.Date{Time: t, Valid: true}, PickupLocation: req.PickupLocation, PickupLocation2: pgtype.Text{String: derefString(req.PickupLocation2), Valid: req.PickupLocation2 != nil}, WarehouseRequestSource: pgtype.Text{String: derefString(req.WarehouseRequestSource), Valid: req.WarehouseRequestSource != nil}, WarehouseRequestSource2: pgtype.Text{String: derefString(req.WarehouseRequestSource2), Valid: req.WarehouseRequestSource2 != nil}, ProductName: req.ProductName, ProductName2: pgtype.Text{String: derefString(req.ProductName2), Valid: req.ProductName2 != nil}, CustomerName: req.CustomerName, Address: req.Address, Street: req.Street, House: req.House, Apartment: pgtype.Text{String: derefString(req.Apartment), Valid: req.Apartment != nil}, Entrance: pgtype.Text{String: derefString(req.Entrance), Valid: req.Entrance != nil}, Floor: pgtype.Text{String: derefString(req.Floor), Valid: req.Floor != nil}, Phone: req.Phone, AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, HasElevator: req.HasElevator, ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, Comment: pgtype.Text{String: req.Comment, Valid: true}, } res, err := h.queries.CreateDelivery(c.Request.Context(), params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create delivery", "details": err.Error()}) return } h.hub.Broadcast(ws.NewEvent(ws.DeliveryCreated, res)) c.JSON(http.StatusOK, gin.H{"message": "Delivery created", "id": res.ID.String()}) } // PATCH /api/deliveries/:id func (h *Handler) UpdateDelivery(c *gin.Context) { var req DeliveryRequest = DeliveryRequest{} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } if err := normalizeWarehouseRequestSources(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"}) return } parsedID, err := uuid.Parse(id) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()}) return } t, err := parseDate(req.Date) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format", "details": err.Error()}) return } if err := h.queries.UpdateDelivery(c.Request.Context(), sqlc.UpdateDeliveryParams{ ID: pgtype.UUID{Bytes: parsedID, Valid: true}, Date: pgtype.Date{Time: t, Valid: true}, PickupLocation: req.PickupLocation, PickupLocation2: pgtype.Text{String: derefString(req.PickupLocation2), Valid: req.PickupLocation2 != nil}, WarehouseRequestSource: pgtype.Text{String: derefString(req.WarehouseRequestSource), Valid: req.WarehouseRequestSource != nil}, WarehouseRequestSource2: pgtype.Text{String: derefString(req.WarehouseRequestSource2), Valid: req.WarehouseRequestSource2 != nil}, ProductName: req.ProductName, ProductName2: pgtype.Text{String: derefString(req.ProductName2), Valid: req.ProductName2 != nil}, CustomerName: req.CustomerName, Address: req.Address, Street: req.Street, House: req.House, Apartment: pgtype.Text{String: derefString(req.Apartment), Valid: req.Apartment != nil}, Entrance: pgtype.Text{String: derefString(req.Entrance), Valid: req.Entrance != nil}, Floor: pgtype.Text{String: derefString(req.Floor), Valid: req.Floor != nil}, Phone: req.Phone, AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, HasElevator: req.HasElevator, ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, Comment: pgtype.Text{String: req.Comment, Valid: true}, }); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update delivery", "details": err.Error()}) return } if updated, err := h.queries.GetDeliveryByID(c.Request.Context(), pgtype.UUID{Bytes: parsedID, Valid: true}); err == nil { h.hub.Broadcast(ws.NewEvent(ws.DeliveryUpdated, updated)) } else { log.Printf("delivery: failed to fetch updated delivery %s for ws broadcast: %v", id, err) } c.JSON(http.StatusOK, gin.H{"message": "Delivery updated"}) } // PATCH /api/deliveries/:id/status func (h *Handler) UpdateDeliveryStatus(c *gin.Context) { var req struct { Status string `json:"status"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"}) return } parsedID, err := uuid.Parse(id) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()}) return } status := req.Status if status == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Status is required"}) return } if err := h.queries.UpdateDeliveryStatus(c.Request.Context(), sqlc.UpdateDeliveryStatusParams{ ID: pgtype.UUID{Bytes: parsedID, Valid: true}, Status: status, }); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update delivery status", "details": err.Error()}) return } h.hub.Broadcast(ws.NewEvent(ws.DeliveryStatusChanged, ws.StatusPayload{ID: id, Status: status})) c.JSON(http.StatusOK, gin.H{"message": "Delivery status updated"}) } // DELETE /api/deliveries/:id func (h *Handler) DeleteDelivery(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"}) return } parsedID, err := uuid.Parse(id) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()}) return } if err := h.queries.DeleteDelivery(c.Request.Context(), pgtype.UUID{Bytes: parsedID, Valid: true}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete delivery", "details": err.Error()}) return } h.hub.Broadcast(ws.NewEvent(ws.DeliveryDeleted, ws.DeletePayload{ID: id})) c.JSON(http.StatusOK, gin.H{"message": "Delivery deleted"}) } func parseDate(dateStr string) (time.Time, error) { t, err := time.Parse("02-01-2006", dateStr) if err != nil { return time.Time{}, err } return t, nil } // derefString safely dereferences a string pointer func derefString(s *string) string { if s == nil { return "" } return *s } func normalizeWarehouseRequestSources(req *DeliveryRequest) error { if req.PickupLocation == "warehouse" && req.WarehouseRequestSource == nil { return errors.New("warehouse_request_source is required when pickup_location is warehouse") } if req.PickupLocation != "warehouse" { req.WarehouseRequestSource = nil } if req.PickupLocation2 != nil && *req.PickupLocation2 == "warehouse" && req.WarehouseRequestSource2 == nil { return errors.New("warehouse_request_source_2 is required when pickup_location_2 is warehouse") } if req.PickupLocation2 == nil || *req.PickupLocation2 != "warehouse" { req.WarehouseRequestSource2 = nil } return nil }