package handlers import ( "log" "net/http" "strconv" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" "github.com/gorilla/mux" ) type PageHandler struct { queries *db.Queries tmpl *templates.TemplateManager } func NewPageHandler(queries *db.Queries, tmpl *templates.TemplateManager) *PageHandler { return &PageHandler{ queries: queries, tmpl: tmpl, } } // Home page func (h *PageHandler) Home(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Title": "Dashboard", } if err := h.tmpl.Render(w, "index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Customer pages func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) { page := 1 if p := r.URL.Query().Get("page"); p != "" { if val, err := strconv.Atoi(p); err == nil && val > 0 { page = val } } limit := 20 offset := (page - 1) * limit customers, err := h.queries.ListCustomers(r.Context(), db.ListCustomersParams{ Limit: int32(limit + 1), // Get one extra to check if there are more Offset: int32(offset), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } hasMore := len(customers) > limit if hasMore { customers = customers[:limit] } data := map[string]interface{}{ "Customers": customers, "Page": page, "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, } // Check if this is an HTMX request if r.Header.Get("HX-Request") == "true" { if err := h.tmpl.Render(w, "customers/table.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if err := h.tmpl.Render(w, "customers/index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) CustomersNew(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Customer": db.Customer{}, } if err := h.tmpl.Render(w, "customers/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) CustomersEdit(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid customer ID", http.StatusBadRequest) return } customer, err := h.queries.GetCustomer(r.Context(), int32(id)) if err != nil { http.Error(w, "Customer not found", http.StatusNotFound) return } data := map[string]interface{}{ "Customer": customer, } if err := h.tmpl.Render(w, "customers/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) CustomersShow(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid customer ID", http.StatusBadRequest) return } customer, err := h.queries.GetCustomer(r.Context(), int32(id)) if err != nil { http.Error(w, "Customer not found", http.StatusNotFound) return } data := map[string]interface{}{ "Customer": customer, } if err := h.tmpl.Render(w, "customers/show.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) CustomersSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("search") page := 1 if p := r.URL.Query().Get("page"); p != "" { if val, err := strconv.Atoi(p); err == nil && val > 0 { page = val } } limit := 20 offset := (page - 1) * limit var customers []db.Customer var err error if query == "" { customers, err = h.queries.ListCustomers(r.Context(), db.ListCustomersParams{ Limit: int32(limit + 1), Offset: int32(offset), }) } else { customers, err = h.queries.SearchCustomersByName(r.Context(), db.SearchCustomersByNameParams{ CONCAT: query, CONCAT_2: query, Limit: int32(limit + 1), Offset: int32(offset), }) } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } hasMore := len(customers) > limit if hasMore { customers = customers[:limit] } data := map[string]interface{}{ "Customers": customers, "Page": page, "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, } w.Header().Set("Content-Type", "text/html") if err := h.tmpl.Render(w, "customers/table.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Product page handlers func (h *PageHandler) ProductsIndex(w http.ResponseWriter, r *http.Request) { // Similar implementation to CustomersIndex but for products data := map[string]interface{}{ "Products": []db.Product{}, // Placeholder } if err := h.tmpl.Render(w, "products/index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) ProductsNew(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Product": db.Product{}, } if err := h.tmpl.Render(w, "products/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) ProductsShow(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid product ID", http.StatusBadRequest) return } product, err := h.queries.GetProduct(r.Context(), int32(id)) if err != nil { http.Error(w, "Product not found", http.StatusNotFound) return } data := map[string]interface{}{ "Product": product, } if err := h.tmpl.Render(w, "products/show.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) ProductsEdit(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid product ID", http.StatusBadRequest) return } product, err := h.queries.GetProduct(r.Context(), int32(id)) if err != nil { http.Error(w, "Product not found", http.StatusNotFound) return } data := map[string]interface{}{ "Product": product, } if err := h.tmpl.Render(w, "products/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) ProductsSearch(w http.ResponseWriter, r *http.Request) { // Similar to CustomersSearch but for products data := map[string]interface{}{ "Products": []db.Product{}, } w.Header().Set("Content-Type", "text/html") if err := h.tmpl.Render(w, "products/table.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Purchase Order page handlers func (h *PageHandler) PurchaseOrdersIndex(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "PurchaseOrders": []db.PurchaseOrder{}, } if err := h.tmpl.Render(w, "purchase-orders/index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) PurchaseOrdersNew(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "PurchaseOrder": db.PurchaseOrder{}, } if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) PurchaseOrdersShow(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) return } purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id)) if err != nil { http.Error(w, "Purchase order not found", http.StatusNotFound) return } data := map[string]interface{}{ "PurchaseOrder": purchaseOrder, } if err := h.tmpl.Render(w, "purchase-orders/show.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) PurchaseOrdersEdit(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) return } purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id)) if err != nil { http.Error(w, "Purchase order not found", http.StatusNotFound) return } data := map[string]interface{}{ "PurchaseOrder": purchaseOrder, } if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) PurchaseOrdersSearch(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "PurchaseOrders": []db.PurchaseOrder{}, } w.Header().Set("Content-Type", "text/html") if err := h.tmpl.Render(w, "purchase-orders/table.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Enquiry page handlers func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) { page := 1 if p := r.URL.Query().Get("page"); p != "" { if val, err := strconv.Atoi(p); err == nil && val > 0 { page = val } } limit := 150 offset := (page - 1) * limit var enquiries interface{} var err error var hasMore bool // Check if we want archived enquiries if r.URL.Query().Get("archived") == "true" { archivedEnquiries, err := h.queries.ListArchivedEnquiries(r.Context(), db.ListArchivedEnquiriesParams{ Limit: int32(limit + 1), Offset: int32(offset), }) if err == nil { hasMore = len(archivedEnquiries) > limit if hasMore { archivedEnquiries = archivedEnquiries[:limit] } enquiries = archivedEnquiries } } else { activeEnquiries, err := h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{ Limit: int32(limit + 1), Offset: int32(offset), }) if err == nil { hasMore = len(activeEnquiries) > limit if hasMore { activeEnquiries = activeEnquiries[:limit] } enquiries = activeEnquiries } } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Get status list for dropdown and CSS classes statuses, err := h.queries.GetAllStatuses(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := map[string]interface{}{ "Enquiries": enquiries, "Statuses": statuses, "Page": page, "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, } // Check if this is an HTMX request if r.Header.Get("HX-Request") == "true" { if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if err := h.tmpl.Render(w, "enquiries/index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) EnquiriesNew(w http.ResponseWriter, r *http.Request) { // Get required form data statuses, err := h.queries.GetAllStatuses(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } principles, err := h.queries.GetAllPrinciples(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } states, err := h.queries.GetAllStates(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } countries, err := h.queries.GetAllCountries(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := map[string]interface{}{ "Enquiry": db.Enquiry{}, "Statuses": statuses, "Principles": principles, "States": states, "Countries": countries, } if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) EnquiriesShow(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) return } enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) if err != nil { http.Error(w, "Enquiry not found", http.StatusNotFound) return } data := map[string]interface{}{ "Enquiry": enquiry, } if err := h.tmpl.Render(w, "enquiries/show.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) EnquiriesEdit(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) return } enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) if err != nil { http.Error(w, "Enquiry not found", http.StatusNotFound) return } // Get required form data statuses, err := h.queries.GetAllStatuses(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } principles, err := h.queries.GetAllPrinciples(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } states, err := h.queries.GetAllStates(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } countries, err := h.queries.GetAllCountries(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := map[string]interface{}{ "Enquiry": enquiry, "Statuses": statuses, "Principles": principles, "States": states, "Countries": countries, } if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("search") page := 1 if p := r.URL.Query().Get("page"); p != "" { if val, err := strconv.Atoi(p); err == nil && val > 0 { page = val } } limit := 150 offset := (page - 1) * limit var enquiries interface{} var hasMore bool if query == "" { // If no search query, return regular list regularEnquiries, err := h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{ Limit: int32(limit + 1), Offset: int32(offset), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } hasMore = len(regularEnquiries) > limit if hasMore { regularEnquiries = regularEnquiries[:limit] } enquiries = regularEnquiries } else { searchResults, err := h.queries.SearchEnquiries(r.Context(), db.SearchEnquiriesParams{ CONCAT: query, CONCAT_2: query, CONCAT_3: query, Limit: int32(limit + 1), Offset: int32(offset), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } hasMore = len(searchResults) > limit if hasMore { searchResults = searchResults[:limit] } enquiries = searchResults } data := map[string]interface{}{ "Enquiries": enquiries, "Page": page, "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, } w.Header().Set("Content-Type", "text/html") if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Document page handlers func (h *PageHandler) DocumentsIndex(w http.ResponseWriter, r *http.Request) { page := 1 if p := r.URL.Query().Get("page"); p != "" { if val, err := strconv.Atoi(p); err == nil && val > 0 { page = val } } // Get document type filter docType := r.URL.Query().Get("type") limit := 20 offset := (page - 1) * limit var documents interface{} var err error if docType != "" { documents, err = h.queries.ListDocumentsByType(r.Context(), db.ListDocumentsByTypeParams{ Type: db.DocumentsType(docType), Limit: int32(limit + 1), Offset: int32(offset), }) } else { documents, err = h.queries.ListDocuments(r.Context(), db.ListDocumentsParams{ Limit: int32(limit + 1), Offset: int32(offset), }) } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Get users list for display names (if needed) users, err := h.queries.GetAllUsers(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := map[string]interface{}{ "Documents": documents, "Users": users, "Page": page, "DocType": docType, } // Check if this is an HTMX request if r.Header.Get("HX-Request") == "true" { if err := h.tmpl.RenderPartial(w, "documents/table.html", "document-table", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if err := h.tmpl.Render(w, "documents/index.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) DocumentsShow(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } document, err := h.queries.GetDocumentWithUser(r.Context(), int32(id)) if err != nil { log.Printf("Error fetching document %d: %v", id, err) http.Error(w, "Document not found", http.StatusNotFound) return } data := map[string]interface{}{ "Document": document, } if err := h.tmpl.Render(w, "documents/show.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) DocumentsSearch(w http.ResponseWriter, r *http.Request) { // query := r.URL.Query().Get("search") // TODO: Use when search is implemented var documents interface{} var err error // For now, just return all documents until search is implemented limit := 20 offset := 0 documents, err = h.queries.ListDocuments(r.Context(), db.ListDocumentsParams{ Limit: int32(limit), Offset: int32(offset), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := map[string]interface{}{ "Documents": documents, } w.Header().Set("Content-Type", "text/html") if err := h.tmpl.RenderPartial(w, "documents/table.html", "document-table", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *PageHandler) DocumentsView(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } document, err := h.queries.GetDocumentWithUser(r.Context(), int32(id)) if err != nil { http.Error(w, "Document not found", http.StatusNotFound) return } // Load line items for this document lineItems, err := h.queries.ListLineItemsByDocument(r.Context(), int32(id)) if err != nil { log.Printf("Error loading line items for document %d: %v", id, err) // Don't fail the entire page if line items can't be loaded lineItems = []db.LineItem{} } // Prepare data based on document type data := map[string]interface{}{ "Document": document, "DocType": string(document.Type), "LineItems": lineItems, } // Add document type specific data switch document.Type { case db.DocumentsTypeQuote: // For quotes, we might need to load enquiry data if document.CmcReference != "" { // The CmcReference for quotes is the enquiry title data["EnquiryTitle"] = document.CmcReference } case db.DocumentsTypeInvoice: // For invoices, load job and customer data if needed data["ShowPaymentButton"] = true case db.DocumentsTypePurchaseOrder: // For purchase orders, load principle data if needed case db.DocumentsTypeOrderAck: // For order acknowledgements, load job data if needed case db.DocumentsTypePackingList: // For packing lists, load job data if needed } // Render the appropriate template if err := h.tmpl.Render(w, "documents/view.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }