package web import ( "bytes" "encoding/json" "fmt" "html/template" "io" "log" root "marmic/servicetrade-toolbox" "marmic/servicetrade-toolbox/internal/api" "marmic/servicetrade-toolbox/internal/middleware" "net/http" "strings" ) type StatusButton struct { Status string Label string Class string ConfirmText string } // Cannot move from draft status to pending_accounting status // Cannot move from draft status to processed status // Cannot move from draft status to failed status // Cannot move from failed status to pending_accounting status // Cannot move from failed status to processed status var statusButtons = []StatusButton{ {"draft", "Draft Invoice", "success-button", "Are you sure you want to draft this invoice?"}, {"ok", "Ok Invoice", "success-button", "Are you sure you want to mark this invoice as OK?"}, {"failed", "Fail Invoice", "caution-button", "Are you sure you want to fail this invoice?"}, {"pending_accounting", "Pending Invoice", "caution-button", "Are you sure you want to mark this invoice as pending?"}, {"processed", "Process Invoice", "warning-button", "Are you sure you want to process this invoice?"}, {"void", "Void Invoice", "warning-button", "Are you sure you want to void this invoice?"}, } func InvoicesHandler(w http.ResponseWriter, r *http.Request) { session, ok := r.Context().Value(middleware.SessionKey).(*api.Session) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method == "GET" { handleInvoiceSearch(w, r, session) return } tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Invoices", } var err error if r.Header.Get("HX-Request") == "true" { err = tmpl.ExecuteTemplate(w, "content", data) } else { err = tmpl.ExecuteTemplate(w, "layout.html", data) } if err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } } func handleInvoiceSearch(w http.ResponseWriter, r *http.Request, session *api.Session) { tmpl := root.WebTemplates searchTerm := strings.TrimSpace(r.URL.Query().Get("search")) if searchTerm == "" { log.Println("Empty search term, returning empty response") w.WriteHeader(http.StatusOK) return } // Parse the search term for multiple invoice IDs invoiceIDs := parseInvoiceIDs(searchTerm) log.Printf("Processing %d invoice IDs from search term: %s", len(invoiceIDs), searchTerm) if len(invoiceIDs) == 0 { log.Println("No valid invoice IDs found") w.WriteHeader(http.StatusOK) tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{ "NotFound": true, "ErrorMsg": "No valid invoice IDs found", "SearchTerm": searchTerm, }) return } // For a single invoice ID, use the original logic if len(invoiceIDs) == 1 { handleSingleInvoice(w, invoiceIDs[0], session, tmpl) return } // For multiple invoice IDs, fetch them in parallel handleMultipleInvoices(w, invoiceIDs, session, tmpl, searchTerm) } // parseInvoiceIDs extracts invoice IDs from a comma or space separated string func parseInvoiceIDs(input string) []string { // Replace commas with spaces for uniform splitting spaceSeparated := strings.Replace(input, ",", " ", -1) // Split by spaces and filter empty strings parts := strings.Fields(spaceSeparated) var invoiceIDs []string for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { invoiceIDs = append(invoiceIDs, trimmed) } } return invoiceIDs } // handleSingleInvoice processes a search for a single invoice ID func handleSingleInvoice(w http.ResponseWriter, invoiceID string, session *api.Session, tmpl *template.Template) { log.Printf("Searching for single invoice with ID: %s", invoiceID) invoice, err := session.GetInvoice(invoiceID) if err != nil { log.Printf("Error fetching invoice: %v", err) w.WriteHeader(http.StatusOK) errorMsg := fmt.Sprintf("No invoice found for: %s", invoiceID) if strings.Contains(err.Error(), "access forbidden") { errorMsg = "You do not have permission to view this invoice." } tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{ "Error": true, "ErrorMsg": errorMsg, "SearchTerm": invoiceID, }) return } if invoice == nil { log.Printf("No invoice found for: %s", invoiceID) w.WriteHeader(http.StatusOK) tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{ "NotFound": true, "ErrorMsg": fmt.Sprintf("No invoice found for: %s", invoiceID), "SearchTerm": invoiceID, }) return } if id, ok := invoice["id"].(float64); ok { invoice["id"] = fmt.Sprintf("%.0f", id) } // Add the buttons to display invoice["buttons"] = getInvoiceStatusButtons(invoice["id"].(string), invoice["status"].(string)) // Add the search term to the template data invoice["SearchTerm"] = invoiceID err = tmpl.ExecuteTemplate(w, "invoice_search_results", invoice) if err != nil { log.Printf("Error executing template: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } // handleMultipleInvoices processes a search for multiple invoice IDs func handleMultipleInvoices(w http.ResponseWriter, invoiceIDs []string, session *api.Session, tmpl *template.Template, originalSearchTerm string) { log.Printf("Searching for %d invoices", len(invoiceIDs)) var invoices []map[string]interface{} var failedIDs []string // Fetch each invoice for _, id := range invoiceIDs { invoice, err := session.GetInvoice(id) if err != nil || invoice == nil { log.Printf("Could not fetch invoice %s: %v", id, err) failedIDs = append(failedIDs, id) continue } // Format the ID if numID, ok := invoice["id"].(float64); ok { invoice["id"] = fmt.Sprintf("%.0f", numID) } // Add status buttons invoice["buttons"] = getInvoiceStatusButtons(invoice["id"].(string), invoice["status"].(string)) // Mark this as part of a multiple invoice view invoice["MultipleInvoices"] = true invoice["SearchTerm"] = originalSearchTerm invoices = append(invoices, invoice) } // Prepare the response data data := map[string]interface{}{ "MultipleInvoices": true, "Invoices": invoices, "TotalFound": len(invoices), "TotalSearched": len(invoiceIDs), "FailedCount": len(failedIDs), "FailedIDs": failedIDs, "SearchTerm": originalSearchTerm, } // Render the results err := tmpl.ExecuteTemplate(w, "invoice_search_results", data) if err != nil { log.Printf("Error executing template: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func getInvoiceStatusButtons(invoiceID, currentStatus string) []map[string]string { var buttons []map[string]string // Define allowed transitions for each status allowedTransitions := map[string]map[string]bool{ "draft": { "ok": true, "void": true, // draft cannot transition to pending_accounting, processed, or failed }, "ok": { "draft": true, "failed": true, "pending_accounting": true, "processed": true, "void": true, }, "failed": { "draft": true, "ok": true, "void": true, // failed cannot transition to pending_accounting or processed }, "pending_accounting": { "failed": true, "processed": true, "void": true, }, "processed": { "void": true, // processed can only be voided }, "void": { // void has no allowed transitions }, } // If we have defined transitions for this status, use them if transitions, exists := allowedTransitions[currentStatus]; exists { for _, button := range statusButtons { if transitions[button.Status] { buttons = append(buttons, map[string]string{ "Action": fmt.Sprintf("/%s-invoice/%s", button.Status, invoiceID), "Label": button.Label, "Class": button.Class, "ConfirmText": button.ConfirmText, "Status": button.Status, }) } } } return buttons } func UpdateInvoiceStatusHandler(w http.ResponseWriter, r *http.Request) { session, ok := r.Context().Value(middleware.SessionKey).(*api.Session) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPut { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Extract the invoice ID and status from the URL parts := strings.Split(r.URL.Path, "/") if len(parts) < 3 { http.Error(w, "Invalid request", http.StatusBadRequest) return } invoiceID := parts[len(parts)-1] statusPart := parts[len(parts)-2] // Extract the status from the statusPart status := strings.TrimSuffix(statusPart, "-invoice") if invoiceID == "" { http.Error(w, "Invalid invoice ID", http.StatusBadRequest) return } // Validate the status validStatuses := map[string]bool{ "draft": true, "failed": true, "ok": true, "pending_accounting": true, "processed": true, "void": true, } if !validStatuses[status] { http.Error(w, "Invalid status", http.StatusBadRequest) return } // Prepare the request body requestBody := map[string]string{"status": status} jsonBody, err := json.Marshal(requestBody) if err != nil { http.Error(w, "Error preparing request", http.StatusInternalServerError) return } // Send the PUT request to update the invoice status endpoint := fmt.Sprintf("/invoice/%s", invoiceID) resp, err := session.DoRequest("PUT", endpoint, bytes.NewBuffer(jsonBody)) if err != nil { log.Printf("Error updating invoice status: %v", err) http.Error(w, fmt.Sprintf("Error updating invoice status: %v", err), http.StatusInternalServerError) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) log.Printf("Failed to update invoice status: %s", body) http.Error(w, fmt.Sprintf("Failed to update invoice status: %s", body), resp.StatusCode) return } // Check if it's a request for a single invoice update within a multi-invoice view isPartialUpdate := r.URL.Query().Get("partial") == "true" if isPartialUpdate { renderSingleInvoiceUpdate(w, invoiceID, session, r) return } // Get the original search term from request headers or query parameters originalSearch := r.Header.Get("X-Original-Search") if originalSearch == "" { // If not in the header, check query parameters originalSearch = r.URL.Query().Get("original_search") } // If we have the original search term with multiple IDs, use it to preserve the multi-invoice view // Otherwise, fall back to just showing the updated invoice searchQuery := invoiceID if originalSearch != "" { invoiceIDs := parseInvoiceIDs(originalSearch) if len(invoiceIDs) > 1 { searchQuery = originalSearch } } // Re-run the search with either the original search term or just the updated invoice ID mockReq, _ := http.NewRequest("GET", fmt.Sprintf("?search=%s", searchQuery), nil) mockReq = mockReq.WithContext(r.Context()) handleInvoiceSearch(w, mockReq, session) } // renderSingleInvoiceUpdate fetches a single invoice and renders just its card with hx-swap-oob // for efficient in-place updates within a multi-invoice view func renderSingleInvoiceUpdate(w http.ResponseWriter, invoiceID string, session *api.Session, r *http.Request) { log.Printf("Rendering single invoice update for: %s", invoiceID) // Get the original search term for maintaining context in buttons originalSearch := r.Header.Get("X-Original-Search") if originalSearch == "" { originalSearch = r.URL.Query().Get("original_search") } log.Printf("Original search term: %s", originalSearch) // Fetch just the updated invoice invoice, err := session.GetInvoice(invoiceID) if err != nil { log.Printf("Error fetching updated invoice: %v", err) http.Error(w, fmt.Sprintf("Error fetching invoice: %v", err), http.StatusInternalServerError) return } if invoice == nil { log.Printf("No invoice found for update: %s", invoiceID) http.Error(w, "Invoice not found", http.StatusNotFound) return } // Log invoice data for debugging invoiceNumberRaw, hasInvoiceNumber := invoice["invoiceNumber"] if !hasInvoiceNumber { log.Printf("WARNING: Invoice does not have invoiceNumber field") } else { log.Printf("Invoice number: %v", invoiceNumberRaw) } // Format the invoice ID if needed if id, ok := invoice["id"].(float64); ok { invoice["id"] = fmt.Sprintf("%.0f", id) log.Printf("Formatted invoice ID: %s", invoice["id"]) } // Add status buttons invoice["buttons"] = getInvoiceStatusButtons(invoice["id"].(string), invoice["status"].(string)) // Add search term and set flags for rendering invoice["SearchTerm"] = originalSearch invoice["MultipleInvoices"] = true // Ensure it renders in the multiple invoice style // Debug output the structure of the template data keyCount := 0 for key := range invoice { keyCount++ log.Printf("Template data key: %s", key) } log.Printf("Total template data keys: %d", keyCount) // Render just the specific invoice card for direct replacement tmpl := root.WebTemplates w.Header().Set("Content-Type", "text/html") err = tmpl.ExecuteTemplate(w, "invoice_card", invoice) if err != nil { log.Printf("Error executing template for partial update: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } log.Printf("Successfully rendered updated invoice card for %s", invoiceID) }