From cca05fe9fc17e768e5afad26a5ee11cdd076409f Mon Sep 17 00:00:00 2001 From: archeious Date: Fri, 17 Apr 2026 17:34:48 -0600 Subject: [PATCH] feat(ledger): expandable entry rows with transactions table and add form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each month entry becomes a
block. The summary is the same dense row (name, planned, applied, delete) plus a leading caret and an applied cell that shows the transaction count ("$412.33 · 7 txns") when postings exist. Expansion adds no horizontal space. Expanded body holds: the entry's notes input, a transactions table with date / description / payee / amount / delete per posting, and an inline add-transaction form (date, description, payee, amount, submit). Every field is HTMX-wired so editing any cell triggers the section partial re-render with fresh derived totals. Closed month: name / planned / notes / posting fields all collapse to read-only spans, delete buttons and add forms are omitted. The existing editable flag controls the branching. Refs #19 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/quartermaster/static/app.css | 289 ++++++++++++++++++ .../templates/partials/month_section.html | 259 +++++++++++----- 2 files changed, 469 insertions(+), 79 deletions(-) diff --git a/src/quartermaster/static/app.css b/src/quartermaster/static/app.css index 2574f91..1a4dd69 100644 --- a/src/quartermaster/static/app.css +++ b/src/quartermaster/static/app.css @@ -675,6 +675,295 @@ tr.entry:hover + tr.entry-notes-row:has(input:placeholder-shown) { padding: 0.15rem 0.4rem; } +/* =============== MONTH ENTRIES — details-based layout =============== */ + +.month-entries { + display: flex; + flex-direction: column; +} + +.entry-block { + border-bottom: 1px dotted var(--rule); + margin: 0; +} + +.entry-block > summary { + list-style: none; + cursor: pointer; + user-select: none; + display: grid; + grid-template-columns: 0.9rem minmax(0, 1fr) 5.5rem 5.5rem 1.2rem; + gap: 0.6rem; + align-items: baseline; + padding: 0.32rem 0.25rem 0.36rem; + position: relative; +} +.entry-block > summary::-webkit-details-marker { display: none; } +.entry-block > summary:hover { background: var(--paper-stripe); } + +/* Progress bar on the summary row */ +.entry-block > summary::after { + content: ""; + position: absolute; + left: 0; right: 0; bottom: -1px; + height: 2px; + background: var(--sage-soft); + width: min(100%, calc(var(--ratio, 1) * 100%)); + transition: width 0.25s ease; + opacity: 0.7; +} +.entry-block > summary.state-edited::after { background: var(--accent); opacity: 0.85; } +.entry-block > summary.state-new_in_month::after { background: var(--indigo); opacity: 0.55; } + +/* Caret: rotates on [open] */ +.entry-block .caret { + display: inline-block; + width: 0.7rem; + position: relative; + transform: rotate(0deg); + transition: transform 0.15s ease; + align-self: center; +} +.entry-block .caret::before { + content: "▸"; + font-size: 0.7rem; + color: var(--muted); + display: inline-block; + line-height: 1; +} +.entry-block[open] > summary .caret { transform: rotate(90deg); } + +/* Entry row cells */ +.entry-block .entry-name { + font-weight: 500; + font-size: 1rem; + min-width: 0; + overflow: hidden; +} +.entry-block .entry-name input, +.entry-block .entry-amount input { + font: inherit; + color: inherit; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + padding: 0; + width: 100%; + outline: none; + transition: border-color 0.12s; +} +.entry-block .entry-name input:hover, +.entry-block .entry-amount input:hover { border-bottom-color: var(--rule); } +.entry-block .entry-name input:focus, +.entry-block .entry-amount input:focus { border-bottom-color: var(--ink); } + +.entry-block .entry-amount { + font-weight: 500; + font-size: 1rem; + text-align: right; + font-feature-settings: "lnum" 1, "tnum" 1; + min-width: 0; +} +.entry-block .entry-amount input { + text-align: right; + font-variant-numeric: tabular-nums; +} +.entry-block .entry-amount.planned input { color: var(--muted); font-weight: 400; } + +.entry-block .applied-cell { + display: flex; + justify-content: flex-end; + align-items: baseline; + gap: 0.3rem; +} +.entry-block .applied-cell .value { + font-weight: 500; +} +.entry-block .applied-cell .count { + font-family: var(--sans); + font-size: 0.66rem; + letter-spacing: 0.16em; + text-transform: lowercase; + color: var(--muted); +} + +.entry-block .entry-actions button.delete { + font-size: 1.1rem; + line-height: 1; + color: var(--rule); + background: none; + border: none; + cursor: pointer; + padding: 0; + opacity: 0; + transition: color 0.12s ease, opacity 0.12s ease; + align-self: center; +} +.entry-block > summary:hover .entry-actions button.delete { opacity: 1; } +.entry-block .entry-actions button.delete:hover { color: var(--accent); } + +/* Expanded body */ +.entry-block .entry-body { + padding: 0.5rem 1.6rem 0.75rem; + background: var(--paper-soft); + border-top: 1px solid var(--rule-soft); +} + +.entry-block .entry-notes { + margin-bottom: 0.5rem; +} +.entry-block .entry-notes input { + font-family: var(--sans); + font-style: italic; + font-size: 0.85rem; + color: var(--ink); + width: 100%; + background: transparent; + border: none; + border-bottom: 1px dashed var(--rule); + padding: 0.15rem 0; + outline: none; +} +.entry-block .entry-notes input:focus { border-bottom-color: var(--ink); } +.entry-block .entry-notes.readonly { + font-family: var(--sans); + font-style: italic; + font-size: 0.85rem; + color: var(--muted); +} + +/* Transactions table */ +table.transactions { + width: 100%; + border-collapse: collapse; + font-family: var(--sans); + font-size: 0.88rem; + margin-bottom: 0.4rem; +} +table.transactions thead th { + text-align: left; + font-weight: 600; + font-size: 0.66rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--muted); + padding: 0.2rem 0.3rem 0.25rem; + border-bottom: 1px solid var(--rule); +} +table.transactions th.col-amount, +table.transactions td.col-amount { text-align: right; width: 5.5rem; } +table.transactions th.col-date, +table.transactions td.col-date { width: 7.5rem; } +table.transactions th.col-actions, +table.transactions td.col-actions { width: 1.2rem; } + +table.transactions td { + padding: 0.2rem 0.3rem; + border-bottom: 1px dotted var(--rule-soft); +} +table.transactions tr.posting:hover { background: var(--paper); } +table.transactions input { + font: inherit; + color: inherit; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + padding: 0; + width: 100%; + outline: none; + transition: border-color 0.12s; +} +table.transactions input:hover { border-bottom-color: var(--rule); } +table.transactions input:focus { border-bottom-color: var(--ink); } +table.transactions input[type="number"] { + text-align: right; + font-variant-numeric: tabular-nums; +} +table.transactions input[type="date"] { + font-family: var(--sans); + color: var(--muted); +} +table.transactions tr.empty td { + color: var(--muted); + font-style: italic; + text-align: center; + padding: 0.5rem; +} + +table.transactions td .readonly { + color: var(--ink); +} + +table.transactions button.delete { + font-size: 1rem; + color: var(--rule); + background: none; + border: none; + cursor: pointer; + padding: 0; + opacity: 0; + transition: color 0.12s ease, opacity 0.12s ease; +} +table.transactions tr:hover button.delete { opacity: 1; } +table.transactions button.delete:hover { color: var(--accent); } + +/* Add-posting form */ +form.add-posting-form { + display: grid; + grid-template-columns: 7.5rem minmax(0, 1fr) minmax(0, 1fr) 5.5rem auto; + gap: 0.4rem; + align-items: center; + padding: 0.35rem 0.3rem 0.2rem; + border-top: 1px dashed var(--rule); +} +form.add-posting-form input { + font: inherit; + font-size: 0.88rem; + padding: 0.2rem 0.4rem; + border: 1px solid var(--rule); + background: var(--paper); + color: var(--ink); + outline: none; + transition: border-color 0.12s; +} +form.add-posting-form input[type="number"] { text-align: right; font-variant-numeric: tabular-nums; } +form.add-posting-form input:focus { border-color: var(--ink); } +form.add-posting-form button[type="submit"] { + font-family: var(--sans); + font-weight: 600; + font-size: 0.72rem; + padding: 0.25rem 0.75rem; + border: 1px solid var(--ink); + background: var(--paper-soft); + color: var(--ink); + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.12s ease, color 0.12s ease; +} +form.add-posting-form button[type="submit"]:hover { background: var(--ink); color: var(--paper); } + +.empty-row { + padding: 0.5rem 0.5rem; + color: var(--muted); + font-style: italic; + font-size: 0.9rem; +} + +@media (max-width: 640px) { + .entry-block > summary { + grid-template-columns: 0.9rem minmax(0, 1fr) 4.6rem 4.6rem 1rem; + gap: 0.4rem; + } + .entry-block .entry-body { padding: 0.5rem 0.75rem 0.6rem; } + form.add-posting-form { + grid-template-columns: 1fr 1fr; + } + form.add-posting-form input[type="date"], + form.add-posting-form input[type="number"], + form.add-posting-form button[type="submit"] { grid-column: 1 / -1; } +} + /* Disabled inputs (closed month) */ input[disabled], select[disabled], diff --git a/src/quartermaster/templates/partials/month_section.html b/src/quartermaster/templates/partials/month_section.html index 8868b2c..757ab49 100644 --- a/src/quartermaster/templates/partials/month_section.html +++ b/src/quartermaster/templates/partials/month_section.html @@ -2,68 +2,61 @@

{{ section.label }}

- ${{ '%.2f' | format(section.total_applied) }} + ${{ '{:,.2f}'.format(section.total_applied) }} / - ${{ '%.2f' | format(section.total_planned) }} + ${{ '{:,.2f}'.format(section.total_planned) }}
- - - - - - - - - - - {% for row in section.rows %} - - - - - - - - - - {% else %} - - {% endfor %} - {% if editable %} - - - - {% endif %} - -
NamePlannedApplied
- + {% for row in section.rows %} + {% set applied = row.entry.applied %} +
+ + + + {% if editable %} + + aria-label="Name" + > + {% else %} + {{ row.entry.name }} + {% endif %} {% if row.state.value == 'edited' %} modified {% elif row.state.value == 'new_in_month' %} new this month {% endif %} -
- + + {% if editable %} + - - - + aria-label="Planned" + > + {% else %} + ${{ '{:,.2f}'.format(row.entry.planned) }} + {% endif %} + + + ${{ '{:,.2f}'.format(applied) }} + {% if row.entry.postings|length > 0 %} + · {{ row.entry.postings|length }} txn{% if row.entry.postings|length != 1 %}s{% endif %} + {% endif %} + + {% if editable %} {% endif %} -
- + +
+ {% if editable %} +
+ -
No entries.
+ aria-label="Notes" + > + + {% elif row.entry.notes %} +
{{ row.entry.notes }}
+ {% endif %} + + + + + + + + + + + + {% for posting in row.entry.postings %} + + + + + + + + {% else %} + + {% endfor %} + +
DateDescriptionPayeeAmount
+ {% if editable %} + + {% else %} + {{ posting.occurred_on.isoformat() }} + {% endif %} + + {% if editable %} + + {% else %} + {{ posting.description or '' }} + {% endif %} + + {% if editable %} + + {% else %} + {{ posting.payee or '' }} + {% endif %} + + {% if editable %} + + {% else %} + ${{ '{:,.2f}'.format(posting.amount) }} + {% endif %} + + {% if editable %} + + {% endif %} +
No transactions yet.
+ {% if editable %}
- - - - + + + + +
-
+ {% endif %} + +
+ {% else %} +
No entries.
+ {% endfor %} + {% if editable %} +
+
+ + + + +
+
+ {% endif %} +