From cfc1d6af6155f2d164493ec96bfdce4afd450336 Mon Sep 17 00:00:00 2001 From: nic Date: Fri, 18 Apr 2025 13:51:59 -0400 Subject: [PATCH] feat: added multi-invoice searching --- internal/handlers/web/invoices.go | 109 ++++++++- static/css/styles.css | 214 ++++++++++++++++++ templates/partials/invoice_search.html | 7 +- .../partials/invoice_search_results.html | 135 ++++++++--- 4 files changed, 425 insertions(+), 40 deletions(-) diff --git a/internal/handlers/web/invoices.go b/internal/handlers/web/invoices.go index cc25735..749f20c 100644 --- a/internal/handlers/web/invoices.go +++ b/internal/handlers/web/invoices.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "html/template" "io" "log" root "marmic/servicetrade-toolbox" @@ -76,34 +77,78 @@ func handleInvoiceSearch(w http.ResponseWriter, r *http.Request, session *api.Se return } - log.Printf("Searching for invoice with term: %s", searchTerm) + // Parse the search term for multiple invoice IDs + invoiceIDs := parseInvoiceIDs(searchTerm) + log.Printf("Processing %d invoice IDs from search term: %s", len(invoiceIDs), searchTerm) - invoice, err := session.GetInvoice(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 + } - // log.Printf("GetInvoice result - invoice: %+v, err: %v", invoice, err) + // 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", searchTerm) + 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": searchTerm, + "SearchTerm": invoiceID, }) return } if invoice == nil { - log.Printf("No invoice found for: %s", searchTerm) + 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", searchTerm), - "SearchTerm": searchTerm, + "ErrorMsg": fmt.Sprintf("No invoice found for: %s", invoiceID), + "SearchTerm": invoiceID, }) return } @@ -122,6 +167,53 @@ func handleInvoiceSearch(w http.ResponseWriter, r *http.Request, session *api.Se } } +// 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 @@ -168,6 +260,7 @@ func getInvoiceStatusButtons(invoiceID, currentStatus string) []map[string]strin "Label": button.Label, "Class": button.Class, "ConfirmText": button.ConfirmText, + "Status": button.Status, }) } } diff --git a/static/css/styles.css b/static/css/styles.css index 7f39331..b49d3f3 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -648,4 +648,218 @@ html { .success-button:disabled { background-color: var(--btn-disabled); cursor: not-allowed; +} + +/* Multiple Invoice Styles */ +.multiple-invoices { + margin-top: 20px; +} + +.invoice-summary { + background-color: var(--dashboard-bg); + padding: 10px 15px; + border-radius: 4px; + margin-bottom: 20px; + border: var(--content-border); +} + +.invoice-card { + border: var(--content-border); + border-radius: 6px; + margin-bottom: 15px; + box-shadow: var(--dashboard-shadow); + overflow: hidden; + background-color: var(--dashboard-bg); +} + +.invoice-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background-color: var(--content-bg); + border-bottom: var(--content-border); +} + +.invoice-header h4 { + margin: 0; + font-size: 16px; + color: var(--dashboard-header-color); +} + +.invoice-status { + padding: 4px 8px; + border-radius: 3px; + font-size: 12px; + font-weight: bold; + text-transform: uppercase; +} + +.invoice-status.ok { + background-color: var(--btn-success-bg); + color: white; + opacity: 0.8; +} + +.invoice-status.draft { + background-color: var(--btn-primary-bg); + color: white; + opacity: 0.8; +} + +.invoice-status.failed { + background-color: var(--btn-warning-bg); + color: white; + opacity: 0.8; +} + +.invoice-status.void { + background-color: var(--btn-disabled); + color: white; + opacity: 0.8; +} + +.invoice-status.pending_accounting { + background-color: var(--btn-caution-bg); + color: var(--text-color); + opacity: 0.8; +} + +.invoice-status.processed { + background-color: var(--btn-primary-bg); + color: white; + opacity: 0.7; +} + +.invoice-details { + display: flex; + padding: 15px; + justify-content: space-between; + align-items: flex-start; +} + +.invoice-info { + flex: 3; + color: var(--content-text); +} + +.invoice-info p { + margin: 5px 0; + font-size: 14px; +} + +.invoice-actions { + flex: 1; + display: flex; + justify-content: flex-end; + padding-left: 10px; +} + +.button-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.small-button { + font-size: 12px; + padding: 4px 8px; +} + +.error-text { + color: var(--btn-warning-bg); +} + +.compact-button { + width: 36px; + height: 36px; + border-radius: 50%; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + color: white; + font-size: 16px; + padding: 0; +} + +.compact-button:hover { + transform: scale(1.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.compact-button:active { + transform: scale(1); +} + +.compact-button.success-button { + background-color: var(--btn-success-bg); +} + +.compact-button.warning-button { + background-color: var(--btn-warning-bg); +} + +.compact-button.caution-button { + background-color: var(--btn-caution-bg); + color: var(--text-color); +} + +.invoice-items { + margin-top: 15px; + border-top: var(--content-border); + padding-top: 10px; +} + +.invoice-items h5 { + margin: 0 0 8px 0; + font-size: 14px; + color: var(--dashboard-header-color); +} + +.invoice-items ul { + margin: 0; + padding-left: 20px; + font-size: 13px; +} + +/* Responsive layout for mobile */ +@media (max-width: 768px) { + .invoice-details { + flex-direction: column; + } + + .invoice-actions { + margin-top: 15px; + justify-content: flex-start; + padding-left: 0; + } + + .button-group { + flex-direction: row; + flex-wrap: wrap; + } +} + +/* Mobile optimization */ +@media (max-width: 768px) { + .invoice-footer { + justify-content: center; + } + + .invoice-button { + flex: 1 1 calc(50% - 8px); + justify-content: center; + } + + .invoice-button .label { + display: none; + } + + .invoice-button .icon { + font-size: 18px; + margin: 0; + } } \ No newline at end of file diff --git a/templates/partials/invoice_search.html b/templates/partials/invoice_search.html index 4295ec0..53ec10c 100644 --- a/templates/partials/invoice_search.html +++ b/templates/partials/invoice_search.html @@ -3,9 +3,14 @@
- +
+ For multiple invoices, separate with commas or spaces (e.g., "123456, 789012" or "123456 789012") +
+
diff --git a/templates/partials/invoice_search_results.html b/templates/partials/invoice_search_results.html index 7bc0b6c..0c24599 100644 --- a/templates/partials/invoice_search_results.html +++ b/templates/partials/invoice_search_results.html @@ -1,43 +1,116 @@ -{{define "invoice_search_results"}} {{if .Error}} +{{define "invoice_search_results"}} +{{if .Error}}
{{.ErrorMsg}}

Search term: "{{.SearchTerm}}"

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

Search term: "{{.SearchTerm}}"

-{{else if .invoiceNumber}} -

Invoice Details

- -{{range .buttons}} - -{{end}} -

Invoice Number: {{.invoiceNumber}}

-

Total Price: ${{.totalPrice}}

-

Status: {{.status}}

- -{{with .customer}} -

Customer: {{.name}}

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

Job: {{.name}}

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

Location: {{.name}}

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

Items:

-
    - {{range .items}} -
  • {{.description}} - ${{.totalPrice}}
  • +{{else if .MultipleInvoices}} +
    +

    Multiple Invoices Found

    +
    +

    Found {{.TotalFound}} of {{.TotalSearched}} invoices for: "{{.SearchTerm}}"

    + {{if gt .FailedCount 0}} +

    Failed to find {{.FailedCount}} invoices: {{range $i, $id := .FailedIDs}}{{if $i}}, + {{end}}{{$id}}{{end}}

    + {{end}} +
    + + {{range .Invoices}} +
    +
    +

    Invoice #{{.invoiceNumber}}

    +
    {{.status}}
    +
    + +
    +
    + {{with .customer}}

    Customer: {{.name}}

    {{end}} + {{with .job}}

    Job: {{.name}}

    {{end}} +

    Total: ${{.totalPrice}}

    +
    + +
    +
    + {{range .buttons}} + + {{end}} +
    +
    +
    +
    {{end}} -
-{{end}} {{else}} +
+ +{{else if .invoiceNumber}} +
+

Invoice Details

+ +
+
+

Invoice #{{.invoiceNumber}}

+
{{.status}}
+
+ +
+
+ {{with .customer}}

Customer: {{.name}}

{{end}} + {{with .job}}

Job: {{.name}}

{{end}} + {{with .location}}

Location: {{.name}}

{{end}} +

Total Price: ${{.totalPrice}}

+ + {{if .items}} +
+
Items
+
    + {{range .items}} +
  • {{.description}} - ${{.totalPrice}}
  • + {{end}} +
+
+ {{end}} +
+ +
+
+ {{range .buttons}} + + {{end}} +
+
+
+
+
+ +{{else}}

Unexpected response. Please try again.

-{{end}} {{end}} +{{end}} +{{end}} \ No newline at end of file