organized structure
parent
b10ae95690
commit
d4686ddfae
@ -0,0 +1,36 @@
|
||||
function setShare(pct) {
|
||||
const el = document.getElementById('a_share');
|
||||
if (el) el.value = Number(pct).toFixed(4);
|
||||
}
|
||||
|
||||
function onKindChange(kind) {
|
||||
const shareWrap = document.getElementById('share-wrap');
|
||||
const presets = document.getElementById('presets');
|
||||
if (!shareWrap || !presets) return;
|
||||
if (kind === 'transfer') {
|
||||
shareWrap.style.display = 'none';
|
||||
presets.style.display = 'none';
|
||||
} else {
|
||||
shareWrap.style.display = '';
|
||||
presets.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// default your share to 2/3
|
||||
setShare(66.6667);
|
||||
// wire presets
|
||||
const p50 = document.getElementById('p50');
|
||||
const p66 = document.getElementById('p66');
|
||||
const p33 = document.getElementById('p33');
|
||||
if (p50) p50.addEventListener('click', () => setShare(50));
|
||||
if (p66) p66.addEventListener('click', () => setShare(66.6667));
|
||||
if (p33) p33.addEventListener('click', () => setShare(33.3333));
|
||||
|
||||
// wire kind radios
|
||||
const bill = document.getElementById('k_bill');
|
||||
const xfer = document.getElementById('k_xfer');
|
||||
if (bill) bill.addEventListener('change', () => onKindChange('bill'));
|
||||
if (xfer) xfer.addEventListener('change', () => onKindChange('transfer'));
|
||||
onKindChange((xfer && xfer.checked) ? 'transfer' : 'bill');
|
||||
});
|
||||
@ -0,0 +1,45 @@
|
||||
:root{ --bg:#0f1115; --card:#141822; --muted:#8c93a6; --ok:#19c37d; --bad:#ef4444; --fg:#e6e7eb; --edge:#202637;}
|
||||
*{ box-sizing:border-box }
|
||||
body{ margin:0; font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; background:var(--bg); color:var(--fg) }
|
||||
.wrap{ max-width:980px; margin:24px auto; padding:0 16px }
|
||||
header{ display:flex; gap:12px; align-items:center; justify-content:space-between; margin-bottom:16px }
|
||||
.h1{ font-size:24px; font-weight:700 }
|
||||
.pill{ display:inline-flex; gap:8px; padding:8px 12px; border-radius:999px; background:var(--card); border:1px solid var(--edge); font-size:14px }
|
||||
.pill.ok{ background:rgba(25,195,125,.12); border-color:#1f7a58 }
|
||||
.pill.bad{ background:rgba(239,68,68,.12); border-color:#7a1f1f }
|
||||
.muted{ color:var(--muted) }
|
||||
|
||||
.grid{ display:grid; grid-template-columns:1.2fr .8fr; gap:16px }
|
||||
@media (max-width:900px){ .grid{ grid-template-columns:1fr } }
|
||||
|
||||
.card{ background:var(--card); border:1px solid var(--edge); border-radius:14px; padding:16px }
|
||||
h2{ font-size:16px; margin:0 0 10px }
|
||||
|
||||
form .row{ display:flex; gap:10px; flex-wrap:wrap }
|
||||
input, select{ background:#0d1117; border:1px solid #263041; color:var(--fg); border-radius:10px; padding:10px; outline:none }
|
||||
input[type="number"]{ max-width:160px }
|
||||
input[type="text"]{ flex:1 }
|
||||
|
||||
.seg{ display:inline-flex; border:1px solid #263041; border-radius:10px; overflow:hidden }
|
||||
.seg input{ display:none }
|
||||
.seg label{ padding:8px 10px; cursor:pointer; background:#0d1117; user-select:none }
|
||||
.seg input:checked + label{ background:#182033; color:#b6d1ff }
|
||||
|
||||
.toggle{ display:inline-flex; border:1px solid #263041; border-radius:10px; overflow:hidden }
|
||||
.toggle input{ display:none }
|
||||
.toggle label{ padding:8px 12px; cursor:pointer; background:#0d1117; user-select:none; min-width:100px; text-align:center }
|
||||
.toggle input:checked + label{ background:#183033; color:#b6d1ff }
|
||||
|
||||
button.btn{ background:#1f6fe8; border:none; color:#fff; padding:10px 14px; border-radius:10px; cursor:pointer }
|
||||
button.btn.secondary{ background:#273244 }
|
||||
button.btn.danger{ background:#c92a2a }
|
||||
|
||||
table{ width:100%; border-collapse:collapse }
|
||||
th, td{ border-bottom:1px solid #222b3b; padding:10px; text-align:left; font-size:14px }
|
||||
th{ color:#aab3c4; font-weight:600 }
|
||||
td.num{ font-variant-numeric:tabular-nums; text-align:right }
|
||||
|
||||
.pos{ color:#ef9a9a } /* you owe */
|
||||
.neg{ color:#9ae6b4 } /* they owe */
|
||||
|
||||
.tag{ font-size:12px; padding:2px 8px; border-radius:999px; background:#1a2332; border:1px solid #243046 }
|
||||
@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SplitBuddy</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script defer src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="h1">SplitBuddy</div>
|
||||
<div>
|
||||
<span class="pill {{ 'bad' if summary.total>0 else ('ok' if summary.total<0 else '') }}">
|
||||
{% if summary.total > 0 %}
|
||||
{{ A }} owes {{ B }} <strong>{{ currency }}{{ '%.2f'|format(summary.total) }}</strong>
|
||||
{% elif summary.total < 0 %}
|
||||
{{ B }} owes {{ A }} <strong>{{ currency }}{{ '%.2f'|format(-summary.total) }}</strong>
|
||||
{% else %}All settled ✨{% endif %}
|
||||
</span>
|
||||
<span class="pill muted">Balance: {{ currency }}{{ '%.2f'|format(summary.total) }}</span>
|
||||
<a class="pill" href="{{ url_for('export_csv') }}">Export CSV</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Add entry</h2>
|
||||
<form method="post" action="{{ url_for('add') }}">
|
||||
<div class="row">
|
||||
<div class="seg" title="Entry type">
|
||||
<input type="radio" id="k_bill" name="kind" value="bill" checked><label for="k_bill">Bill</label>
|
||||
<input type="radio" id="k_xfer" name="kind" value="transfer"><label for="k_xfer">Transfer</label>
|
||||
</div>
|
||||
<div class="toggle" title="Who paid / who sent">
|
||||
<input type="radio" id="payer_a" name="payer" value="A" checked><label for="payer_a">{{ A }}</label>
|
||||
<input type="radio" id="payer_b" name="payer" value="B"><label for="payer_b">{{ B }}</label>
|
||||
</div>
|
||||
<input type="number" step="0.01" min="0" name="total" placeholder="Amount" required>
|
||||
<select name="method">
|
||||
<option>cash</option><option>transfer</option><option>other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row" id="share-wrap" style="margin-top:8px">
|
||||
<input id="a_share" type="number" step="0.01" min="0" max="100" name="a_share_pct"
|
||||
placeholder="{{ A }} share %" value="{{ '%.4f'|format(default_a_share_pct) }}"
|
||||
title="{{ A }}'s share (%)">
|
||||
<input type="text" name="note" placeholder="Reason (e.g. rent, groceries, washer)" style="flex:1">
|
||||
<input type="datetime-local" name="created_at" value="{{ now_local }}">
|
||||
<button class="btn" type="submit">Add</button>
|
||||
</div>
|
||||
|
||||
<div class="row" id="presets" style="margin-top:8px">
|
||||
<div class="seg" title="Quick presets">
|
||||
<input type="radio" id="p50" name="preset"><label for="p50">50/50</label>
|
||||
<input type="radio" id="p66" name="preset" checked><label for="p66">{{ A }} 2/3</label>
|
||||
<input type="radio" id="p33" name="preset"><label for="p33">{{ A }} 1/3</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Stats</h2>
|
||||
<div class="muted">Entries: {{ summary.count }}</div>
|
||||
<div class="muted">Latest: {{ summary.latest or '—' }}</div>
|
||||
<div style="margin-top:8px">
|
||||
<span class="tag">Cash Δ: {{ currency }}{{ '%.2f'|format(summary.by_method.cash) }}</span>
|
||||
<span class="tag">Transfer Δ: {{ currency }}{{ '%.2f'|format(summary.by_method.transfer) }}</span>
|
||||
<span class="tag">Other Δ: {{ currency }}{{ '%.2f'|format(summary.by_method.other) }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card" style="margin-top:16px">
|
||||
<h2>Ledger</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:160px">Time</th>
|
||||
<th>Type</th>
|
||||
<th>Payer/Sender</th>
|
||||
<th>Reason</th>
|
||||
<th>Method</th>
|
||||
<th class="num">Your share %</th>
|
||||
<th class="num">Amount</th>
|
||||
<th class="num">Δ Balance</th>
|
||||
<th style="width:120px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
<tr>
|
||||
<td>{{ e.created_at }}</td>
|
||||
<td>{{ e.kind }}</td>
|
||||
<td>{{ A if e.payer=='A' else B }}</td>
|
||||
<td>{{ e.note or '—' }}</td>
|
||||
<td>{{ e.method }}</td>
|
||||
<td class="num">{{ e.kind == 'bill' and ('%.2f'|format(e.a_share*100) ~ '%') or '—' }}</td>
|
||||
<td class="num">{{ currency }}{{ '%.2f'|format(e.total) }}</td>
|
||||
<td class="num {{ 'pos' if e.delta>0 else 'neg' if e.delta<0 else '' }}">
|
||||
{{ currency }}{{ '%.2f'|format(e.delta) }}
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('delete', entry_id=e.id) }}" onsubmit="return confirm('Delete this entry?');">
|
||||
<button class="btn danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue