diff --git a/internal/api/invoices.go b/internal/api/invoices.go index 063df4a..76eb2c5 100644 --- a/internal/api/invoices.go +++ b/internal/api/invoices.go @@ -9,7 +9,6 @@ import ( func (s *Session) GetInvoice(identifier string) (map[string]interface{}, error) { var endpoint string - isInvoiceNumber, _ := regexp.MatchString(`^[A-Za-z]`, identifier) if isInvoiceNumber { @@ -20,13 +19,22 @@ func (s *Session) GetInvoice(identifier string) (map[string]interface{}, error) resp, err := s.DoRequest("GET", endpoint, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("error making request: %v", err) } defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode == 403 { + // Handle 403 Forbidden specifically + return nil, fmt.Errorf("access forbidden: you may not have permission to view this invoice") + } + if resp.StatusCode != 200 { - return nil, fmt.Errorf("failed to get invoice info: %s, response: %s", resp.Status, string(body)) + return nil, fmt.Errorf("failed to get invoice info: %s", resp.Status) } var result map[string]interface{} @@ -34,15 +42,30 @@ func (s *Session) GetInvoice(identifier string) (map[string]interface{}, error) return nil, fmt.Errorf("error unmarshalling response: %v", err) } - if data, ok := result["data"].(map[string]interface{}); ok { - if invoices, ok := data["invoices"].([]interface{}); ok && len(invoices) > 0 { - if invoice, ok := invoices[0].(map[string]interface{}); ok { - return invoice, nil - } - } else { - return data, nil + // Log the entire response for debugging + fmt.Printf("API Response: %+v\n", result) + + if isInvoiceNumber { + // Handle invoice number case + data, ok := result["data"].(map[string]interface{}) + if !ok { + return nil, nil // No invoice found + } + invoices, ok := data["invoices"].([]interface{}) + if !ok || len(invoices) == 0 { + return nil, nil // No invoice found + } + invoice, ok := invoices[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected invoice structure") } + return invoice, nil + } else { + // Handle invoice ID case + data, ok := result["data"].(map[string]interface{}) + if !ok || len(data) == 0 { + return nil, nil // No invoice found + } + return data, nil } - - return nil, fmt.Errorf("no invoice found in the response") } diff --git a/internal/handlers/web/invoices.go b/internal/handlers/web/invoices.go index cd7ff9e..5c105bf 100644 --- a/internal/handlers/web/invoices.go +++ b/internal/handlers/web/invoices.go @@ -1,6 +1,7 @@ package web import ( + "fmt" "html/template" "log" "marmic/servicetrade-toolbox/internal/api" @@ -15,7 +16,7 @@ func InvoicesHandler(w http.ResponseWriter, r *http.Request) { return } - if r.Method == "GET" && r.URL.Query().Get("search") != "" { + if r.Method == "GET" { handleInvoiceSearch(w, r, session) return } @@ -41,15 +42,42 @@ func handleInvoiceSearch(w http.ResponseWriter, r *http.Request, session *api.Se return } + log.Printf("Searching for invoice with term: %s", searchTerm) + invoice, err := session.GetInvoice(searchTerm) + + log.Printf("GetInvoice result - invoice: %+v, err: %v", invoice, err) + if err != nil { log.Printf("Error fetching invoice: %v", err) - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusOK) + errorMsg := fmt.Sprintf("Error fetching invoice: %v", err) + if strings.Contains(err.Error(), "access forbidden") { + errorMsg = "You do not have permission to view this invoice." + } + tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) + tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{ + "Error": true, + "ErrorMsg": errorMsg, + "SearchTerm": searchTerm, + }) + return + } + + if invoice == nil { + log.Printf("No invoice found for: %s", searchTerm) + w.WriteHeader(http.StatusOK) tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) - tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{"Error": err.Error()}) + tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{ + "NotFound": true, + "ErrorMsg": fmt.Sprintf("No invoice found for: %s", searchTerm), + "SearchTerm": searchTerm, + }) return } + log.Printf("Invoice found: %+v", invoice) + tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) err = tmpl.ExecuteTemplate(w, "invoice_search_results", invoice) if err != nil { diff --git a/static/css/styles.css b/static/css/styles.css index 06333a3..cbbeea1 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -288,4 +288,89 @@ body, html { .submenu-item a:hover, .submenu-item button:hover { background-color: #2b6cb0; +} + +/* Style for error and not-found message display */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.error, .not-found { + text-align: center; + padding: 0.75rem; + border-radius: 0.25rem; + font-size: 0.875rem; + margin-top: 0.5rem; + animation: fadeIn 0.3s ease-in-out; +} + +.error { + background-color: #fee2e2; + border: 1px solid #ef4444; + color: #b91c1c; +} + +.not-found { + background-color: #fef3c7; + border: 1px solid #f59e0b; + color: #92400e; +} + +.error::before, .not-found::before { + margin-right: 0.5rem; +} + +.error::before { + content: '⚠️'; +} + +.not-found::before { + content: '🔍'; +} + +/* Loading indicator */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator { + display: flex; + align-items: center; +} + +.htmx-request.htmx-indicator { + display: flex; + align-items: center; +} + +.loading-indicator { + width: 1.5rem; + height: 1.5rem; + border: 0.25rem solid #e2e8f0; + border-top: 0.25rem solid #4299e1; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 0.5rem; +} + +/* Fade effect for existing content */ +.htmx-request .fade-me-out { + opacity: 0.5; + transition: opacity 0.3s ease-in-out; +} + +/* Highlight effect for changed content */ +@keyframes highlightBackground { + 0% { background-color: #fef3c7; } + 100% { background-color: transparent; } +} + +.highlight-change { + animation: highlightBackground 2s ease-in-out; } \ No newline at end of file diff --git a/templates/partials/invoice_search.html b/templates/partials/invoice_search.html index 81b44fd..1af7169 100644 --- a/templates/partials/invoice_search.html +++ b/templates/partials/invoice_search.html @@ -1,13 +1,21 @@ {{define "invoice_search"}} - - -
Searching...
-
+
+ + +
+ Searching... +
+
+
+
+
+ +
{{end}} diff --git a/templates/partials/invoice_search_results.html b/templates/partials/invoice_search_results.html index 2fac7d0..f0be7dd 100644 --- a/templates/partials/invoice_search_results.html +++ b/templates/partials/invoice_search_results.html @@ -1,16 +1,24 @@ {{define "invoice_search_results"}} {{if .Error}} -

Error: {{.Error}}

+
+

{{.ErrorMsg}}

+

Invoice ID: "{{.SearchTerm}}"

+
+{{else if .NotFound}} +
+

{{.ErrorMsg}}

+

Invoice Number: "{{.SearchTerm}}"

+
{{else if .invoiceNumber}}

Invoice Details

-

Invoice Number: {{.invoiceNumber}}

-

Total Price: ${{.totalPrice}}

-

Status: {{.status}}

+

Invoice Number: {{.invoiceNumber}}

+

Total Price: ${{.totalPrice}}

+

Status: {{.status}}

{{with .customer}} -

Customer: {{.name}}

+

Customer: {{.name}}

{{end}} {{with .job}} -

Job: {{.name}}

+

Job: {{.name}}

{{end}} {{with .location}} -

Location: {{.name}}

+

Location: {{.name}}

{{end}} {{if .items}}

Items:

{{end}} {{else}} -

No invoice found with the given identifier.

+

Unexpected response. Please try again.

{{end}} {{end}}