feat: view receipt image from expense list

- Added 🖼️ button on each expense — opens full-screen lightbox
- Click anywhere on the overlay to close
- Images served via /storage/{filename} (auth-protected)
- Fixed ImagePath to store only filename (was storage/storage/...)
- Both expense_list.html fragment and event_expenses.html page updated
This commit is contained in:
Claus Lohmar 2026-06-01 23:16:35 +00:00
parent 4593dd1f66
commit 8ff1a4175a
3 changed files with 38 additions and 9 deletions

View file

@ -136,8 +136,11 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) {
exchangeRate = 1.0
}
// 9. Call the Gemini Vision API for AI extraction.
receipt, aiErr := ai.ExtractReceipt(storagePath)
// 9. Call the Gemini Vision API for AI extraction (uses full disk path).
receipt, aiErr := ai.ExtractReceipt(filepath.Join("storage", filename))
// Strip the storage/ prefix so the template can build a proper URL: /storage/{file}
storagePath = filename
// 10. Render the receipt_form.html fragment.
tmpl := getTemplate("receipt_form.html")

View file

@ -43,12 +43,25 @@
<div style="font-size: 0.75rem; color: var(--color-primary);">{{printf "%.2f" .ConvertedAmount}} {{.BaseCurrency}}</div>
{{end}}
</div>
<div style="display: flex; gap: 0.25rem;">
{{if .ImagePath}}
<button class="btn btn-sm" style="background: none; border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer; flex-shrink: 0; color: var(--color-text);"
onclick="document.getElementById('img-{{.ID}}').classList.toggle('hidden')"
title="View receipt image">🖼️</button>
{{end}}
<button class="btn btn-sm" style="background: none; border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer; flex-shrink: 0; color: var(--color-text);"
hx-get="/expenses/{{.ID}}/edit"
hx-target="#receipt-form"
hx-swap="innerHTML"
title="Edit receipt">✏️</button>
</div>
</div>
{{if .ImagePath}}
<div id="img-{{.ID}}" class="hidden" style="position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: 999; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 1rem;"
onclick="this.classList.add('hidden')">
<img src="/storage/{{.ImagePath}}" alt="Receipt" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 0.5rem;">
</div>
{{end}}
{{end}}
</div>
{{else}}

View file

@ -28,8 +28,15 @@
</div>
{{end}}
</div>
<div style="display: flex; align-items: center; margin-left: 0.5rem;">
<button class="btn btn-sm" style="background: none; border: 1px solid #d1d5db; border-radius: 0.375rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer;"
<div style="display: flex; align-items: center; gap: 0.25rem;">
{{if .ImagePath}}
<button class="btn btn-sm" style="background: none; border: 1px solid #475569; border-radius: 0.375rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer;"
onclick="document.getElementById('img-{{.ID}}').classList.toggle('hidden')"
title="View receipt image">
🖼️
</button>
{{end}}
<button class="btn btn-sm" style="background: none; border: 1px solid #475569; border-radius: 0.375rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer;"
hx-get="/expenses/{{.ID}}/edit"
hx-target="#receipt-form"
hx-swap="innerHTML"
@ -37,6 +44,12 @@
✏️
</button>
</div>
{{if .ImagePath}}
<div id="img-{{.ID}}" class="hidden" style="position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: 999; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 1rem;"
onclick="this.classList.add('hidden')">
<img src="/storage/{{.ImagePath}}" alt="Receipt" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 0.5rem;">
</div>
{{end}}
</div>
{{end}}
</div>