feat: event editing — change exchange rate and currency

- Added Edit button on open event cards in dashboard
- EditEvent handler returns dashboard.html with pre-filled form
- UpdateEvent handler (PUT /events/{id}) saves new currency + rate
- UpdateEvent in db.go updates base_currency and exchange_rate
- Form auto-switches between create (POST) and edit (PUT) mode
- Reuses the same conversion sample pattern as event creation
This commit is contained in:
Claus Lohmar 2026-06-03 07:33:37 +00:00
parent 4ea3d63b29
commit 5f5f2be75b
4 changed files with 105 additions and 6 deletions

View file

@ -287,6 +287,16 @@ func UpdateEventStatus(db *sql.DB, id, status string) error {
return err
}
// UpdateEvent updates the base currency and exchange rate of an existing event.
func UpdateEvent(db *sql.DB, id, baseCurrency string, exchangeRate float64) error {
_, err := db.Exec("UPDATE events SET base_currency = ?, exchange_rate = ? WHERE id = ?", baseCurrency, exchangeRate, id)
if err != nil {
log.Printf("ERROR [%s] database: UpdateEvent(%s): %v",
time.Now().Format(time.RFC3339), id, err)
}
return err
}
// ---------------------------------------------------------------------------
// Expense queries
// ---------------------------------------------------------------------------

View file

@ -259,3 +259,71 @@ func (h *EventHandler) ViewEventExpenses(w http.ResponseWriter, r *http.Request)
time.Now().Format(time.RFC3339), err)
}
}
// ---------------------------------------------------------------------------
// GET /events/{id}/edit — EditEvent
// ---------------------------------------------------------------------------
// EditEvent returns the event edit form pre-filled with current event data.
func (h *EventHandler) EditEvent(w http.ResponseWriter, r *http.Request) {
eventID := chi.URLParam(r, "id")
event, err := database.GetEventByID(h.DB, eventID)
if err != nil || event == nil || event.UserID != getUserID(r) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
tmpl := getTemplate("dashboard.html")
data := map[string]interface{}{
"Event": event,
"Editing": true,
"Events": []database.Event{},
"BaseCurrency": event.BaseCurrency,
"ExchangeRate": event.ExchangeRate,
"LastSampleRcpt": "",
"LastSampleClm": "",
}
if err := tmpl.Execute(w, data); err != nil {
log.Printf("ERROR [%s] handlers: EditEvent: execute template: %v",
time.Now().Format(time.RFC3339), err)
}
}
// ---------------------------------------------------------------------------
// PUT /events/{id} — UpdateEvent
// ---------------------------------------------------------------------------
// UpdateEvent updates the event's base currency and exchange rate.
func (h *EventHandler) UpdateEvent(w http.ResponseWriter, r *http.Request) {
eventID := chi.URLParam(r, "id")
event, err := database.GetEventByID(h.DB, eventID)
if err != nil || event == nil || event.UserID != getUserID(r) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
baseCurrency := r.FormValue("base_currency")
if baseCurrency == "" {
baseCurrency = "USD"
}
exchangeRate := 1.0
sampleReceipt := r.FormValue("sample_receipt_amount")
sampleClaim := r.FormValue("sample_claim_amount")
if sampleReceipt != "" && sampleClaim != "" {
sampleReceiptVal, err1 := strconv.ParseFloat(sampleReceipt, 64)
sampleClaimVal, err2 := strconv.ParseFloat(sampleClaim, 64)
if err1 == nil && err2 == nil && sampleReceiptVal > 0 && sampleClaimVal > 0 {
exchangeRate = sampleClaimVal / sampleReceiptVal
}
}
if err := database.UpdateEvent(h.DB, eventID, baseCurrency, exchangeRate); err != nil {
log.Printf("ERROR [%s] handlers: UpdateEvent: %v", time.Now().Format(time.RFC3339), err)
http.Error(w, "Failed to update event", http.StatusInternalServerError)
return
}
w.Header().Set("HX-Redirect", "/dashboard")
w.WriteHeader(http.StatusOK)
}

View file

@ -205,6 +205,8 @@ func main() {
// Events.
r.Get("/dashboard", eventHandler.Dashboard)
r.Post("/events", eventHandler.CreateEvent)
r.Put("/events/{id}", eventHandler.UpdateEvent)
r.Get("/events/{id}/edit", eventHandler.EditEvent)
r.Put("/events/{id}/reopen", eventHandler.ReopenEvent)
r.Get("/events/{id}/expenses", eventHandler.ViewEventExpenses)

View file

@ -28,20 +28,26 @@
<div id="create-event-form" class="hidden" style="margin-bottom: 1rem;">
<div class="card" style="padding: 1rem;">
{{if .Editing}}
<h3 style="margin-bottom: 1rem;">Edit Event</h3>
<form hx-put="/events/{{.Event.ID}}" hx-target="body" hx-push-url="true">
{{else}}
<form hx-post="/events" hx-target="body" hx-push-url="true">
{{end}}
<div class="form-group">
<input type="text" id="name" name="name" placeholder="Event name, e.g. WebSummit 2026" required>
<label class="form-label">{{if .Editing}}Event{{else}}Event Name{{end}}</label>
<input type="text" id="name" name="name" placeholder="Event name" value="{{.Event.Name}}" {{if not .Editing}}required{{end}}>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Claim Currency</label>
<input type="text" id="base_currency" name="base_currency" value="USD" placeholder="USD, EUR, KES…" required maxlength="3" style="text-transform: uppercase;">
<input type="text" id="base_currency" name="base_currency" value="{{if .Editing}}{{.Event.BaseCurrency}}{{else}}USD{{end}}" placeholder="USD, EUR, KES…" required maxlength="3" style="text-transform: uppercase;">
</div>
</div>
<div style="background: #064e3b; border: 1px solid #065f46; border-radius: 0.5rem; padding: 0.75rem; margin-bottom: 0.5rem;">
<div style="font-size: 0.8rem; font-weight: 600; color: #6ee7b7; margin-bottom: 0.5rem;">Conversion Sample</div>
<p style="font-size: 0.75rem; color: var(--color-text-muted); margin-bottom: 0.5rem;">
From a payment notification: receipt amount and what you were charged.
{{if .Editing}}Update the sample to recalculate the rate.{{else}}From a payment notification: receipt amount and what you were charged.{{end}}
</p>
<div class="form-row">
<div class="form-group">
@ -50,15 +56,19 @@
</div>
<div class="form-group" style="flex: 0 0 110px;">
<label class="form-label">Currency</label>
<input type="text" id="sample_receipt_currency" name="sample_receipt_currency" value="KES" placeholder="KES" required maxlength="3" style="text-transform: uppercase;">
<input type="text" id="sample_receipt_currency" name="sample_receipt_currency" value="{{if .Editing}}{{.Event.BaseCurrency}}{{else}}KES{{end}}" placeholder="KES" required maxlength="3" style="text-transform: uppercase;">
</div>
</div>
<div class="form-group">
<label class="form-label">What you were charged (claim currency)</label>
<label class="form-label">{{if .Editing}}Converted amount{{else}}What you were charged (claim currency){{end}}</label>
<input type="number" id="sample_claim_amount" name="sample_claim_amount" step="0.01" min="0.01" placeholder="e.g. 7.73" required>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Create Event</button>
<button type="submit" class="btn btn-primary btn-block">{{if .Editing}}Save Changes{{else}}Create Event{{end}}</button>
{{if .Editing}}
<a href="/dashboard" class="btn btn-secondary btn-block" style="display: block; text-align: center; margin-top: 0.5rem;"
hx-get="/dashboard" hx-target="body" hx-push-url="true">Cancel</a>
{{end}}
</form>
</div>
</div>
@ -80,6 +90,15 @@
style="text-decoration: none;">
Open
</a>
{{if eq .Status "open"}}
<button class="btn btn-secondary btn-sm"
hx-get="/events/{{.ID}}/edit"
hx-target="#create-event-form"
hx-swap="innerHTML"
style="font-size: 0.75rem;">
Edit
</button>
{{end}}
{{if eq .Status "closed"}}
<button class="btn btn-secondary btn-sm"
hx-put="/events/{{.ID}}/reopen"