- Restructure hierarchy: Month → Event → Expense (new months table, FK) - Add MonthHandler with CRUD, monthly reports, dropdown create form - Events now scoped under months with extended ownership chain - AI extraction: 16 specific expense categories (Airfare, Meals, etc.) - UI: category dropdown, button-consistent cards, centered mobile shell on desktop - Dashboard/month views show claim totals per card - Description field now mandatory, forms simplified - Months sorted by name chronologically (latest first)
138 lines
8.5 KiB
HTML
138 lines
8.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="#10b981">
|
|
<title>{{.Event.Name}} - NextExpense</title>
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
|
<link rel="alternate icon" href="/static/icons/icon-192.png">
|
|
<link rel="manifest" href="/manifest.json">
|
|
<link rel="stylesheet" href="/static/css/style.css?v=5">
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
</head>
|
|
<body>
|
|
<div class="app-shell">
|
|
<header class="app-header">
|
|
<a href="/months/{{.Month.ID}}" class="btn btn-secondary btn-sm"
|
|
hx-get="/months/{{.Month.ID}}" hx-target="body" hx-push-url="true">← {{.Month.Name}}</a>
|
|
<h1 class="app-title" style="font-size: 1.1rem;">{{.Event.Name}}</h1>
|
|
<button class="btn btn-secondary btn-sm" style="font-size: 0.75rem;"
|
|
hx-post="/logout" hx-target="body" hx-push-url="true">Logout</button>
|
|
</header>
|
|
|
|
<main class="main-content">
|
|
<!-- Breadcrumb -->
|
|
<div style="font-size: 0.75rem; color: var(--color-text-muted); margin-bottom: 1rem;">
|
|
<a href="/dashboard" style="color: var(--color-primary); text-decoration: none;"
|
|
hx-get="/dashboard" hx-target="body" hx-push-url="true">Dashboard</a>
|
|
› <a href="/months/{{.Month.ID}}" style="color: var(--color-primary); text-decoration: none;"
|
|
hx-get="/months/{{.Month.ID}}" hx-target="body" hx-push-url="true">{{.Month.Name}}</a>
|
|
› {{.Event.Name}}
|
|
</div>
|
|
|
|
<!-- Event Metadata -->
|
|
<div style="background: var(--color-card); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem;">
|
|
<div style="font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem;">Event Details</div>
|
|
<div class="form-row" style="margin:0;">
|
|
<div style="flex:2; font-size:0.9rem; color:var(--color-text);">{{.Event.Name}}</div>
|
|
<div style="flex:1; font-size:0.9rem; color:var(--color-text);">{{.Event.BaseCurrency}}</div>
|
|
<div style="flex:1; font-size:0.9rem; color:var(--color-text-muted);">{{printf "%.6f" .Event.ExchangeRate}}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Receipt List -->
|
|
<div id="expense-list">
|
|
<h3 style="margin-bottom: 1rem; font-size: 1rem; font-weight: 600;">
|
|
Receipts
|
|
{{if .Event.BaseCurrency}}
|
|
<span style="font-weight: 400; color: var(--color-text-muted);">(claim in {{.Event.BaseCurrency}})</span>
|
|
{{end}}
|
|
</h3>
|
|
|
|
{{if .Expenses}}
|
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
{{range .Expenses}}
|
|
<div style="display: flex; align-items: center; background: var(--color-card); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 0.75rem;">
|
|
<div style="flex: 1; min-width: 0;">
|
|
<div style="font-weight: 500; color: var(--color-text);">{{.Merchant}}</div>
|
|
<div style="font-size: 0.75rem; color: var(--color-text-muted);">{{.Date}} · {{.Category}} {{if .Description}}· {{.Description}}{{end}}</div>
|
|
</div>
|
|
<div style="text-align: right; margin-right: 0.75rem;">
|
|
<div style="font-weight: 600; color: var(--color-text);">{{printf "%.2f" .Amount}} {{.Currency}}</div>
|
|
{{if .ConvertedAmount}}
|
|
<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>
|
|
<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; flex-shrink: 0; color: #fca5a5;"
|
|
onclick="if(confirm('Delete this receipt?')) htmx.trigger('#del-{{.ID}}','click')"
|
|
title="Delete receipt">🗑️</button>
|
|
<div hx-delete="/expenses/{{.ID}}" hx-target="#expense-list" hx-swap="outerHTML" id="del-{{.ID}}" style="display:none"></div>
|
|
</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}}
|
|
{{end}}
|
|
</div>
|
|
{{else}}
|
|
<div style="text-align: center; padding: 2rem; color: var(--color-text-muted); font-size: 0.875rem;">
|
|
<p>No receipts yet.</p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<!-- Receipt Form (edit/add) -->
|
|
<div id="receipt-form" style="margin-top: 1.5rem;"></div>
|
|
|
|
<!-- Add Receipt Buttons -->
|
|
<div style="margin-top: 1.5rem; display: flex; gap: 0.75rem;">
|
|
<label for="receipt-camera" class="btn btn-primary" style="flex: 1; text-align: center; cursor: pointer;">
|
|
📷 Camera
|
|
</label>
|
|
<label for="receipt-upload" class="btn btn-secondary" style="flex: 1; text-align: center; cursor: pointer;">
|
|
📁 Upload
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Hidden file input: camera (mobile) -->
|
|
<input type="file" id="receipt-camera" name="receipt" accept="image/*" capture="environment" style="display: none;"
|
|
hx-post="/expenses/upload"
|
|
hx-encoding="multipart/form-data"
|
|
hx-target="#receipt-form"
|
|
hx-swap="innerHTML"
|
|
hx-indicator="#upload-indicator"
|
|
hx-trigger="change">
|
|
|
|
<!-- Hidden file input: gallery / PDF upload -->
|
|
<input type="file" id="receipt-upload" name="receipt" accept="image/*,.pdf" style="display: none;"
|
|
hx-post="/expenses/upload"
|
|
hx-encoding="multipart/form-data"
|
|
hx-target="#receipt-form"
|
|
hx-swap="innerHTML"
|
|
hx-indicator="#upload-indicator"
|
|
hx-trigger="change">
|
|
|
|
<div id="upload-indicator" class="htmx-indicator" style="text-align: center; padding: 1rem;">
|
|
<div class="spinner"></div>
|
|
<p>Analyzing receipt...</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>
|