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}}
+
+
+
+
+
+ {{with .customer}}
Customer: {{.name}}
{{end}}
+ {{with .job}}
Job: {{.name}}
{{end}}
+
Total: ${{.totalPrice}}
+
+
+
+
+ {{range .buttons}}
+
+ {{end}}
+
+
+
+
{{end}}
-
-{{end}} {{else}}
+
+
+{{else if .invoiceNumber}}
+
+
Invoice Details
+
+
+
+
+
+
+ {{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