fix(templates): convert 8 broken multi-line {# #} comments + clarify cryptic sublines

CLAUDE.md gotcha #1 strikes again — the dashboard audit pass added
7 multi-line {# ... #} comment blocks across index.html, report.html,
and pdf/report_pdf.html. All rendered as literal text on the live
pages (Konrad screenshotted them). Also caught an old one in
admin/base_site.html that was technically broken syntax but
non-rendering (outside any block). All 8 converted to
{% comment %}{% endcomment %}.

CLAUDE.md updated:
- Bumped the bit-us count (4 → confirmed 4 + 5 + 7 across three
  features). Added a grep-one-liner sanity check that finds broken
  multi-line {# blocks across all templates so future passes can
  spot-check before committing.

Cryptic hero-card sublines on /report/ clarified (Konrad asked
what they mean):
- "as of 08:13" → "Live total at 08:13 today · for <scope>" with
  hover tooltip explaining the snapshot semantics.
- "Company Avg / Working Day" / "/ Month" labels renamed to
  "Avg Labour Cost / Working Day" / "/ Month". Sublines simplified
  to "Lifetime average across all crews" / "Daily figure × 30.44
  days". Both gain hover tooltips that explain the math and the
  "current pay rates" basis.

Pure template + docs change. 173/173 tests still passing
(no test changes — these are cosmetic fixes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-15 08:23:20 +02:00
parent 652168fe88
commit 1d224bc01b
5 changed files with 56 additions and 30 deletions

View File

@ -28,7 +28,7 @@ answers — see `docs/plans/parked-work.md`.
- Add plain English comments explaining what complex logic does
- The project owner is not a programmer — comments should be understandable by a non-technical person
- When creating or editing code, maintain the existing comment structure
- **Django template comments `{# ... #}` are SINGLE-LINE only.** Multi-line blocks need `{% comment %}...{% endcomment %}`. A `{#` on line N with no closing `#}` on the same line renders the whole block as literal text onto the page (and silently — no error). This bit us 4× during the Adjustments feature. Also: the literal tokens `{#` and `#}` cannot appear inside a `{% comment %}` block — they'll be parsed as a nested comment marker. Rephrase meta-notes about comment syntax OUTSIDE the block.
- **Django template comments `{# ... #}` are SINGLE-LINE only.** Multi-line blocks need `{% comment %}...{% endcomment %}`. A `{#` on line N with no closing `#}` on the same line renders the whole block as literal text onto the page (and silently — no error). This bit us 4× during the Adjustments feature, 5× during the Absences feature, and 7× during the 15 May dashboard-audit pass. **Sanity check after any template edit:** `grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'` — every match is a multi-line broken comment. Also: the literal tokens `{#` and `#}` cannot appear inside a `{% comment %}` block — they'll be parsed as a nested comment marker. Rephrase meta-notes about comment syntax OUTSIDE the block.
- **Duplicate `id=""` attributes cause silent bugs.** `document.getElementById()` returns only the FIRST match in DOM order, so adding a second element with an existing id silently steals the handler from the original. Grep the template before assigning any new id (caught `adjSelectAll` collision in Task 6 — header checkbox stole the Add-Adjustment modal's Select-All handler).
- **Bootstrap dropdowns inside `.card` elements get clipped by sibling cards.** A `.dropdown-menu` with `z-index: 1050` rendered inside a filter `.card` will STILL appear behind a sibling table `.card` that follows in document order. Bootstrap's `transform: translate(...)` Popper positioning creates a new stacking context — the z-index is measured INSIDE the parent card, not globally. The fix: lift the wrapping element (e.g. the filter `<form class="card">`) with `style="position: relative; z-index: 10;"` so the entire card sits above its siblings. The dropdown's local z-index then resolves correctly. Bit us on the Absences filter dropdown (May 2026).
- **JS reading from `data-worker-id` was unreliable; read from `<input name="workers">[value]` directly.** Round A's first absence-form team filter rendered `data-worker-id="{{ worker.choice_value }}"` on the row `<div>` and read it via `row.dataset.workerId`. On production this hid ALL workers when a team was selected — likely a stale-template / template-render mismatch. The proven pattern (used by `attendance_log.html` for years) is to read `row.querySelector('input[name="workers"]').value`. The form widget's `<input value="<pk>">` is the source of truth; data attributes are an unnecessary indirection.

View File

@ -1,11 +1,13 @@
{% extends "admin/base.html" %}
{# ===========================================================
Minimal override of the default admin/base_site.html.
The sole purpose right now is to inject a small <style> block
into every Django admin page. Add more admin CSS tweaks here
as needed — keeps them in one place and isolated from the
main app's custom.css.
=========================================================== #}
{% comment %}
===========================================================
Minimal override of the default admin/base_site.html.
The sole purpose right now is to inject a small <style> block
into every Django admin page. Add more admin CSS tweaks here
as needed — keeps them in one place and isolated from the
main app's custom.css.
===========================================================
{% endcomment %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}

View File

@ -148,8 +148,10 @@
<div style="flex: 1;">
<div class="stat-label">Outstanding by Project</div>
{% if outstanding_by_project_list %}
{# Sorted by amount desc. Keyed by project_id so two projects
sharing a name don't get merged. #}
{% comment %}
Sorted by amount desc. Keyed by project_id so two projects
sharing a name don't get merged.
{% endcomment %}
<div style="font-size: 0.85rem; margin-top: 0.35rem;">
{% for proj in outstanding_by_project_list %}
<div class="d-flex justify-content-between" style="color: var(--text-primary);">

View File

@ -744,8 +744,10 @@
<td class="name">{{ w.name }}</td>
<td class="r">{{ w.days }}</td>
<td class="total">R&nbsp;{{ w.total_paid|money }}</td>
{# adj_values now contains {'amount', 'is_deductive'} per column.
Render "-R …" for deductive types. #}
{% comment %}
adj_values now contains an amount + is_deductive flag per column.
Render with a leading minus for deductive types.
{% endcomment %}
{% for val in w.adj_values %}
{% if val.amount %}
<td class="r">{% if val.is_deductive %}&minus;R&nbsp;{% else %}R&nbsp;{% endif %}{{ val.amount|money_abs }}</td>

View File

@ -199,9 +199,11 @@
</div>
{# === HERO KPI BAND === #}
{# Sub-labels intentionally call out current-pay-rate basis (Finding 2)
and the active filter scope (Finding 5) so the KPIs aren't read as
apples-to-apples when filters are on. #}
{% comment %}
Sub-labels intentionally call out current-pay-rate basis (Finding 2)
and the active filter scope (Finding 5) so the KPIs aren't read as
apples-to-apples when filters are on.
{% endcomment %}
<div class="row g-3 mb-4 hero-kpi-row">
<div class="col-lg-3 col-md-6">
<div class="stat-card stat-card--danger stat-card--hero h-100">
@ -216,26 +218,36 @@
<div class="stat-card stat-card--warning stat-card--hero h-100">
<div class="stat-label">Outstanding Now</div>
<div class="stat-value">R {{ current_outstanding.total|money }}</div>
<div class="stat-subline">
as of {{ current_as_of|date:"H:i" }}
<div class="stat-subline"
data-bs-toggle="tooltip"
title="Live total: unpaid wages plus any pending payroll adjustments. Refresh the page to see the latest.">
Live total at {{ current_as_of|date:"H:i" }} today
{% if project_name != 'All Projects' or team_name != 'All Teams' %}
&middot; scoped to {{ project_name }}{% if team_name != 'All Teams' %} / {{ team_name }}{% endif %}
&middot; for {{ project_name }}{% if team_name != 'All Teams' %} / {{ team_name }}{% endif %}
{% endif %}
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="stat-card stat-card--info stat-card--hero h-100">
<div class="stat-label">Company Avg / Working Day</div>
<div class="stat-label">Avg Labour Cost / Working Day</div>
<div class="stat-value">R {{ company_avg_daily|money }}</div>
<div class="stat-subline">lifetime &middot; all crews &middot; at current pay rates</div>
<div class="stat-subline"
data-bs-toggle="tooltip"
title="Sum of daily rates across all crews on every day FoxFitt has logged work, divided by the count of those days. Uses each worker's CURRENT salary — a recent raise will retroactively change this figure.">
Lifetime average across all crews
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="stat-card stat-card--info stat-card--hero h-100">
<div class="stat-label">Company Avg / Month</div>
<div class="stat-label">Avg Labour Cost / Month</div>
<div class="stat-value">R {{ company_avg_monthly|money }}</div>
<div class="stat-subline">lifetime &middot; ~30.44 days/month &middot; at current pay rates</div>
<div class="stat-subline"
data-bs-toggle="tooltip"
title="The daily figure to the left multiplied by 30.44 (the average number of days in a month, i.e. 365.25 ÷ 12). Same lifetime + current-pay-rates basis.">
Daily figure &times; 30.44 days
</div>
</div>
</div>
</div>
@ -390,8 +402,10 @@
<table class="table table-sm mb-0">
<thead><tr><th>Category</th><th class="text-end">Total</th></tr></thead>
<tbody>
{# Sign-aware rendering: deductive types show as red "-R …";
additive types stay in default colour with no sign. Finding 4. #}
{% comment %}
Sign-aware rendering: deductive types show as red "-R ...";
additive types stay in default colour with no sign. Finding 4.
{% endcomment %}
{% for item in adjustment_totals %}
<tr>
<td>{{ item.label }}</td>
@ -487,8 +501,10 @@
<th>Worker</th>
<th class="text-end">Days</th>
<th class="text-end">Total Paid</th>
{# Sign-aware headers: deductive types render in muted red
so the negative sign on rows below is unmistakable. Finding 4. #}
{% comment %}
Sign-aware headers: deductive types render in muted red
so the negative sign on rows below is unmistakable. Finding 4.
{% endcomment %}
{% for h in active_adj_headers %}
<th class="text-end d-none d-md-table-cell {% if h.is_deductive %}text-danger{% endif %}" style="font-size: 0.75rem;">{{ h.label }}</th>
{% endfor %}
@ -500,8 +516,10 @@
<td class="fw-medium">{{ w.name }}</td>
<td class="text-end">{{ w.days }}</td>
<td class="text-end fw-semibold">R {{ w.total_paid|money }}</td>
{# Each val is now {'amount': Decimal, 'is_deductive': bool}
— render "-R 500.00" in red for deductive types. #}
{% comment %}
Each val is now an "amount" Decimal plus an "is_deductive" bool
— render the amount in red with a leading minus sign for deductive types.
{% endcomment %}
{% for val in w.adj_values %}
<td class="text-end d-none d-md-table-cell {% if val.is_deductive and val.amount %}text-danger{% endif %}" style="font-size: 0.8rem;">
{% if val.amount %}
@ -514,8 +532,10 @@
</tbody>
</table>
</div>
{# Footnote — explains why Days and Total Paid can read as
"not matching" within a single period (different timing). #}
{% comment %}
Footnote — explains why Days and Total Paid can read as
"not matching" within a single period (different timing).
{% endcomment %}
<p class="text-muted px-3 py-2 mb-0" style="font-size: 0.75rem; border-top: 1px solid var(--border-subtle);">
<i class="fas fa-info-circle me-1"></i>
<strong>Days</strong> reflect work logged in this period.