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)) 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)) 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 } mockReq, _ := http.NewRequest("GET", fmt.Sprintf("?search=%s", invoiceID), nil) mockReq = mockReq.WithContext(r.Context()) handleInvoiceSearch(w, mockReq, session) }