diff --git a/internal/handlers/file.go b/internal/handlers/file.go index 85de176..da5b19f 100644 --- a/internal/handlers/file.go +++ b/internal/handlers/file.go @@ -10,6 +10,7 @@ import ( "database/sql" "encoding/csv" "fmt" + "html/template" "log" "net/http" "os" @@ -61,7 +62,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if eventID == "" { log.Printf("ERROR [%s] handlers: FileEvent: missing event ID in URL", time.Now().Format(time.RFC3339)) - http.Error(w, "Missing event ID", http.StatusBadRequest) + renderFileError(w, "Missing event ID.") return } @@ -69,7 +70,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { log.Printf("ERROR [%s] handlers: FileEvent: parse form: %v", time.Now().Format(time.RFC3339), err) - http.Error(w, "Cannot parse form data", http.StatusBadRequest) + renderFileError(w, "Cannot parse form data.") return } @@ -79,13 +80,13 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if to == "" { log.Printf("ERROR [%s] handlers: FileEvent: missing email field", time.Now().Format(time.RFC3339)) - http.Error(w, "Email address is required", http.StatusBadRequest) + renderFileError(w, "Email address is required.") return } if format != "csv" && format != "pdf" { log.Printf("ERROR [%s] handlers: FileEvent: invalid format %q", time.Now().Format(time.RFC3339), format) - http.Error(w, "Format must be 'csv' or 'pdf'", http.StatusBadRequest) + renderFileError(w, "Format must be 'csv' or 'pdf'.") return } @@ -94,7 +95,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if userID == "" { log.Printf("ERROR [%s] handlers: FileEvent: unauthenticated request", time.Now().Format(time.RFC3339)) - http.Error(w, "Unauthorized", http.StatusUnauthorized) + renderFileError(w, "Session expired. Please log in again.") return } @@ -102,19 +103,19 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("ERROR [%s] handlers: FileEvent: GetEventByID(%s): %v", time.Now().Format(time.RFC3339), eventID, err) - http.Error(w, "Failed to retrieve event", http.StatusInternalServerError) + renderFileError(w, "Failed to retrieve event. Please try again.") return } if event == nil { log.Printf("ERROR [%s] handlers: FileEvent: event not found: %s", time.Now().Format(time.RFC3339), eventID) - http.Error(w, "Event not found", http.StatusNotFound) + renderFileError(w, "Event not found.") return } if event.UserID != userID { log.Printf("ERROR [%s] handlers: FileEvent: user %s does not own event %s", time.Now().Format(time.RFC3339), userID, eventID) - http.Error(w, "Forbidden", http.StatusForbidden) + renderFileError(w, "You do not have permission to file this event.") return } @@ -123,7 +124,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("ERROR [%s] handlers: FileEvent: GetExpensesByEvent(%s): %v", time.Now().Format(time.RFC3339), eventID, err) - http.Error(w, "Failed to retrieve expenses", http.StatusInternalServerError) + renderFileError(w, "Failed to retrieve expenses. Please try again.") return } @@ -138,7 +139,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("ERROR [%s] handlers: FileEvent: generate %s report: %v", time.Now().Format(time.RFC3339), format, err) - http.Error(w, "Failed to generate report", http.StatusInternalServerError) + renderFileError(w, "Failed to generate report. Please try again.") return } @@ -158,7 +159,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if h.EmailSender == nil { log.Printf("ERROR [%s] handlers: FileEvent: SMTP not configured, cannot send email", time.Now().Format(time.RFC3339)) - http.Error(w, "SMTP not configured. Please set SMTP environment variables.", http.StatusInternalServerError) + renderFileError(w, "SMTP not configured. Please contact the administrator.") return } subject := "Expense report for event " + event.Name @@ -166,7 +167,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if err := h.EmailSender.SendReport(to, subject, body, attachments); err != nil { log.Printf("ERROR [%s] handlers: FileEvent: SendReport(%s): %v", time.Now().Format(time.RFC3339), to, err) - http.Error(w, "Failed to send report email", http.StatusInternalServerError) + renderFileError(w, "Failed to send report email. Please check the recipient address and try again.") return } @@ -174,7 +175,7 @@ func (h *FileHandler) FileEvent(w http.ResponseWriter, r *http.Request) { if err := database.UpdateEventStatus(h.DB, eventID, "closed"); err != nil { log.Printf("ERROR [%s] handlers: FileEvent: UpdateEventStatus(%s): %v", time.Now().Format(time.RFC3339), eventID, err) - http.Error(w, "Failed to close event", http.StatusInternalServerError) + renderFileError(w, "Report sent but failed to close the event. Please try again.") return } @@ -427,6 +428,17 @@ func createReceiptZip(eventName string, expenses []database.Expense) (*email.Att }, nil } +// renderFileError writes an HTMX-compatible error fragment targeted at the +// #submit-error container on the event expenses page. Using a 200 status +// ensures HTMX always swaps the content (HTMX skips 4xx/5xx by default). +func renderFileError(w http.ResponseWriter, message string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("HX-Retarget", "#submit-error") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `
%s
`, + template.HTMLEscapeString(message)) +} + // truncateString truncates a string to the given maximum length, appending "…" // if the string was shortened. func truncateString(s string, maxLen int) string { diff --git a/templates/event_expenses.html b/templates/event_expenses.html index b25bea8..856ea6e 100644 --- a/templates/event_expenses.html +++ b/templates/event_expenses.html @@ -128,7 +128,9 @@ {{if and (eq .Event.Status "open") .Expenses}}

Submit Event

-
+
+
@@ -152,6 +154,10 @@
+
+
+

Sending report via email…

+