package handlers import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "strconv" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "github.com/gorilla/mux" ) type LineItemHandler struct { queries *db.Queries } func NewLineItemHandler(queries *db.Queries) *LineItemHandler { return &LineItemHandler{queries: queries} } func (h *LineItemHandler) List(w http.ResponseWriter, r *http.Request) { limit := 50 offset := 0 if l := r.URL.Query().Get("limit"); l != "" { if val, err := strconv.Atoi(l); err == nil { limit = val } } if o := r.URL.Query().Get("offset"); o != "" { if val, err := strconv.Atoi(o); err == nil { offset = val } } lineItems, err := h.queries.ListLineItems(r.Context(), db.ListLineItemsParams{ Limit: int32(limit), Offset: int32(offset), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(lineItems) } func (h *LineItemHandler) Get(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid line item ID", http.StatusBadRequest) return } lineItem, err := h.queries.GetLineItem(r.Context(), int32(id)) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Line item not found", http.StatusNotFound) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } // Add CORS headers for API access w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(lineItem) } func (h *LineItemHandler) AjaxAdd(w http.ResponseWriter, r *http.Request) { // Parse form data if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("NO-DATA")) return } // Parse required fields documentID, err := strconv.Atoi(r.FormValue("document_id")) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("FAILURE")) return } // Get next item number maxItemInterface, err := h.queries.GetMaxItemNumber(r.Context(), int32(documentID)) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("FAILURE")) return } maxItemNumber := float64(0) if maxItemInterface != nil { switch v := maxItemInterface.(type) { case string: if parsed, err := strconv.ParseFloat(v, 64); err == nil { maxItemNumber = parsed } case float64: maxItemNumber = v case int64: maxItemNumber = float64(v) } } nextItemNumber := fmt.Sprintf("%.2f", maxItemNumber+1.0) // Parse optional fields var productID sql.NullInt32 if pid := r.FormValue("product_id"); pid != "" { if id, err := strconv.Atoi(pid); err == nil { productID = sql.NullInt32{Int32: int32(id), Valid: true} } } var costingID sql.NullInt32 if cid := r.FormValue("costing_id"); cid != "" { if id, err := strconv.Atoi(cid); err == nil { costingID = sql.NullInt32{Int32: int32(id), Valid: true} } } // Parse boolean fields option := r.FormValue("option") == "1" || r.FormValue("option") == "true" hasTextPrices := r.FormValue("has_text_prices") == "1" || r.FormValue("has_text_prices") == "true" hasPrice := int8(1) if hp := r.FormValue("has_price"); hp == "0" || hp == "false" { hasPrice = 0 } params := db.CreateLineItemParams{ ItemNumber: nextItemNumber, Option: option, Quantity: r.FormValue("quantity"), Title: r.FormValue("title"), Description: r.FormValue("description"), DocumentID: int32(documentID), ProductID: productID, HasTextPrices: hasTextPrices, HasPrice: hasPrice, UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""}, GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""}, CostingID: costingID, GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""}, NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""}, DiscountPercent: sql.NullString{String: r.FormValue("discount_percent"), Valid: r.FormValue("discount_percent") != ""}, DiscountAmountUnit: sql.NullString{String: r.FormValue("discount_amount_unit"), Valid: r.FormValue("discount_amount_unit") != ""}, DiscountAmountTotal: sql.NullString{String: r.FormValue("discount_amount_total"), Valid: r.FormValue("discount_amount_total") != ""}, GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""}, NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""}, } _, err = h.queries.CreateLineItem(r.Context(), params) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("FAILURE")) return } // TODO: Update invoice totals like the original CakePHP code // h.updateInvoice(documentID) w.WriteHeader(http.StatusOK) w.Write([]byte("SUCCESS")) } func (h *LineItemHandler) AjaxEdit(w http.ResponseWriter, r *http.Request) { // Parse form data if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("NO-DATA")) return } // Parse ID id, err := strconv.Atoi(r.FormValue("id")) if err != nil { log.Printf("Error updating line item ID: %d %s\n", id, err) w.WriteHeader(http.StatusBadRequest) w.Write([]byte("FAILURE")) return } documentID, err := strconv.Atoi(r.FormValue("document_id")) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("FAILURE")) return } // Parse optional fields var productID sql.NullInt32 if pid := r.FormValue("product_id"); pid != "" { if prodID, err := strconv.Atoi(pid); err == nil { productID = sql.NullInt32{Int32: int32(prodID), Valid: true} } } var costingID sql.NullInt32 if cid := r.FormValue("costing_id"); cid != "" { if cID, err := strconv.Atoi(cid); err == nil { costingID = sql.NullInt32{Int32: int32(cID), Valid: true} } } // Parse boolean fields option := r.FormValue("option") == "1" || r.FormValue("option") == "true" hasTextPrices := r.FormValue("has_text_prices") == "1" || r.FormValue("has_text_prices") == "true" hasPrice := int8(1) if hp := r.FormValue("has_price"); hp == "0" || hp == "false" { hasPrice = 0 } params := db.UpdateLineItemParams{ ID: int32(id), ItemNumber: r.FormValue("item_number"), Option: option, Quantity: r.FormValue("quantity"), Title: r.FormValue("title"), Description: r.FormValue("description"), DocumentID: int32(documentID), ProductID: productID, HasTextPrices: hasTextPrices, HasPrice: hasPrice, UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""}, GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""}, CostingID: costingID, GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""}, NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""}, DiscountPercent: sql.NullString{String: r.FormValue("discount_percent"), Valid: r.FormValue("discount_percent") != ""}, DiscountAmountUnit: sql.NullString{String: r.FormValue("discount_amount_unit"), Valid: r.FormValue("discount_amount_unit") != ""}, DiscountAmountTotal: sql.NullString{String: r.FormValue("discount_amount_total"), Valid: r.FormValue("discount_amount_total") != ""}, GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""}, NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""}, } err = h.queries.UpdateLineItem(r.Context(), params) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("FAILURE")) return } // TODO: Update invoice totals like the original CakePHP code // h.updateInvoice(documentID) w.WriteHeader(http.StatusOK) w.Write([]byte("SUCCESS")) } func (h *LineItemHandler) AjaxDelete(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("FAILURE")) return } // Get the line item to find document_id for invoice update _, err = h.queries.GetLineItem(r.Context(), int32(id)) if err != nil { w.WriteHeader(http.StatusNotFound) w.Write([]byte("FAILURE")) return } err = h.queries.DeleteLineItem(r.Context(), int32(id)) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("FAILURE")) return } // TODO: Update invoice totals like the original CakePHP code // h.updateInvoice(lineItem.DocumentID) w.WriteHeader(http.StatusOK) w.Write([]byte("SUCCESS")) } func (h *LineItemHandler) GetTable(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) documentID, err := strconv.Atoi(vars["documentID"]) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } lineItems, err := h.queries.GetLineItemsTable(r.Context(), int32(documentID)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // If AJAX request, return HTML table rows only (for refreshing) if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { w.Header().Set("Content-Type", "text/html") // Generate HTML table rows html := "" if len(lineItems) == 0 { html = `No line items yet` } else { for _, item := range lineItems { unitPrice := "-" if item.GrossUnitPrice.Valid { unitPrice = "$" + item.GrossUnitPrice.String } else if item.UnitPriceString.Valid { unitPrice = item.UnitPriceString.String } totalPrice := "-" if item.GrossPrice.Valid { totalPrice = "$" + item.GrossPrice.String } else if item.GrossPriceString.Valid { totalPrice = item.GrossPriceString.String } html += fmt.Sprintf(` %s %s %s %s %s %s `, item.ID, item.ItemNumber, item.Title, item.Description, item.Quantity, unitPrice, totalPrice, item.ID, item.ID) } } w.Write([]byte(html)) return } // JSON response for API w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(lineItems) } func (h *LineItemHandler) Create(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) documentID, err := strconv.Atoi(vars["documentID"]) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } // Parse form data for HTMX requests if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Get next item number maxItemInterface, err := h.queries.GetMaxItemNumber(r.Context(), int32(documentID)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } maxItemNumber := float64(0) if maxItemInterface != nil { switch v := maxItemInterface.(type) { case string: if parsed, err := strconv.ParseFloat(v, 64); err == nil { maxItemNumber = parsed } case float64: maxItemNumber = v case int64: maxItemNumber = float64(v) } } nextItemNumber := fmt.Sprintf("%.2f", maxItemNumber+1.0) // Parse optional fields var productID sql.NullInt32 if pid := r.FormValue("product_id"); pid != "" { if id, err := strconv.Atoi(pid); err == nil { productID = sql.NullInt32{Int32: int32(id), Valid: true} } } params := db.CreateLineItemParams{ ItemNumber: nextItemNumber, Option: r.FormValue("option") == "1", Quantity: r.FormValue("quantity"), Title: r.FormValue("title"), Description: r.FormValue("description"), DocumentID: int32(documentID), ProductID: productID, HasTextPrices: r.FormValue("has_text_prices") == "1", HasPrice: 1, // Default to has price UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""}, GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""}, GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""}, NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""}, GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""}, NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""}, } // Check if this is a JSON request if r.Header.Get("Content-Type") == "application/json" { if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } } result, err := h.queries.CreateLineItem(r.Context(), params) if err != nil { if r.Header.Get("HX-Request") == "true" { w.Header().Set("Content-Type", "text/html") w.Write([]byte(`
Error creating line item
`)) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } id, err := result.LastInsertId() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // If HTMX request, return success message if r.Header.Get("HX-Request") == "true" { w.Header().Set("Content-Type", "text/html") w.Write([]byte(`
Line item created successfully
`)) return } // JSON response for API w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "id": id, }) } func (h *LineItemHandler) Update(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid line item ID", http.StatusBadRequest) return } var params db.UpdateLineItemParams if r.Header.Get("Content-Type") == "application/json" { if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } } else { // Handle form data if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var productID sql.NullInt32 if pid := r.FormValue("product_id"); pid != "" { if prodID, err := strconv.Atoi(pid); err == nil { productID = sql.NullInt32{Int32: int32(prodID), Valid: true} } } documentID, _ := strconv.Atoi(r.FormValue("document_id")) params = db.UpdateLineItemParams{ ItemNumber: r.FormValue("item_number"), Option: r.FormValue("option") == "1", Quantity: r.FormValue("quantity"), Title: r.FormValue("title"), Description: r.FormValue("description"), DocumentID: int32(documentID), ProductID: productID, HasTextPrices: r.FormValue("has_text_prices") == "1", HasPrice: 1, UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""}, GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""}, GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""}, NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""}, GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""}, NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""}, } } params.ID = int32(id) if err := h.queries.UpdateLineItem(r.Context(), params); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (h *LineItemHandler) Delete(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid line item ID", http.StatusBadRequest) return } if err := h.queries.DeleteLineItem(r.Context(), int32(id)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }