diff --git a/src/quartermaster/routes.py b/src/quartermaster/routes.py
index dc65812..926eb60 100644
--- a/src/quartermaster/routes.py
+++ b/src/quartermaster/routes.py
@@ -66,6 +66,14 @@ def _render_zero(request: Request, db: Session) -> HTMLResponse:
)
+def _render_group_totals(request: Request, db: Session) -> HTMLResponse:
+ return templates.TemplateResponse(
+ request,
+ "partials/budget_group_totals.html",
+ {"groups": service.budget_group_views(db)},
+ )
+
+
def _append_oob(base: HTMLResponse, *extras: HTMLResponse) -> HTMLResponse:
body = base.body.decode() + "".join(e.body.decode() for e in extras)
return HTMLResponse(body)
@@ -73,7 +81,7 @@ def _append_oob(base: HTMLResponse, *extras: HTMLResponse) -> HTMLResponse:
@router.get("/", response_class=HTMLResponse)
def index(request: Request, db: Session = Depends(get_session)) -> HTMLResponse:
- sections = [_section_view(db, s) for s in Section]
+ groups = service.budget_group_views(db)
target = service.get_debt_target(db)
debt_minimums = service.list_entries(db, Section.debt_minimum)
current_ym = month_service.current_year_month()
@@ -82,7 +90,7 @@ def index(request: Request, db: Session = Depends(get_session)) -> HTMLResponse:
request,
"index.html",
{
- "sections": sections,
+ "groups": groups,
"target": target,
"debt_minimums": debt_minimums,
"current_year_month": current_ym,
@@ -107,7 +115,10 @@ def create_entry(
parsed = _parse_amount(amount)
service.add_entry(db, section, clean_name, parsed)
response = _render_section(request, db, section)
- extras: list[HTMLResponse] = [_render_zero(request, db)]
+ extras: list[HTMLResponse] = [
+ _render_zero(request, db),
+ _render_group_totals(request, db),
+ ]
if section == Section.debt_minimum:
extras.append(_render_target(request, db))
return _append_oob(response, *extras)
@@ -123,7 +134,10 @@ def remove_entry(
if entry is None:
raise HTTPException(status_code=404, detail="entry not found")
response = _render_section(request, db, entry.section)
- extras: list[HTMLResponse] = [_render_zero(request, db)]
+ extras: list[HTMLResponse] = [
+ _render_zero(request, db),
+ _render_group_totals(request, db),
+ ]
if entry.section == Section.debt_minimum:
extras.append(_render_target(request, db))
return _append_oob(response, *extras)
diff --git a/src/quartermaster/routes_month.py b/src/quartermaster/routes_month.py
index 397501a..8faa748 100644
--- a/src/quartermaster/routes_month.py
+++ b/src/quartermaster/routes_month.py
@@ -83,6 +83,14 @@ def _render_zero(request: Request, month: Month) -> HTMLResponse:
)
+def _render_group_totals(request: Request, month: Month) -> HTMLResponse:
+ return templates.TemplateResponse(
+ request,
+ "partials/month_group_totals.html",
+ {"groups": month_service.month_group_views(month)},
+ )
+
+
def _append_oob(base: HTMLResponse, *extras: HTMLResponse) -> HTMLResponse:
body = base.body.decode() + "".join(e.body.decode() for e in extras)
return HTMLResponse(body)
@@ -120,7 +128,7 @@ def view_month(
"prev_year_month": prev_ym,
"next_year_month": next_ym,
"all_months": all_months,
- "sections": _section_views(month),
+ "groups": month_service.month_group_views(month),
"target": month_service.get_month_target(db, month),
"debt_minimums": sorted(
(e for e in month.entries if e.section == Section.debt_minimum),
@@ -169,6 +177,7 @@ def add_month_entry(
return _append_oob(
_render_section(request, month, section),
_render_zero(request, month),
+ _render_group_totals(request, month),
)
@@ -184,7 +193,10 @@ def delete_month_entry(
if section is None:
raise HTTPException(status_code=404, detail="entry not found")
db.refresh(month)
- extras: list[HTMLResponse] = [_render_zero(request, month)]
+ extras: list[HTMLResponse] = [
+ _render_zero(request, month),
+ _render_group_totals(request, month),
+ ]
if section == Section.debt_minimum:
extras.append(_render_target(request, db, month))
return _append_oob(_render_section(request, month, section), *extras)
@@ -222,6 +234,7 @@ def update_month_entry(
return _append_oob(
_render_section(request, month, updated.section),
_render_zero(request, month),
+ _render_group_totals(request, month),
)
diff --git a/src/quartermaster/static/app.css b/src/quartermaster/static/app.css
index 216aa9a..8a9fb97 100644
--- a/src/quartermaster/static/app.css
+++ b/src/quartermaster/static/app.css
@@ -157,6 +157,70 @@ button[type=submit] {
border-bottom-style: dashed;
}
+/* Section groups --------------------------------------------------------- */
+
+details.group {
+ margin-top: 1rem;
+ border-top: 2px solid var(--ink);
+}
+
+details.group:last-of-type {
+ border-bottom: 2px solid var(--ink);
+}
+
+details.group > summary {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ padding: 0.6rem 0.5rem;
+ cursor: pointer;
+ list-style: none;
+ user-select: none;
+}
+
+details.group > summary::-webkit-details-marker { display: none; }
+
+details.group > summary .chevron {
+ display: inline-block;
+ width: 0.8rem;
+ transition: transform 0.12s ease-out;
+ color: var(--muted);
+}
+
+details.group > summary .chevron::before {
+ content: "\25B6"; /* black right-pointing triangle */
+ font-size: 0.7rem;
+}
+
+details.group[open] > summary .chevron {
+ transform: rotate(90deg);
+}
+
+details.group > summary .group-name {
+ flex: 1;
+ font-size: 1.05rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+details.group > summary .group-total {
+ font-variant-numeric: tabular-nums;
+ font-weight: 700;
+ color: var(--accent);
+ font-size: 1.05rem;
+}
+
+details.group > .section {
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+ padding-left: 1rem;
+}
+
+details.group > .section:last-child {
+ margin-bottom: 0.75rem;
+}
+
/* Zero Amount widget ----------------------------------------------------- */
:root {
diff --git a/src/quartermaster/templates/index.html b/src/quartermaster/templates/index.html
index 71719f3..04e85e4 100644
--- a/src/quartermaster/templates/index.html
+++ b/src/quartermaster/templates/index.html
@@ -19,13 +19,20 @@
{% endif %}
{% include "partials/budget_zero.html" %}
- {% for section in sections %}
- {% if section.section.value == 'debt_minimum' %}
- {% include "partials/section.html" %}
- {% include "partials/target_card.html" %}
- {% else %}
- {% include "partials/section.html" %}
- {% endif %}
+ {% for g in groups %}
+
+
+ {{ g.label }}
+ ${{ '%.2f' | format(g.total) }}
+
+ {% for section in g.sections %}
+ {% include "partials/section.html" %}
+ {% if g.group.value == 'committed' and section.section.value == 'debt_minimum' %}
+ {% include "partials/target_card.html" %}
+ {% endif %}
+ {% endfor %}
+