feat: rebrand to ReceiptNext with 'The Terminal Mint' dark palette
- New favicon: Rx symbol in emerald green square (prescription/receipt) - Dark theme: #0F172A base, #1E293B cards, #F8FAFC text - Emerald #10B981 primary accent throughout - Dashboard simplified to list view with event name + Open button - Event page: receipt list with edit, Add Receipt button, Submit Event form - Submit form shows total claim amount + email + format selection - PWA manifest + service worker updated for ReceiptNext branding - Variables-based theming for easy palette switching
This commit is contained in:
parent
ded4954c72
commit
67f5ed5624
8 changed files with 178 additions and 242 deletions
|
|
@ -249,9 +249,17 @@ func (h *EventHandler) ViewEventExpenses(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalClaim := 0.0
|
||||||
|
for _, exp := range expenses {
|
||||||
|
if exp.ConvertedAmount > 0 {
|
||||||
|
totalClaim += exp.ConvertedAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"Event": event,
|
"Event": event,
|
||||||
"Expenses": expenses,
|
"Expenses": expenses,
|
||||||
|
"TotalClaim": totalClaim,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,47 @@
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
ExpenseFlow — PWA Expense Tracker Stylesheet
|
ReceiptNext — PWA Expense Tracker Stylesheet
|
||||||
Mobile-first responsive | Plain CSS | System-ui font
|
"The Terminal Mint" dark palette | Mobile-first responsive
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------
|
/* --------------------------------------------------------------------------
|
||||||
0. CSS Custom Properties (Design Tokens)
|
0. CSS Custom Properties (Design Tokens)
|
||||||
-------------------------------------------------------------------------- */
|
-------------------------------------------------------------------------- */
|
||||||
:root {
|
:root {
|
||||||
/* Colors */
|
/* Colors — "The Terminal Mint" Dark Palette */
|
||||||
--color-primary: #10b981;
|
--color-primary: #10b981;
|
||||||
--color-primary-hover: #059669;
|
--color-primary-hover: #34d399;
|
||||||
--color-primary-light: #d1fae5;
|
--color-primary-light: #064e3b;
|
||||||
--color-primary-dark: #047857;
|
--color-primary-dark: #047857;
|
||||||
|
|
||||||
--color-secondary: #1e293b;
|
--color-secondary: #334155;
|
||||||
--color-secondary-hover: #334155;
|
--color-secondary-hover: #475569;
|
||||||
|
|
||||||
--color-bg: #f8fafc;
|
--color-bg: #0f172a;
|
||||||
--color-card: #ffffff;
|
--color-card: #1e293b;
|
||||||
--color-text: #0f172a;
|
--color-text: #f8fafc;
|
||||||
--color-text-muted: #64748b;
|
--color-text-muted: #94a3b8;
|
||||||
--color-text-light: #94a3b8;
|
--color-text-light: #64748b;
|
||||||
|
|
||||||
--color-border: #e2e8f0;
|
--color-border: #334155;
|
||||||
--color-border-focus: #10b981;
|
--color-border-focus: #10b981;
|
||||||
|
|
||||||
--color-danger: #ef4444;
|
--color-danger: #ef4444;
|
||||||
--color-danger-hover: #dc2626;
|
--color-danger-hover: #f87171;
|
||||||
--color-danger-light: #fef2f2;
|
--color-danger-light: #450a0a;
|
||||||
|
|
||||||
--color-success: #10b981;
|
--color-success: #10b981;
|
||||||
--color-warning: #f59e0b;
|
--color-warning: #f59e0b;
|
||||||
|
|
||||||
--color-open-bg: #d1fae5;
|
--color-open-bg: #064e3b;
|
||||||
--color-open-text: #065f46;
|
--color-open-text: #6ee7b7;
|
||||||
--color-closed-bg: #f1f5f9;
|
--color-closed-bg: #334155;
|
||||||
--color-closed-text: #475569;
|
--color-closed-text: #cbd5e1;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows — more pronounced on dark bg */
|
||||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
--shadow-md: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
--shadow-md: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.04);
|
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
--shadow-xl: 0 10px 25px rgba(0, 0, 0, 0.08);
|
--shadow-xl: 0 10px 25px rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
/* Border radius */
|
/* Border radius */
|
||||||
--radius-sm: 6px;
|
--radius-sm: 6px;
|
||||||
|
|
|
||||||
4
static/favicon.svg
Normal file
4
static/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" rx="12" fill="#10b981"/>
|
||||||
|
<text x="32" y="42" font-family="Georgia, 'Times New Roman', serif" font-size="36" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="-2">Rx</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 317 B |
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "ExpenseFlow",
|
"name": "ReceiptNext",
|
||||||
"short_name": "ExpenseFlow",
|
"short_name": "ReceiptNext",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#10b981",
|
"theme_color": "#10b981",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#0f172a",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static/icons/icon-192.png",
|
"src": "/static/icons/icon-192.png",
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* ExpenseFlow — Service Worker
|
* ReceiptNext — Service Worker
|
||||||
* Version: 1.0.0
|
* Version: 1.0.0
|
||||||
* Cache name: expenseflow-v1
|
* Cache name: receiptnext-v1
|
||||||
* Strategy: Cache-first for shell assets, network-only for API
|
* Strategy: Cache-first for shell assets, network-only for API
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
|
|
||||||
const CACHE_NAME = 'expenseflow-v1';
|
const CACHE_NAME = 'receiptnext-v1';
|
||||||
|
|
||||||
// Shell assets to pre-cache on install
|
// Shell assets to pre-cache on install
|
||||||
const SHELL_ASSETS = [
|
const SHELL_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/static/css/style.css',
|
'/static/css/style.css',
|
||||||
|
'/static/favicon.svg',
|
||||||
'https://unpkg.com/htmx.org@1.9.10'
|
'https://unpkg.com/htmx.org@1.9.10'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -194,7 +195,7 @@ self.addEventListener('fetch', event => {
|
||||||
// Return a minimal offline fallback for navigations
|
// Return a minimal offline fallback for navigations
|
||||||
if (request.mode === 'navigate') {
|
if (request.mode === 'navigate') {
|
||||||
return new Response(
|
return new Response(
|
||||||
'<!DOCTYPE html><html><head><title>Offline — ExpenseFlow</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#f9fafb;color:#111827}h1{font-size:1.5rem;margin-bottom:0.5rem}p{color:#6b7280;max-width:24rem}</style></head><body><h1>You\'re offline</h1><p>ExpenseFlow needs an internet connection to load. Please check your connection and try again.</p></body></html>',
|
'<!DOCTYPE html><html><head><title>Offline — ReceiptNext</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#0f172a;color:#f8fafc}h1{font-size:1.5rem;margin-bottom:0.5rem}p{color:#94a3b8;max-width:24rem}</style></head><body><h1>You\'re offline</h1><p>ReceiptNext needs an internet connection to load. Please check your connection and try again.</p></body></html>',
|
||||||
{
|
{
|
||||||
status: 503,
|
status: 503,
|
||||||
statusText: 'Service Unavailable',
|
statusText: 'Service Unavailable',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="theme-color" content="#10b981">
|
<meta name="theme-color" content="#10b981">
|
||||||
<title>Dashboard - ExpenseFlow</title>
|
<title>ReceiptNext</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="manifest" href="/manifest.json">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
|
@ -12,31 +14,25 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<h1 class="app-title">ExpenseFlow</h1>
|
<h1 class="app-title"><img src="/static/favicon.svg" width="24" height="24" alt="" style="vertical-align: middle; margin-right: 0.5rem;">ReceiptNext</h1>
|
||||||
<a href="/" class="btn btn-secondary btn-sm"
|
|
||||||
hx-post="/logout" hx-target="body" hx-push-url="true"
|
|
||||||
hx-confirm="Are you sure you want to logout?">Logout</a>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="dashboard-header">
|
<div class="dashboard-header">
|
||||||
<h2>My Events</h2>
|
<h2>My Events</h2>
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary btn-sm"
|
||||||
onclick="document.getElementById('create-event-form').classList.toggle('hidden')">
|
onclick="document.getElementById('create-event-form').classList.toggle('hidden')">
|
||||||
+ New Event
|
+ New
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="create-event-form" class="hidden" style="margin-bottom: 1.5rem;">
|
<div id="create-event-form" class="hidden" style="margin-bottom: 1rem;">
|
||||||
<div class="card" style="padding: 1rem;">
|
<div class="card" style="padding: 1rem;">
|
||||||
<form hx-post="/events" hx-target="body" hx-push-url="true">
|
<form hx-post="/events" hx-target="body" hx-push-url="true">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Event Name</label>
|
<input type="text" id="name" name="name" placeholder="Event name, e.g. WebSummit 2026" required>
|
||||||
<input type="text" id="name" name="name" placeholder="e.g., WebSummit 2026" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="base_currency">Claim Currency</label>
|
|
||||||
<select id="base_currency" name="base_currency" required>
|
<select id="base_currency" name="base_currency" required>
|
||||||
<option value="EUR">EUR - Euro</option>
|
<option value="EUR">EUR - Euro</option>
|
||||||
<option value="USD" selected>USD - US Dollar</option>
|
<option value="USD" selected>USD - US Dollar</option>
|
||||||
|
|
@ -48,45 +44,26 @@
|
||||||
<option value="PLN">PLN - Polish Zloty</option>
|
<option value="PLN">PLN - Polish Zloty</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 0.5rem; padding: 0.75rem; margin-bottom: 0.5rem;">
|
||||||
<div class="card" style="padding: 0.75rem; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 0.5rem; margin-bottom: 0.5rem;">
|
<div style="font-size: 0.8rem; font-weight: 600; color: #166534; margin-bottom: 0.5rem;">Conversion Sample</div>
|
||||||
<div style="font-size: 0.875rem; font-weight: 600; color: #166534; margin-bottom: 0.5rem;">
|
<p style="font-size: 0.75rem; color: #4b5563; margin-bottom: 0.5rem;">
|
||||||
💱 Conversion Sample
|
From a payment notification: receipt amount and what you were charged.
|
||||||
</div>
|
|
||||||
<p style="font-size: 0.8rem; color: #4b5563; margin-bottom: 0.75rem;">
|
|
||||||
Enter a real receipt amount and what you were actually charged in your claim currency.
|
|
||||||
The exchange rate is calculated automatically. Check your payment app/bank notification for the exact converted amount.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sample_receipt_amount">Receipt Amount</label>
|
<input type="number" id="sample_receipt_amount" name="sample_receipt_amount" step="0.01" min="0.01" placeholder="Receipt amount" required>
|
||||||
<input type="number" id="sample_receipt_amount" name="sample_receipt_amount" step="0.01" min="0.01" placeholder="e.g. 1000" required inputmode="decimal">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="flex: 0 0 120px;">
|
<div class="form-group" style="flex: 0 0 100px;">
|
||||||
<label for="sample_receipt_currency">Currency</label>
|
|
||||||
<select id="sample_receipt_currency" name="sample_receipt_currency" required>
|
<select id="sample_receipt_currency" name="sample_receipt_currency" required>
|
||||||
<option value="KES" selected>KES</option>
|
<option value="KES" selected>KES</option>
|
||||||
<option value="USD">USD</option>
|
<option value="USD">USD</option>
|
||||||
<option value="EUR">EUR</option>
|
<option value="EUR">EUR</option>
|
||||||
<option value="GBP">GBP</option>
|
<option value="GBP">GBP</option>
|
||||||
<option value="JPY">JPY</option>
|
|
||||||
<option value="CHF">CHF</option>
|
|
||||||
<option value="SEK">SEK</option>
|
|
||||||
<option value="NOK">NOK</option>
|
|
||||||
<option value="DKK">DKK</option>
|
|
||||||
<option value="PLN">PLN</option>
|
|
||||||
<option value="ZAR">ZAR</option>
|
|
||||||
<option value="NGN">NGN</option>
|
|
||||||
<option value="TZS">TZS</option>
|
|
||||||
<option value="UGX">UGX</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sample_claim_amount">What you claimed / were charged</label>
|
<input type="number" id="sample_claim_amount" name="sample_claim_amount" step="0.01" min="0.01" placeholder="What you were charged" required>
|
||||||
<input type="number" id="sample_claim_amount" name="sample_claim_amount" step="0.01" min="0.01" placeholder="e.g. 7.73" required inputmode="decimal">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-block">Create Event</button>
|
<button type="submit" class="btn btn-primary btn-block">Create Event</button>
|
||||||
|
|
@ -96,33 +73,27 @@
|
||||||
|
|
||||||
<div id="event-list">
|
<div id="event-list">
|
||||||
{{if .Events}}
|
{{if .Events}}
|
||||||
<div class="event-grid">
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
{{range .Events}}
|
{{range .Events}}
|
||||||
<div class="card event-card">
|
<div style="display: flex; align-items: center; justify-content: space-between; background: white; border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 0.75rem 1rem;">
|
||||||
<div class="event-card-header">
|
<div>
|
||||||
<h3 class="event-card-name">{{.Name}}</h3>
|
<div style="font-weight: 500;">{{.Name}}</div>
|
||||||
<span id="status-badge-{{.ID}}" class="badge badge-{{.Status}}">{{.Status}}</span>
|
<div style="font-size: 0.75rem; color: #6b7280;">
|
||||||
|
{{.BaseCurrency}} · {{printf "%.6f" .ExchangeRate}} rate
|
||||||
</div>
|
</div>
|
||||||
<div class="event-card-meta">
|
|
||||||
<span>Created: {{.CreatedAt}}</span>
|
|
||||||
<span>Claim in: {{.BaseCurrency}} @ {{printf "%.6f" .ExchangeRate}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="event-card-actions">
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
{{if eq .Status "open"}}
|
|
||||||
<a href="/events/{{.ID}}/expenses" class="btn btn-primary btn-sm"
|
<a href="/events/{{.ID}}/expenses" class="btn btn-primary btn-sm"
|
||||||
hx-get="/events/{{.ID}}/expenses" hx-target="body" hx-push-url="true">
|
|
||||||
Add Expenses
|
|
||||||
</a>
|
|
||||||
<a href="/events/{{.ID}}/expenses" class="btn btn-secondary btn-sm"
|
|
||||||
hx-get="/events/{{.ID}}/expenses" hx-target="body" hx-push-url="true"
|
hx-get="/events/{{.ID}}/expenses" hx-target="body" hx-push-url="true"
|
||||||
style="border-color: #10b981; color: #10b981;">
|
style="text-decoration: none;">
|
||||||
File Report
|
Open
|
||||||
</a>
|
</a>
|
||||||
{{else}}
|
{{if eq .Status "closed"}}
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm"
|
||||||
hx-put="/events/{{.ID}}/reopen"
|
hx-put="/events/{{.ID}}/reopen"
|
||||||
hx-target="#status-badge-{{.ID}}"
|
hx-target="#event-list"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML"
|
||||||
|
style="font-size: 0.75rem;">
|
||||||
Reopen
|
Reopen
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -131,26 +102,12 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="empty-state">
|
<div class="empty-state" style="text-align: center; padding: 3rem; color: #94a3b8;">
|
||||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
<p>No events yet. Create one to get started.</p>
|
||||||
<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 Events Yet</h3>
|
|
||||||
<p>Create your first event to start tracking expenses.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Toggle hidden class
|
|
||||||
document.querySelector('button[onclick]')?.addEventListener('click', function() {
|
|
||||||
// handled inline
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="theme-color" content="#10b981">
|
<meta name="theme-color" content="#10b981">
|
||||||
<title>{{.Event.Name}} - ExpenseFlow</title>
|
<title>{{.Event.Name}} - ReceiptNext</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="manifest" href="/manifest.json">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
|
@ -13,22 +15,56 @@
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<a href="/dashboard" class="btn btn-secondary btn-sm"
|
<a href="/dashboard" class="btn btn-secondary btn-sm"
|
||||||
hx-get="/dashboard" hx-target="body" hx-push-url="true">
|
hx-get="/dashboard" hx-target="body" hx-push-url="true">← Events</a>
|
||||||
← Back
|
<h1 class="app-title" style="font-size: 1.1rem;">{{.Event.Name}}</h1>
|
||||||
</a>
|
|
||||||
<h1 class="app-title">{{.Event.Name}}</h1>
|
|
||||||
<span class="badge badge-{{.Event.Status}}">{{.Event.Status}}</span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<!-- Capture Receipt Button -->
|
<!-- Receipt List -->
|
||||||
<div style="margin-bottom: 1.5rem;">
|
<div id="expense-list">
|
||||||
<label for="receipt-upload" class="btn btn-primary btn-block" style="display: inline-block; text-align: center; cursor: pointer;">
|
<h3 style="margin-bottom: 1rem; font-size: 1rem; font-weight: 600;">
|
||||||
<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;">
|
Receipts
|
||||||
<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"/>
|
{{if .Event.BaseCurrency}}
|
||||||
<circle cx="12" cy="13" r="4"/>
|
<span style="font-weight: 400; color: #6b7280;">(claim in {{.Event.BaseCurrency}})</span>
|
||||||
</svg>
|
{{end}}
|
||||||
Capture Receipt
|
</h3>
|
||||||
|
|
||||||
|
{{if .Expenses}}
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
{{range .Expenses}}
|
||||||
|
<div style="display: flex; align-items: center; background: white; border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 0.75rem;">
|
||||||
|
<div style="flex: 1; min-width: 0;">
|
||||||
|
<div style="font-weight: 500;">{{.Merchant}}</div>
|
||||||
|
<div style="font-size: 0.75rem; color: #6b7280;">{{.Date}} · {{.Category}} {{if .Description}}· {{.Description}}{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right; margin-right: 0.75rem;">
|
||||||
|
<div style="font-weight: 600;">{{printf "%.2f" .Amount}} {{.Currency}}</div>
|
||||||
|
{{if .ConvertedAmount}}
|
||||||
|
<div style="font-size: 0.75rem; color: #166534;">{{printf "%.2f" .ConvertedAmount}} {{.BaseCurrency}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<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; flex-shrink: 0;"
|
||||||
|
hx-get="/expenses/{{.ID}}/edit"
|
||||||
|
hx-target="#receipt-form"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
title="Edit receipt">✏️</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div style="text-align: center; padding: 2rem; color: #94a3b8; 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 Button -->
|
||||||
|
<div style="margin-top: 1.5rem; text-align: center;">
|
||||||
|
<label for="receipt-upload" class="btn btn-primary btn-block" style="display: block; text-align: center; cursor: pointer;">
|
||||||
|
+ Add Receipt
|
||||||
</label>
|
</label>
|
||||||
<input type="file" id="receipt-upload" name="receipt" accept="image/*" capture="environment" style="display: none;"
|
<input type="file" id="receipt-upload" name="receipt" accept="image/*" capture="environment" style="display: none;"
|
||||||
hx-post="/expenses/upload"
|
hx-post="/expenses/upload"
|
||||||
|
|
@ -43,121 +79,53 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Receipt Form (filled by AI or empty) -->
|
<!-- Submit Event (only if open and has expenses) -->
|
||||||
<div id="receipt-form"></div>
|
{{if and (eq .Event.Status "open") .Expenses}}
|
||||||
|
<div style="margin-top: 2rem; border-top: 1px solid #e2e8f0; padding-top: 1.5rem;">
|
||||||
<!-- Expense List -->
|
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 1rem;">Submit Event</h3>
|
||||||
<div id="expense-list">
|
|
||||||
<h3 style="margin-bottom: 1rem;">
|
|
||||||
Expenses
|
|
||||||
{{if and .Event.BaseCurrency (ne .Event.BaseCurrency "")}}
|
|
||||||
<span style="font-size: 0.875rem; font-weight: 400; color: #6b7280;">
|
|
||||||
(claim in {{.Event.BaseCurrency}})
|
|
||||||
</span>
|
|
||||||
{{end}}
|
|
||||||
</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; 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;"
|
|
||||||
hx-get="/expenses/{{.ID}}/edit"
|
|
||||||
hx-target="#receipt-form"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
title="Edit expense">
|
|
||||||
✏️
|
|
||||||
</button>
|
|
||||||
</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">
|
<form hx-post="/events/{{.Event.ID}}/file" hx-target="body" hx-push-url="true">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="file-email">Send to</label>
|
<label for="file-email">Send claim to</label>
|
||||||
<input type="email" id="file-email" name="email" placeholder="recipient@example.com" required>
|
<input type="email" id="file-email" name="email" placeholder="finance@company.com" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Format</label>
|
<label>Format</label>
|
||||||
<div style="display: flex; gap: 1rem;">
|
<div style="display: flex; gap: 1rem; margin-top: 0.25rem;">
|
||||||
<label class="radio-label">
|
<label style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
<input type="radio" name="format" value="csv" checked> CSV
|
<input type="radio" name="format" value="csv" checked> CSV
|
||||||
</label>
|
</label>
|
||||||
<label class="radio-label">
|
<label style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
<input type="radio" name="format" value="pdf"> PDF
|
<input type="radio" name="format" value="pdf"> PDF
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
<div class="form-group">
|
||||||
<button type="button" class="btn btn-secondary"
|
<label>Total claim</label>
|
||||||
onclick="document.getElementById('file-modal').classList.add('hidden')">Cancel</button>
|
<div style="font-size: 1.25rem; font-weight: 700; color: #166534; margin-top: 0.25rem;">
|
||||||
<button type="submit" class="btn btn-primary">Send Report & Close</button>
|
{{printf "%.2f" .TotalClaim}} {{.Event.BaseCurrency}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block" style="margin-top: 0.5rem;">
|
||||||
|
Submit & Close Event
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Reopen (only if closed) -->
|
||||||
|
{{if eq .Event.Status "closed"}}
|
||||||
|
<div style="margin-top: 2rem; text-align: center;">
|
||||||
|
<button class="btn btn-secondary"
|
||||||
|
hx-put="/events/{{.Event.ID}}/reopen"
|
||||||
|
hx-target="body"
|
||||||
|
hx-push-url="true">
|
||||||
|
Reopen Event
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Close modal on overlay click
|
|
||||||
document.querySelector('.modal-overlay')?.addEventListener('click', function(e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
this.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
<meta name="theme-color" content="#10b981">
|
<meta name="theme-color" content="#10b981">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<title>ExpenseFlow</title>
|
<title>ReceiptNext</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="manifest" href="/manifest.json">
|
||||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -16,12 +18,8 @@
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="card login-card">
|
<div class="card login-card">
|
||||||
<div class="login-brand">
|
<div class="login-brand">
|
||||||
<div class="login-icon">
|
<div class="login-icon"><img src="/static/favicon.svg" width="48" height="48" alt="Rx"></div>
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<h1>ReceiptNext</h1>
|
||||||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1>ExpenseFlow</h1>
|
|
||||||
<p class="text-secondary">AI-Powered Expense Tracking</p>
|
<p class="text-secondary">AI-Powered Expense Tracking</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue