- Fix createReceiptZip path traversal guard blocking new ImagePath entries
(UploadReceipt stores bare filenames, guard required 'storage/' prefix)
- Add DeleteExpense DB function, handler with ownership verification,
DELETE /expenses/{id} route, and 🗑️ button in both expense templates
- Uses existing htmx.trigger confirm pattern for delete UX
75 lines
4.2 KiB
HTML
75 lines
4.2 KiB
HTML
<div id="expense-list">
|
|
<h3 style="margin-bottom: 1rem;">Expenses ({{len .Expenses}})</h3>
|
|
|
|
{{if .Expenses}}
|
|
<div class="expense-list">
|
|
{{range .Expenses}}
|
|
<div class="expense-item">
|
|
<div class="expense-item-icon">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<polyline points="21 15 16 10 5 21"/>
|
|
</svg>
|
|
</div>
|
|
<div class="expense-item-info">
|
|
<div class="expense-item-merchant">{{.Merchant}}</div>
|
|
<div class="expense-item-meta">{{.Date}} · {{.Category}}</div>
|
|
{{if .Description}}
|
|
<div class="expense-item-desc">{{.Description}}</div>
|
|
{{end}}
|
|
</div>
|
|
<div class="expense-item-amount">
|
|
<span class="amount">{{printf "%.2f" .Amount}}</span>
|
|
<span class="currency">{{.Currency}}</span>
|
|
{{if .ConvertedAmount}}
|
|
<div style="font-size: 0.75rem; color: #166534;">
|
|
≈ {{printf "%.2f" .ConvertedAmount}} {{.BaseCurrency}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
<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"
|
|
title="Edit expense">
|
|
✏️
|
|
</button>
|
|
<button class="btn btn-sm" style="background: none; border: 1px solid #7f1d1d; border-radius: 0.375rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer; color: #fca5a5;"
|
|
onclick="if(confirm('Delete this expense?')) htmx.trigger('#del-{{.ID}}','click')"
|
|
title="Delete expense">
|
|
🗑️
|
|
</button>
|
|
<div hx-delete="/expenses/{{.ID}}" hx-target="#expense-list" hx-swap="outerHTML" id="del-{{.ID}}" style="display:none"></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; align-items: center; justify-content: center; cursor: pointer; padding: 1rem;"
|
|
onclick="this.classList.add('hidden')">
|
|
<span style="position: absolute; top: 1rem; right: 1rem; font-size: 2rem; color: #fff; line-height: 1; cursor: pointer; z-index: 1000;">×</span>
|
|
<img src="/storage/{{.ImagePath}}" alt="Receipt" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 0.5rem;" onclick="event.stopPropagation()">
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{else}}
|
|
<div class="empty-state" style="padding: 2rem;">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
<polyline points="14 2 14 8 20 8"/>
|
|
<line x1="12" y1="18" x2="12" y2="12"/>
|
|
<line x1="9" y1="15" x2="15" y2="15"/>
|
|
</svg>
|
|
<h3>No Expenses Yet</h3>
|
|
<p>Capture a receipt to get started.</p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|