- Passwordless email OTP authentication - Event-based expense tracking with HTMX UI - AI receipt extraction via DeepSeek Vision API - CSV/PDF report generation with email filing - PWA with service worker and manifest - Mobile-first responsive design - SQLite database with auto-migration
146 lines
7.2 KiB
HTML
146 lines
7.2 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}} - ExpenseFlow</title>
|
|
<link rel="manifest" href="/manifest.json">
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
</head>
|
|
<body>
|
|
<div class="app-shell">
|
|
<header class="app-header">
|
|
<a href="/dashboard" class="btn btn-secondary btn-sm"
|
|
hx-get="/dashboard" hx-target="body" hx-push-url="true">
|
|
← Back
|
|
</a>
|
|
<h1 class="app-title">{{.Event.Name}}</h1>
|
|
<span class="badge badge-{{.Event.Status}}">{{.Event.Status}}</span>
|
|
</header>
|
|
|
|
<main class="main-content">
|
|
<!-- Capture Receipt Button -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<label for="receipt-upload" class="btn btn-primary btn-block" style="display: inline-block; text-align: center; cursor: pointer;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 0.5rem;">
|
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
<circle cx="12" cy="13" r="4"/>
|
|
</svg>
|
|
Capture Receipt
|
|
</label>
|
|
<input type="file" id="receipt-upload" 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">
|
|
<div id="upload-indicator" class="htmx-indicator" style="text-align: center; padding: 1rem;">
|
|
<div class="spinner"></div>
|
|
<p>Analyzing receipt...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Receipt Form (filled by AI or empty) -->
|
|
<div id="receipt-form"></div>
|
|
|
|
<!-- Expense List -->
|
|
<div id="expense-list">
|
|
<h3 style="margin-bottom: 1rem;">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>
|
|
</div>
|
|
</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="18"/>
|
|
<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>
|
|
|
|
<!-- File Event Button (only for open events) -->
|
|
{{if eq .Event.Status "open"}}
|
|
<div style="margin-top: 2rem; text-align: center;">
|
|
<button class="btn btn-secondary"
|
|
onclick="document.getElementById('file-modal').classList.toggle('hidden')">
|
|
File Event
|
|
</button>
|
|
</div>
|
|
|
|
<!-- File Event Modal -->
|
|
<div id="file-modal" class="modal-overlay hidden">
|
|
<div class="modal-content">
|
|
<h3>File Event Report</h3>
|
|
<p class="text-secondary">Generate and email the expense report for "{{.Event.Name}}".</p>
|
|
<form hx-post="/events/{{.Event.ID}}/file" hx-target="body" hx-push-url="true">
|
|
<div class="form-group">
|
|
<label for="file-email">Send to</label>
|
|
<input type="email" id="file-email" name="email" placeholder="recipient@example.com" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Format</label>
|
|
<div style="display: flex; gap: 1rem;">
|
|
<label class="radio-label">
|
|
<input type="radio" name="format" value="csv" checked> CSV
|
|
</label>
|
|
<label class="radio-label">
|
|
<input type="radio" name="format" value="pdf"> PDF
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
|
<button type="button" class="btn btn-secondary"
|
|
onclick="document.getElementById('file-modal').classList.add('hidden')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Send Report & Close</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
// Auto-trigger file input on label click
|
|
document.querySelector('label[for="receipt-upload"]')?.addEventListener('click', function() {
|
|
document.getElementById('receipt-upload').click();
|
|
});
|
|
|
|
// Close modal on overlay click
|
|
document.querySelector('.modal-overlay')?.addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
this.classList.add('hidden');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|