Zero Amount header at the top of budget and month pages #8

Merged
claude-code merged 3 commits from feat/7-zero-amount into main 2026-04-17 12:08:49 -06:00
7 changed files with 128 additions and 12 deletions
Showing only changes of commit 29c6594347 - Show all commits

View file

@ -57,12 +57,27 @@ def _render_target(request: Request, db: Session) -> HTMLResponse:
)
def _render_zero(request: Request, db: Session) -> HTMLResponse:
zero = service.budget_zero(db)
return templates.TemplateResponse(
request,
"partials/budget_zero.html",
{"zero": zero, "tone": service.zero_tone(zero)},
)
def _append_oob(base: HTMLResponse, *extras: HTMLResponse) -> HTMLResponse:
body = base.body.decode() + "".join(e.body.decode() for e in extras)
return HTMLResponse(body)
@router.get("/", response_class=HTMLResponse)
def index(request: Request, db: Session = Depends(get_session)) -> HTMLResponse:
sections = [_section_view(db, s) for s in Section]
target = service.get_debt_target(db)
debt_minimums = service.list_entries(db, Section.debt_minimum)
current_ym = month_service.current_year_month()
zero = service.budget_zero(db)
return templates.TemplateResponse(
request,
"index.html",
@ -72,6 +87,8 @@ def index(request: Request, db: Session = Depends(get_session)) -> HTMLResponse:
"debt_minimums": debt_minimums,
"current_year_month": current_ym,
"all_months": month_service.list_months(db),
"zero": zero,
"tone": service.zero_tone(zero),
},
)
@ -90,10 +107,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)]
if section == Section.debt_minimum:
target_html = _render_target(request, db).body.decode()
response = HTMLResponse(response.body.decode() + target_html)
return response
extras.append(_render_target(request, db))
return _append_oob(response, *extras)
@router.delete("/entries/{entry_id}", response_class=HTMLResponse)
@ -106,10 +123,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)]
if entry.section == Section.debt_minimum:
target_html = _render_target(request, db).body.decode()
response = HTMLResponse(response.body.decode() + target_html)
return response
extras.append(_render_target(request, db))
return _append_oob(response, *extras)
@router.post("/debt-target", response_class=HTMLResponse)

View file

@ -70,6 +70,24 @@ def _render_target(request: Request, db: Session, month: Month) -> HTMLResponse:
)
def _render_zero(request: Request, month: Month) -> HTMLResponse:
zero = month_service.month_zero(month)
return templates.TemplateResponse(
request,
"partials/month_zero.html",
{
"zero": zero,
"planned_tone": service.zero_tone(zero.planned),
"applied_tone": service.zero_tone(zero.applied),
},
)
def _append_oob(base: HTMLResponse, *extras: HTMLResponse) -> HTMLResponse:
body = base.body.decode() + "".join(e.body.decode() for e in extras)
return HTMLResponse(body)
@router.get("", response_class=HTMLResponse)
def view_month(
year_month: str,
@ -92,6 +110,7 @@ def view_month(
"all_months": all_months,
},
)
zero = month_service.month_zero(month)
return templates.TemplateResponse(
request,
"month.html",
@ -107,6 +126,9 @@ def view_month(
(e for e in month.entries if e.section == Section.debt_minimum),
key=lambda e: e.id,
),
"zero": zero,
"planned_tone": service.zero_tone(zero.planned),
"applied_tone": service.zero_tone(zero.applied),
},
)
@ -144,7 +166,10 @@ def add_month_entry(
db, month, section, clean_name, _parse_amount(planned)
)
db.refresh(month)
return _render_section(request, month, section)
return _append_oob(
_render_section(request, month, section),
_render_zero(request, month),
)
@router.delete("/entries/{entry_id}", response_class=HTMLResponse)
@ -159,11 +184,10 @@ def delete_month_entry(
if section is None:
raise HTTPException(status_code=404, detail="entry not found")
db.refresh(month)
response = _render_section(request, month, section)
extras: list[HTMLResponse] = [_render_zero(request, month)]
if section == Section.debt_minimum:
target_html = _render_target(request, db, month).body.decode()
response = HTMLResponse(response.body.decode() + target_html)
return response
extras.append(_render_target(request, db, month))
return _append_oob(_render_section(request, month, section), *extras)
@router.post("/entries/{entry_id}", response_class=HTMLResponse)
@ -195,7 +219,10 @@ def update_month_entry(
if updated is None:
raise HTTPException(status_code=404, detail="entry not found")
db.refresh(month)
return _render_section(request, month, updated.section)
return _append_oob(
_render_section(request, month, updated.section),
_render_zero(request, month),
)
@router.post("/target", response_class=HTMLResponse)

View file

@ -157,6 +157,59 @@ button[type=submit] {
border-bottom-style: dashed;
}
/* Zero Amount widget ----------------------------------------------------- */
:root {
--tone-zero: #2f6b4f;
--tone-positive: #b77b00;
--tone-negative: #a03030;
}
.zero-widget {
margin-top: 1rem;
padding: 1rem 1.25rem;
background: #fff;
border: 1px solid var(--rule);
border-radius: 6px;
text-align: center;
}
.zero-widget .zero-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--muted);
margin-bottom: 0.25rem;
}
.zero-widget .zero-value {
font-size: 2.4rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.zero-widget .zero-sublabel {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.zero-widget-pair .zero-values {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.zero-widget-pair .zero-value {
font-size: 2rem;
}
.tone-zero { color: var(--tone-zero); }
.tone-positive { color: var(--tone-positive); }
.tone-negative { color: var(--tone-negative); }
/* Monthly view ----------------------------------------------------------- */
.month-nav {

View file

@ -18,6 +18,7 @@
</select>
{% endif %}
</nav>
{% include "partials/budget_zero.html" %}
{% for section in sections %}
{% if section.section.value == 'debt_minimum' %}
{% include "partials/section.html" %}

View file

@ -2,6 +2,7 @@
{% block content %}
<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" %}

View file

@ -0,0 +1,4 @@
<section class="zero-widget" id="zero-widget" hx-swap-oob="outerHTML">
<div class="zero-label">Zero Amount</div>
<div class="zero-value tone-{{ tone }}">${{ '%.2f' | format(zero) }}</div>
</section>

View file

@ -0,0 +1,13 @@
<section class="zero-widget zero-widget-pair" id="zero-widget" hx-swap-oob="outerHTML">
<div class="zero-label">Zero Amount</div>
<div class="zero-values">
<div class="zero-cell">
<div class="zero-sublabel">Planned</div>
<div class="zero-value tone-{{ planned_tone }}">${{ '%.2f' | format(zero.planned) }}</div>
</div>
<div class="zero-cell">
<div class="zero-sublabel">Applied</div>
<div class="zero-value tone-{{ applied_tone }}">${{ '%.2f' | format(zero.applied) }}</div>
</div>
</div>
</section>