Section groups with collapsible headers + Sinking Funds #12
7 changed files with 139 additions and 20 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -19,13 +19,20 @@
|
|||
{% endif %}
|
||||
</nav>
|
||||
{% 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 %}
|
||||
<details class="group" id="group-{{ g.group.value }}"{% if g.default_open %} open{% endif %}>
|
||||
<summary class="group-header">
|
||||
<span class="chevron" aria-hidden="true"></span>
|
||||
<span class="group-name">{{ g.label }}</span>
|
||||
<span class="group-total" id="group-total-{{ g.group.value }}">${{ '%.2f' | format(g.total) }}</span>
|
||||
</summary>
|
||||
{% 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 %}
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,24 @@
|
|||
<div class="budget">
|
||||
{% include "partials/month_nav.html" %}
|
||||
{% include "partials/month_zero.html" %}
|
||||
{% for section in sections %}
|
||||
{% if section.section.value == 'debt_minimum' %}
|
||||
{% include "partials/month_section.html" %}
|
||||
{% include "partials/month_target.html" %}
|
||||
{% else %}
|
||||
{% include "partials/month_section.html" %}
|
||||
{% endif %}
|
||||
{% for g in groups %}
|
||||
<details class="group" id="group-{{ g.group.value }}"{% if g.default_open %} open{% endif %}>
|
||||
<summary class="group-header">
|
||||
<span class="chevron" aria-hidden="true"></span>
|
||||
<span class="group-name">{{ g.label }}</span>
|
||||
<span class="group-total" id="group-total-{{ g.group.value }}">
|
||||
<span class="applied">${{ '%.2f' | format(g.total_applied) }}</span>
|
||||
<span class="divider">/</span>
|
||||
<span class="planned">${{ '%.2f' | format(g.total_planned) }}</span>
|
||||
</span>
|
||||
</summary>
|
||||
{% for section in g.sections %}
|
||||
{% include "partials/month_section.html" %}
|
||||
{% if g.group.value == 'committed' and section.section.value == 'debt_minimum' %}
|
||||
{% include "partials/month_target.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{% for g in groups -%}
|
||||
<span class="group-total" id="group-total-{{ g.group.value }}" hx-swap-oob="outerHTML">${{ '%.2f' | format(g.total) }}</span>
|
||||
{% endfor -%}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{% for g in groups -%}
|
||||
<span class="group-total" id="group-total-{{ g.group.value }}" hx-swap-oob="outerHTML">
|
||||
<span class="applied">${{ '%.2f' | format(g.total_applied) }}</span>
|
||||
<span class="divider">/</span>
|
||||
<span class="planned">${{ '%.2f' | format(g.total_planned) }}</span>
|
||||
</span>
|
||||
{% endfor -%}
|
||||
Loading…
Reference in a new issue