organized structure

main
oscar 2 months ago
parent b10ae95690
commit d4686ddfae

@ -1,17 +1,18 @@
# SplitBuddy — Flask app with bills/transfer + payer toggle
from __future__ import annotations from __future__ import annotations
import os, sqlite3, csv, io, datetime as dt import os, sqlite3, csv, io, datetime as dt
from typing import Optional from typing import Optional
from flask import Flask, g, request, redirect, url_for, render_template_string, send_file from flask import Flask, g, request, redirect, url_for, render_template, send_file
app = Flask(__name__) app = Flask(__name__)
# ----- Config -----
DB_PATH = os.environ.get("SPLITBUDDY_DB", "splitbuddy.db") DB_PATH = os.environ.get("SPLITBUDDY_DB", "splitbuddy.db")
CURRENCY = "" CURRENCY = ""
PERSON_A = os.environ.get("SPLITBUDDY_ME", "Me") # you PERSON_A = os.environ.get("SPLITBUDDY_ME", "Me") # you
PERSON_B = os.environ.get("SPLITBUDDY_ROOMIE", "Idan") # roommate PERSON_B = os.environ.get("SPLITBUDDY_ROOMIE", "Idan") # roommate
DEFAULT_A_SHARE_PCT = 66.6667 # <-- you pay 2/3 by default
# ------------------------- DB helpers --------------------------- # # ----- DB helpers -----
def get_db() -> sqlite3.Connection: def get_db() -> sqlite3.Connection:
if "db" not in g: if "db" not in g:
g.db = sqlite3.connect(DB_PATH) g.db = sqlite3.connect(DB_PATH)
@ -31,197 +32,34 @@ def init_db():
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'bill', -- 'bill' | 'transfer' kind TEXT NOT NULL DEFAULT 'bill', -- 'bill' | 'transfer'
total REAL NOT NULL DEFAULT 0, -- total amount total REAL NOT NULL DEFAULT 0, -- amount
payer TEXT NOT NULL DEFAULT 'A', -- 'A' (you) or 'B' (Idan) payer TEXT NOT NULL DEFAULT 'A', -- 'A' (you) or 'B' (Idan)
a_share REAL NOT NULL DEFAULT 0.5, -- your share (0..1), only for bills a_share REAL NOT NULL DEFAULT 0.666667, -- your share (0..1) for bills
method TEXT NOT NULL DEFAULT 'cash', method TEXT NOT NULL DEFAULT 'cash',
note TEXT note TEXT
) )
""") """)
# Migrations for older versions # Migrations (no-op if columns already exist)
for col, ddl in [ for ddl in [
("kind", "ALTER TABLE entries ADD COLUMN kind TEXT NOT NULL DEFAULT 'bill'"), "ALTER TABLE entries ADD COLUMN kind TEXT NOT NULL DEFAULT 'bill'",
("total", "ALTER TABLE entries ADD COLUMN total REAL"), "ALTER TABLE entries ADD COLUMN total REAL",
("payer", "ALTER TABLE entries ADD COLUMN payer TEXT"), "ALTER TABLE entries ADD COLUMN payer TEXT",
("a_share","ALTER TABLE entries ADD COLUMN a_share REAL"), "ALTER TABLE entries ADD COLUMN a_share REAL",
]: ]:
try: try:
db.execute(ddl) db.execute(ddl)
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
# Normalize NULLs from legacy rows
db.execute(""" db.execute("""
UPDATE entries UPDATE entries
SET kind = COALESCE(kind, 'bill'), SET kind = COALESCE(kind, 'bill'),
total = COALESCE(total, 0), total = COALESCE(total, 0),
payer = COALESCE(payer, 'A'), payer = COALESCE(payer, 'A'),
a_share= COALESCE(a_share, 0.5) a_share= COALESCE(a_share, 0.666667)
""") """)
db.commit() db.commit()
# --------------------------- Template --------------------------- # # ----- Utility & models -----
BASE = r"""
<!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">
<style>
: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; 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 } td.num{ font-variant-numeric:tabular-nums; text-align:right }
.pos{ color:#ef9a9a } .neg{ color:#9ae6b4 }
.tag{ font-size:12px; padding:2px 8px; border-radius:999px; background:#1a2332; border:1px solid #243046 }
</style>
<script>
function setShare(pct) {
const el = document.getElementById('a_share');
el.value = pct.toFixed(2);
}
function onKindChange(kind) {
const shareWrap = document.getElementById('share-wrap');
const presets = document.getElementById('presets');
if (kind === 'transfer') {
shareWrap.style.display = 'none';
presets.style.display = 'none';
} else {
shareWrap.style.display = '';
presets.style.display = '';
}
}
document.addEventListener('DOMContentLoaded', () => {
const kindRadios = document.querySelectorAll('input[name="kind"]');
kindRadios.forEach(r => r.addEventListener('change', () => onKindChange(r.value)));
onKindChange(document.querySelector('input[name="kind"]:checked').value);
});
</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="50.00" 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" onclick="setShare(50)" checked><label for="p50">50/50</label>
<input type="radio" id="p66" name="preset" onclick="setShare(66.6667)"><label for="p66">{{ A }} 2/3</label>
<input type="radio" id="p33" name="preset" onclick="setShare(33.3333)"><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>
"""
# --------------------------- Utilities -------------------------- #
def _now_local_iso_min() -> str: def _now_local_iso_min() -> str:
return dt.datetime.now().replace(second=0, microsecond=0).isoformat(timespec="minutes") return dt.datetime.now().replace(second=0, microsecond=0).isoformat(timespec="minutes")
@ -236,16 +74,15 @@ class Summary:
def _delta_for_entry(kind: str, total: float, payer: str, a_share: float) -> float: def _delta_for_entry(kind: str, total: float, payer: str, a_share: float) -> float:
""" """
Positive => YOU owe Idan. Negative => Idan owes YOU. Positive => YOU owe Idan. Negative => Idan owes YOU.
- bill: delta = (your_share * total) - (total if YOU paid else 0) - bill: delta = (your_share * total) - (total if YOU paid else 0)
- transfer: delta = -total if YOU sent; +total if Idan sent - transfer: delta = -total if YOU sent; +total if Idan sent
""" """
if kind == "transfer": if kind == "transfer":
return -total if payer == "A" else total return -total if payer == "A" else total
# bill
paid_by_a = total if payer == "A" else 0.0 paid_by_a = total if payer == "A" else 0.0
return a_share * total - paid_by_a return a_share * total - paid_by_a
# ----------------------------- Routes --------------------------- # # ----- Routes -----
@app.before_request @app.before_request
def _ensure_db(): def _ensure_db():
init_db() init_db()
@ -258,6 +95,7 @@ def index():
FROM entries FROM entries
ORDER BY datetime(created_at) DESC, id DESC ORDER BY datetime(created_at) DESC, id DESC
""").fetchall() """).fetchall()
entries = [] entries = []
for r in rows: for r in rows:
e = dict(r) e = dict(r)
@ -273,13 +111,14 @@ def index():
) )
summary = Summary(total=total_balance, count=len(entries), latest=latest, by_method=bm) summary = Summary(total=total_balance, count=len(entries), latest=latest, by_method=bm)
return render_template_string( return render_template(
BASE, "index.html",
entries=entries, entries=entries,
summary=summary, summary=summary,
A=PERSON_A, B=PERSON_B, A=PERSON_A, B=PERSON_B,
currency=CURRENCY, currency=CURRENCY,
now_local=_now_local_iso_min(), now_local=_now_local_iso_min(),
default_a_share_pct=DEFAULT_A_SHARE_PCT,
) )
@app.post("/add") @app.post("/add")
@ -296,10 +135,12 @@ def add():
if payer not in ("A","B"): payer = "A" if payer not in ("A","B"): payer = "A"
if kind not in ("bill","transfer"): kind = "bill" if kind not in ("bill","transfer"): kind = "bill"
a_share = 0.5
if kind == "bill": if kind == "bill":
if a_share_pct is None: a_share_pct = 50.0 if a_share_pct is None:
a_share_pct = DEFAULT_A_SHARE_PCT
a_share = max(0.0, min(100.0, a_share_pct)) / 100.0 a_share = max(0.0, min(100.0, a_share_pct)) / 100.0
else:
a_share = DEFAULT_A_SHARE_PCT / 100.0 # stored but ignored for transfers
db = get_db() db = get_db()
db.execute( db.execute(
@ -336,9 +177,8 @@ def export_csv():
return send_file(io.BytesIO(buff.read().encode("utf-8")), mimetype="text/csv", return send_file(io.BytesIO(buff.read().encode("utf-8")), mimetype="text/csv",
as_attachment=True, download_name="splitbuddy_export.csv") as_attachment=True, download_name="splitbuddy_export.csv")
# --------------------------- Entrypoint -------------------------- #
if __name__ == "__main__": if __name__ == "__main__":
os.makedirs(os.path.dirname(DB_PATH) or ".", exist_ok=True) os.makedirs(os.path.dirname(DB_PATH) or ".", exist_ok=True)
with app.app_context(): with app.app_context():
init_db() init_db()
app.run(debug=True) app.run(debug=True)

@ -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…
Cancel
Save