From 84e9d247bed0a76e28e5e33620e9f67b04ab642c Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Fri, 24 Apr 2026 09:36:51 +0200 Subject: [PATCH] docs(ux): task-by-task plan for UX Polish Pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five tasks: (1) docs/design-tokens.md as the canonical colour reference; (2) CLAUDE.md UI-vs-DB naming-drift note (ships BEFORE the rename so it's searchable from minute one); (3) display-only TYPE_CHOICES rename + auto-migration + template visible-text swap to get_type_display; (4) badge colour unification on Pending + History tabs + loan-flag recolor; (5) CSS root-cause fix for the group-summary narrow-wrap bug (move display:flex from to ). Execute via subagent-driven-development. Auto mode — no mid-execution checkpoints. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/2026-04-24-ux-polish-plan.md | 742 ++++++++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 docs/plans/2026-04-24-ux-polish-plan.md diff --git a/docs/plans/2026-04-24-ux-polish-plan.md b/docs/plans/2026-04-24-ux-polish-plan.md new file mode 100644 index 0000000..eb916fd --- /dev/null +++ b/docs/plans/2026-04-24-ux-polish-plan.md @@ -0,0 +1,742 @@ +# UX Polish Pass — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Auto mode is active — execute continuously, no mid-execution checkpoints. + +**Goal:** Shorter adjustment-type labels in tables (display-only, DB untouched), uniform semantic badge colours across all payroll tabs, a CSS bug fix on the group-summary row, and two new documentation artifacts to prevent future confusion. + +**Architecture:** Path A — display-only rename. Every DB value stays exactly as it is today; only the human-readable labels change via the second tuple element of `TYPE_CHOICES`. Unifies badge colours by replacing ~3 branches of Bootstrap-state-class conditionals with the existing `.badge-type-*` semantic palette. Moves one CSS property (`display: flex`) from a `` (where it silently breaks table rendering) to a `` (where it does what the original author intended). + +**Tech Stack:** Django 5.2.7 (TYPE_CHOICES + get_type_display + auto-generated AlterField migration); custom.css semantic palette (already in place); Django template filter `type_slug` (already in place). No new libraries. + +**Design doc:** `docs/plans/2026-04-24-ux-polish-design.md` (committed as `9aba9b8`). + +**Starting HEAD:** `9aba9b8` on branch `ai-dev`. + +**Expected net change:** ~120-180 lines across 6 files + 1 new doc + 1 auto-generated migration. + +--- + +## Critical context for every task + +**The naming drift:** After this pass, what the user SEES and what the DATABASE stores for `PayrollAdjustment.type` will diverge permanently. Learn this before touching anything: + +| DB value (CANONICAL) | Display label (new) | +|---|---| +| `'New Loan'` | `Loan` | +| `'Advance Payment'` | `Advance` | +| `'Advance Repayment'` | `Advance Repaid` | + +**Never change the left column.** All logic, constants (`ADDITIVE_TYPES`), tests, CSS class slugs (`.badge-type-new-loan`), and `data-type="..."` attributes use the left column. + +**Two kinds of template usage of `adj.type`:** +1. **Visible text** (renders letters on the page): `{{ adj.type }}` → change to `{{ adj.get_type_display }}` +2. **Identifier** (feeds a CSS class via `|type_slug`, or a `data-type=` attribute, or a control-flow `{% if %}`): keep as `{{ adj.type }}` — the raw DB value is the right thing to emit + +The implementer for Tasks 3 and 4 MUST grep both patterns before editing and make the call case-by-case. + +--- + +## Task 1: Create `docs/design-tokens.md` (canonical colour reference) + +**Goal:** A single doc that lists every semantic colour token in the app, where it's used, and how to add a new one. Do this BEFORE any colour code-changes so the reference exists first. + +**Files:** +- Create: `docs/design-tokens.md` + +**Step 1: Create the file with the full content** + +Write the following to `docs/design-tokens.md` (verbatim): + +````markdown +# Design Tokens — Semantic Colour Palette + +_Last reviewed: 24 Apr 2026._ + +## How colours are structured + +The app has TWO colour categories — they MUST NOT share colours: + +1. **Type-of-adjustment** — 7 types × 2 themes. Used wherever a + `PayrollAdjustment` is shown as a badge or a group-header accent. + Token naming: `--badge--bg` / `--badge--fg`. +2. **Transactional state** — Bootstrap's `bg-success` / + `bg-warning` / `bg-danger`. Used for Paid, Unpaid, Overdue — + the payment lifecycle, not the kind of adjustment. + +Mixing the two would make a green badge mean both "this is a Bonus" +AND "this is Paid" — the user would lose the ability to read the +colour as a signal. Keep the categories separate. + +## Type-of-adjustment tokens + +| DB type (canonical) | Displayed as | Dark BG | Dark FG | Light BG | Light FG | CSS class | +|---|---|---|---|---|---|---| +| Bonus | Bonus | `#5b8260` | `#e8f3ea` | `#d7e8d9` | `#385640` | `.badge-type-bonus` | +| Overtime | Overtime | `#a16881` | `#fce4ec` | `#f3d1dd` | `#703347` | `.badge-type-overtime` | +| Deduction | Deduction | `#5b4f8c` | `#e0daf3` | `#d8d0ef` | `#3b2f6d` | `.badge-type-deduction` | +| New Loan | Loan | `#9b7f39` | `#fef4d1` | `#f0dc9d` | `#6a5320` | `.badge-type-new-loan` | +| Loan Repayment | Loan Repayment | `#b48a1a` | `#fef4d1` | `#f7d873` | `#5a4418` | `.badge-type-loan-repayment` | +| Advance Payment | Advance | `#3e5c7b` | `#d7e5f2` | `#bccee0` | `#243b56` | `.badge-type-advance-payment` | +| Advance Repayment | Advance Repaid | `#2f679a` | `#d7e5f2` | `#9ec1dd` | `#1d3550` | `.badge-type-advance-repayment` | + +Token definitions live in `static/css/custom.css`: +- Dark theme: `:root { ... }` block around lines 85-91 +- Light theme: `[data-theme="light"] { ... }` block around lines 149-155 + +## Where each colour appears + +| Semantic | Used by | +|---|---| +| `--badge-bonus-*` (green) | Adjustments tab type badge; By-Type group-header left-border accent | +| `--badge-overtime-*` (mauve) | Adjustments tab type badge; By-Type group-header accent | +| `--badge-deduction-*` (purple) | Adjustments tab type badge; By-Type group-header accent | +| `--badge-loan-*` (amber/yellow) | Adjustments tab type badge; By-Type group-header accent; Pending tab "Loan" worker flag (`.loan-flag-badge`) | +| `--badge-loan-rep-*` (deeper amber, +15% saturation) | Adjustments tab type badge for Loan Repayment; By-Type group-header accent | +| `--badge-advance-*` (blue) | Adjustments tab type badge; By-Type group-header accent | +| `--badge-advance-rep-*` (deeper blue, +15% saturation) | Adjustments tab type badge for Advance Repayment; By-Type group-header accent | + +## Transactional-state colours (Bootstrap — unchanged) + +| Use | Class | +|---|---| +| Paid payslip badge | `bg-success` | +| Unpaid status badge | `bg-warning` | +| Overdue worker flag (Pending tab) | `bg-danger` | + +## How to add a new colour token + +1. Define in BOTH the `:root` and `[data-theme="light"]` blocks in + `static/css/custom.css`. Choose colours that retain enough contrast + against the card background in both themes. +2. Add a row to the mapping table in this doc. +3. Reference via `var(--badge-*-bg)` in CSS — never hard-code hex + anywhere else. +4. If it's a new adjustment type, add: + - A `.badge-type-` class in the `.badge-type-*` block + (around line 1935 of `custom.css`) + - An entry in the `.adj-group-header[data-type="..."]` block + (around line 1994) + - The new TYPE_CHOICES entry in `core/models.py::PayrollAdjustment` + (and run `makemigrations`) + +## Maintenance + +This doc is the single source of truth for app-wide colour semantics. +When CSS tokens are added / removed / renamed in `custom.css`, update +this doc in the SAME commit. +```` + +**Step 2: Commit** + +```bash +git add docs/design-tokens.md +git commit -m "$(cat <<'EOF' +docs(tokens): add canonical design-tokens reference + +New doc covering the semantic colour palette: every badge token, its +hex values in both themes, its CSS class, and where it's used across +the app. Categorises tokens into "type-of-adjustment" (custom semantic +palette) vs "transactional state" (Bootstrap defaults) and explains +why the two must not share colours. + +Intended to be the single source of truth for UI colour decisions. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +No tests, no code change. File-only addition. + +--- + +## Task 2: CLAUDE.md — the "UI-vs-DB naming drift" section + +**Goal:** Document the permanent gap between display labels and DB values so future Claude (and Konrad, on a tired day) don't chase ghosts. This goes in BEFORE any rename work — so it's searchable the moment the rename ships. + +**Files:** +- Modify: `CLAUDE.md` (insert a new section near the existing "Schema name-drifts to remember" section) + +**Step 1: Find the insertion point** + +Use Grep to find the exact line: + +``` +grep -n "Schema name-drifts to remember" CLAUDE.md +``` + +Expected: one match, around line 52 (give or take). + +**Step 2: Read the next ~20 lines to see the end of that block** + +Use the Read tool at that line, limit=30, to see where the "Schema name-drifts" section ends. Note the line of the last bullet + the blank line after it. + +**Step 3: Insert the new section** + +Insert the following block AFTER the last `- ` bullet of the "Schema name-drifts to remember" section (before the next `##` header): + +```markdown + +## UI-vs-DB naming drift (Apr 2026) — READ BEFORE WRITING FORMULAS + +`PayrollAdjustment.type` is DISPLAYED to users with short labels, +but the raw string stored in the database is always the long +legacy value: + +| What the user SEES | What the DATABASE stores | +|---|---| +| Bonus | `'Bonus'` | +| Overtime | `'Overtime'` | +| Deduction | `'Deduction'` | +| Loan Repayment | `'Loan Repayment'` | +| Loan | `'New Loan'` ← mismatch | +| Advance | `'Advance Payment'` ← mismatch | +| Advance Repaid | `'Advance Repayment'` ← mismatch | + +When writing ANY formula, filter, comparison, ORM query, test +fixture, CSS class name, or `data-type=` attribute: use the +DATABASE value (left column of the model). + +- `ADDITIVE_TYPES = ['Bonus', 'Overtime', 'New Loan', 'Advance Payment']` + in `views.py` uses DB values. +- `if adj.type == 'New Loan':` checks the DB value. +- `` produces + `.badge-type-new-loan` from the DB value. +- `` emits the DB value. +- Tests use `PayrollAdjustment.objects.create(type='New Loan', ...)`. + +Only user-facing template TEXT uses the short label — via +`{{ adj.get_type_display }}`, Django's built-in choices lookup. +The label mapping lives in `PayrollAdjustment.TYPE_CHOICES` +(`core/models.py`). + +**How this happened:** originally the adjustment-creation dropdown +said "New Loan" because that's what the action meant (_"log a new +loan"_). That label then propagated into every other view — tables, +badges, reports. On 24 Apr 2026 we renamed the user-visible labels +to be shorter and cleaner BUT deliberately kept the database values +untouched — to avoid breaking historic rows, tests, and hardcoded +string comparisons across ~30 source locations. + +**Symptom of getting this wrong:** code that filters for +`type='Loan'` returns zero rows. Fix: use `type='New Loan'`. + +``` + +**Step 4: Verify the insert didn't break anything** + +``` +grep -c "^## " CLAUDE.md # section-count should have increased by exactly 1 +``` + +Run the test suite as a sanity check (CLAUDE.md isn't code, but any accidental wholesale rewrite of the file would show up elsewhere): + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 0 +``` + +Expected: 69/69 pass (no code changed). + +**Step 5: Commit** + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +docs(claude): UI-vs-DB naming drift note (pre-rename) + +Adds a new CLAUDE.md section documenting the display/DB gap that +Path A of the UX Polish Pass creates: user sees 'Loan' / 'Advance' +/ 'Advance Repaid' while DB stores 'New Loan' / 'Advance Payment' +/ 'Advance Repayment'. Includes a lookup table, the rule for when +to use which (DB for logic, display for templates), and the failure +symptom so future Claude sessions don't chase ghost filters. + +Ships BEFORE the rename so the doc is searchable from minute one. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 3: Display-only rename (TYPE_CHOICES + visible templates + migration) + +**Goal:** The three rename labels take effect everywhere a user sees a type. DB stays untouched. `makemigrations` generates the required no-op `AlterField`. + +**Files:** +- Modify: `core/models.py` — the `TYPE_CHOICES` list in `PayrollAdjustment` +- Create: `core/migrations/0012_alter_payrolladjustment_type.py` (auto-generated — do NOT hand-write) +- Modify: `core/templates/core/payroll_dashboard.html` — lines 370, 454 (visible `{{ adj.type }}` emissions) +- Modify: `core/templates/core/_adjustment_row.html` — the type-cell render (uses `badge-type-{{ adj.type|type_slug }}` and also emits `{{ adj.type }}` as visible text — only the visible-text copy changes) +- Grep-audit: other templates (`payslip_detail.html`, PDF templates, any other `{{ adj.type }}` emission) — audit & fix where visible + +**Step 1: Edit `core/models.py`** + +Read the current `TYPE_CHOICES` block first: + +``` +grep -n "TYPE_CHOICES = \[" core/models.py +``` + +Expected: one match around line 190. Read lines 190-210 to confirm current state matches: + +```python +TYPE_CHOICES = [ + ('Bonus', 'Bonus'), + ('Overtime', 'Overtime'), + ('Deduction', 'Deduction'), + ('Loan Repayment', 'Loan Repayment'), + ('New Loan', 'New Loan'), + ('Advance Payment', 'Advance Payment'), + ('Advance Repayment', 'Advance Repayment'), +] +``` + +Replace with: + +```python +# === PayrollAdjustment TYPE_CHOICES — canonical DB value | display label === +# Path A rename (24 Apr 2026): DB values are PRESERVED as-is. Only the +# second tuple element (the human label) changes for three types, so +# users see shorter labels in tables while every historic row, formula, +# constant, test fixture, CSS class, and data-attribute KEEP WORKING +# UNCHANGED because they all key off the DB value on the left. +# See CLAUDE.md "UI-vs-DB naming drift" section for the full rule. +TYPE_CHOICES = [ + ('Bonus', 'Bonus'), + ('Overtime', 'Overtime'), + ('Deduction', 'Deduction'), + ('Loan Repayment', 'Loan Repayment'), + ('New Loan', 'Loan'), + ('Advance Payment', 'Advance'), + ('Advance Repayment', 'Advance Repaid'), +] +``` + +**Step 2: Generate the migration** + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py makemigrations core --name alter_payrolladjustment_type_display_labels +``` + +Expected output: creates `core/migrations/0012_alter_payrolladjustment_type_display_labels.py` with a single `AlterField` operation changing `choices`. The migration is a no-op at the database level — Django tracks choices in its model metadata, not in the DB schema. + +Open the generated file and confirm the operation is EXACTLY `AlterField` with `choices=[...]` — no `RunPython`, no `RunSQL`, no schema-altering operation. If you see anything more, stop and ask — something is wrong. + +**Step 3: Run the migration locally** + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py migrate +``` + +Expected: `Applying core.0012_alter_payrolladjustment_type_display_labels... OK` — 1 operation, < 1 second. + +**Step 4: Grep-audit visible `{{ adj.type }}` template usages** + +``` +grep -rn "{{ adj\.type }}" core/templates/ +grep -rn "{{ adjustment\.type }}" core/templates/ +grep -rn "{{ a\.type }}" core/templates/ +``` + +For EACH match, decide: +- Is the surrounding context VISIBLE text (e.g., `>{{ adj.type }}<` inside a badge span, or in a table cell)? → change to `{{ adj.get_type_display }}` (or `{{ adjustment.get_type_display }}` / `{{ a.get_type_display }}`) +- Is it a DATA ATTRIBUTE (`data-adj-type="{{ adj.type }}"`) or a CSS CLASS slug (`badge-type-{{ adj.type|type_slug }}`)? → LEAVE AS-IS. This is an identifier feed, not a display text. + +Known sites (from the brainstorm grep — verify each): +1. `core/templates/core/payroll_dashboard.html:370` — inside `{{ adj.type }}` → SWAP to `{{ adj.get_type_display }}` +2. `core/templates/core/payroll_dashboard.html:454` — `{{ adj.type }}: R {{ adj.amount }}` → SWAP +3. `core/templates/core/payroll_dashboard.html:363` — `data-adj-type="{{ adj.type }}"` → LEAVE (identifier, consumed by JS) +4. `core/templates/core/_adjustment_row.html` — grep in that file; type appears both as a CSS class slug AND potentially as visible text. Only the visible-text copy swaps. + +Also audit PDF + other detail pages: + +``` +grep -rn "{{ adj\.type }}\|{{ adjustment\.type }}\|{{ a\.type }}" core/templates/core/pdf/ core/templates/core/payslip_detail.html core/templates/core/report.html +``` + +Apply the same visible-vs-identifier call. + +**Step 5: Run the full test suite** + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2 +``` + +Expected: 69/69 pass. Tests use DB values in fixtures (`type='New Loan'`) so NONE should break. If any test fails, something in Step 4 went wrong — revert and re-audit. + +**Step 6: Visual smoke test via `manage.py shell`** + +Quick sanity check that `get_type_display` returns the new labels: + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py shell -c "from core.models import PayrollAdjustment; choices = dict(PayrollAdjustment.TYPE_CHOICES); print('New Loan displays as:', choices.get('New Loan')); print('Advance Payment displays as:', choices.get('Advance Payment')); print('Advance Repayment displays as:', choices.get('Advance Repayment'))" +``` + +Expected output: +``` +New Loan displays as: Loan +Advance Payment displays as: Advance +Advance Repayment displays as: Advance Repaid +``` + +**Step 7: Commit** + +```bash +git add core/models.py core/migrations/0012_*.py core/templates/ +git commit -m "$(cat <<'EOF' +ux(labels): shorter adjustment type labels (display-only rename) + +Path A rename — DB values untouched, only TYPE_CHOICES display +labels change: + 'New Loan' → shown as 'Loan' + 'Advance Payment' → shown as 'Advance' + 'Advance Repayment' → shown as 'Advance Repaid' + +Templates that render the type as visible text switched from +{{ adj.type }} to {{ adj.get_type_display }}. Data attributes and +CSS class slugs keep the raw DB value (identifiers, not labels). + +Zero data migration. Zero changes to ADDITIVE_TYPES / DEDUCTIVE_TYPES +constants, hardcoded string comparisons, CSS class names, test +fixtures, or any other code that references the canonical DB value. +Every historic PayrollAdjustment row keeps type='New Loan' / +'Advance Payment' / 'Advance Repayment' as stored. + +Django's makemigrations generated a no-op AlterField migration to +record the choices-metadata change. + +Tests: 69/69. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 4: Badge colour unification + loan-flag recolor + +**Goal:** Replace the three occurrences of the 4-branch Bootstrap-state conditional with a one-liner `badge-type-{{ adj.type|type_slug }}` that reuses the existing semantic palette. Recolour the Pending-tab "Loan" worker flag to the loan type colour. + +**Files:** +- Modify: `core/templates/core/payroll_dashboard.html` — lines 360, 453 (type-badge blocks) + line 345 (loan flag) +- Modify: `static/css/custom.css` — add `.loan-flag-badge` class near the `.badge-type-*` block + +**Step 1: Preconditions** + +Confirm the existing context-variable `additive_types` is threaded to the Pending + History tab contexts. The Adjustments tab already uses it (see `_adjustment_row.html` line 42). For Pending + History, grep: + +``` +grep -n "additive_types" core/views.py +``` + +If the Pending/History branch of `payroll_dashboard()` does NOT set `additive_types` in its context, we need to add it. The constant is already defined at `views.py:45` — it's just a matter of adding one key to the context dict. Look for the `context = { ... }` block in the `payroll_dashboard` view that handles the default/pending branch (grep for `'workers_data'` as a nearby key — the same context dict). + +If `additive_types` is already set for those branches, no view change is needed. Only the templates change. + +**Step 2: Replace the type-badge block at line 360 (Pending tab)** + +Current (lines 356-372): +```django + {% for adj in wd.adjustments %} + {# Badge colour logic: #} + {# GREEN = earned money (Bonus, Overtime) or debt recovery (Loan/Advance Repayment) #} + {# YELLOW = loan-related outflow (New Loan, Advance Payment) — matches the Loan tag #} + {# RED = deductions (Deduction) #} + + {% if adj.type == 'Bonus' or adj.type == 'Overtime' or adj.type == 'New Loan' or adj.type == 'Advance Payment' %}+{% else %}-{% endif %}R{{ adj.amount|floatformat:2 }} + {{ adj.type }} + {% if adj.project %}({{ adj.project.name }}){% endif %} + + {% endfor %} +``` + +Note: `{{ adj.type }}` at the old line 370 will already have been changed to `{{ adj.get_type_display }}` by Task 3. If Task 3 shipped correctly, the current text at line 370 is `{{ adj.get_type_display }}`. Don't revert it. + +Replace the badge block with: +```django + {% for adj in wd.adjustments %} + {# Type badge uses the semantic palette: colour = type (Bonus, Loan, etc.). #} + {# Sign + / − reflects additive-vs-deductive (orthogonal to the colour). #} + + {% if adj.type in additive_types %}+{% else %}-{% endif %}R{{ adj.amount|floatformat:2 }} + {{ adj.get_type_display }} + {% if adj.project %}({{ adj.project.name }}){% endif %} + + {% endfor %} +``` + +Three things happened in this change: +- `class="badge ...multi-line conditional..."` → `class="badge badge-type-{{ adj.type|type_slug }}"` +- Sign logic refactored from a long `{% if %}` chain to `{% if adj.type in additive_types %}` (cleaner, single source of truth for additive set) +- Outdated comment block removed; new comment describes the current semantic scheme + +**Step 3: Replace the type-badge block at line 453 (History tab)** + +Current: +```django + {% for adj in record.adjustments.all %} + + {{ adj.type }}: R {{ adj.amount|floatformat:2 }} + + {% empty %} +``` + +(The `{{ adj.type }}` on the inner line was already swapped to `{{ adj.get_type_display }}` by Task 3.) + +Replace with: +```django + {% for adj in record.adjustments.all %} + + {{ adj.get_type_display }}: R {{ adj.amount|floatformat:2 }} + + {% empty %} +``` + +**Step 4: Recolour the loan flag at line 345 (Pending tab)** + +Current line 345: +```django + Loan +``` + +Replace with: +```django + Loan +``` + +Line 342 (Overdue flag) stays `bg-danger` — it's transactional / urgency, not type. Don't touch it. + +**Step 5: Add the `.loan-flag-badge` CSS class** + +In `static/css/custom.css`, find the `.badge-type-advance-repayment` line (around line 1941) and append a new block right after the type-badge definitions: + +```css +/* --- Status flags that borrow a type's colour for semantic consistency. + "Has an active loan or advance" → Loan-type amber/yellow, so the + worker flag on the Pending tab visually matches the Adjustments + type badge for Loan. Keeps the Loan colour family unified across + the app regardless of which tab you're looking at. --- */ +.loan-flag-badge { + background: var(--badge-loan-bg); + color: var(--badge-loan-fg); +} +``` + +**Step 6: Run the full test suite** + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2 +``` + +Expected: 69/69. Template changes don't hit any test assertion. + +**Step 7: Visual smoke-test checklist** + +The implementer must mentally walk through these with the template open in an editor (no browser needed if Django check passes): + +- [ ] Pending tab: `{% for adj in wd.adjustments %}` block — the badge has exactly one class besides `badge`: `badge-type-{{ adj.type|type_slug }}`. No `bg-success`/`bg-warning`/`bg-danger` left. +- [ ] Pending tab: the "Loan" worker flag uses `loan-flag-badge` class, NOT `bg-warning`. +- [ ] Pending tab: the "Overdue" worker flag still uses `bg-danger`. (Don't "helpfully" change this.) +- [ ] History tab: `{% for adj in record.adjustments.all %}` block — same check as Pending. +- [ ] Paid #N / Unpaid badges elsewhere — still use Bootstrap state classes (`bg-success`/`bg-warning`). NOT touched. + +Run `python manage.py check` as a final sanity check: +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py check +``` +Expected: no errors (the pre-existing `staticfiles.W004` warning is fine). + +**Step 8: Commit** + +```bash +git add core/templates/core/payroll_dashboard.html static/css/custom.css +git commit -m "$(cat <<'EOF' +ux(colors): unify badge colours across all payroll tabs + +Replaces the 4-branch Bootstrap-state conditional on the Pending +and History tabs with the semantic .badge-type-{{ adj.type|type_slug }} +palette that the Adjustments tab has been using. Now "Loan" badges +are the same colour in every tab instead of Pending=yellow / +Adjustments=amber. + +Also recolours the Pending-tab "Loan" worker flag to the same amber +(.loan-flag-badge class). "Overdue" flag stays red — it's an urgency +signal, not a type signal, and we deliberately keep transactional +state colours (Bootstrap bg-success/bg-warning/bg-danger) separate +from the type palette so a green badge can only mean "Bonus" and +never ambiguously "Paid". + +Tests: 69/69. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 5: Fix `.adj-group-header` CSS (narrow-wrap bug) + +**Goal:** Make the group-summary row span the full table width and push the "N row · +R X net" meta to the right, as originally intended. Root cause: `display: flex` was applied to the ``, which breaks table rendering (a flexed `` ignores `colspan` and shrinks to intrinsic content). + +**Files:** +- Modify: `static/css/custom.css` — the `.adj-group-header` block around lines 1972-1988 + +**Step 1: Read the current block** + +``` +grep -n "\.adj-group-header {" static/css/custom.css +``` + +Expected: one match at line 1972. Read lines 1972-2000 to see the full current state. + +**Step 2: Rewrite the block** + +Replace the CURRENT content of lines 1972-1988 (note: preserve line 1984's `:hover` rule and everything below it): + +```css +/* --- Group header (collapsible section divider for group-by mode) --- + NOTE: display: flex MUST be on the , NOT on the . Setting + display on a removes it from table row/column participation + (colspan is ignored, the row shrinks to intrinsic content width), + which caused the "narrow wrap" screenshot bug in Apr 2026. The td + is an ordinary block box and flexes fine. --- */ +.adj-group-header { + cursor: pointer; + background: var(--bg-inset); + border-top: 1px solid var(--border-default); + border-bottom: 1px solid var(--border-default); + user-select: none; + transition: background-color 120ms; +} +.adj-group-header > td { + padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} +.adj-group-header:hover { background: var(--bg-card-hover); } +.adj-group-header .fa-chevron-down, +.adj-group-header .fa-chevron-right { opacity: 0.7; width: 0.8rem; } +.adj-group-header .adj-group-label { font-weight: 600; } +.adj-group-header .adj-group-meta { color: var(--text-secondary); font-size: 0.875rem; margin-left: auto; white-space: nowrap; } +``` + +Diff from before: +- REMOVED `display: flex; align-items: center; gap: 0.75rem;` from `.adj-group-header` (it was on the ``, which was the bug) +- REMOVED `padding: 0.75rem 1rem;` from `.adj-group-header` (moved to the `` where it belongs) +- ADDED `.adj-group-header > td { ... }` with the flex stuff now applied to the `` — where it actually works +- ADDED `white-space: nowrap;` on `.adj-group-meta` so the meta text can overflow or fit on one line, never wrap into an ugly stub even in narrow viewports +- ADDED the explanatory comment documenting why this looks slightly unusual + +Leave lines 1994-2007 (the `[data-type="X"]` border-left rules + chevron-rotation rules) alone — they were already correctly scoped and don't need to change. + +**Step 3: Run the test suite** + +``` +set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2 +``` + +Expected: 69/69. CSS-only change; no test should react. + +**Step 4: Visual confirmation** (walk the template + CSS mentally) + +- [ ] `` no longer gets `display: flex`, so the `` participates in table layout again. `colspan="10"` on the `` is now honoured — the row spans all 10 columns. +- [ ] `` has `display: flex`, so the icon / label / meta are flex children. `align-items: center` vertically centres them. `gap: 0.75rem` puts space between them. +- [ ] `.adj-group-meta { margin-left: auto; }` now works (it's a flex child of a flex container). +- [ ] `.adj-group-meta { white-space: nowrap; }` prevents the "Bonus 1 / row · +R / 444" stutter wrap even if something downstream tries to squeeze the cell. +- [ ] The `[data-type="X"]` border-left accent still paints the left edge of the row (it targets `.adj-group-header`, which is the ``; the `` is once again a normal table row). + +**Step 5: Commit** + +```bash +git add static/css/custom.css +git commit -m "$(cat <<'EOF' +fix(css): move display:flex from to on adj-group-header + +Root cause of Konrad's narrow-wrap screenshot: display:flex was set +on .adj-group-header (a ), which causes the browser to remove +the row from table layout. A flex-mode ignores colspan and +shrinks to intrinsic content width — which is why a row with +colspan=10 ended up rendering at ~80-100px and wrapping the meta +text into a 5-char column. + +Moved display:flex, align-items, gap, and padding onto the single + child. The td is a normal block box and flexes correctly, +putting icon + label + meta in a horizontal row with the meta +pushed to the right via margin-left:auto (now working since its +parent is a real flex container). + +Also added white-space:nowrap on .adj-group-meta so the meta never +wraps mid-phrase even if a narrow viewport squeezes the cell. + +Inline comment documents the vs distinction so future +sessions don't re-introduce the bug. + +Tests: 69/69. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Final acceptance checklist + +Before declaring the pass complete, the controller verifies: + +- [ ] Every commit's prefix is appropriate (`docs(...)`, `ux(...)`, `fix(css)`) +- [ ] `set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2` → 69/69 passing +- [ ] `python manage.py check` → only the pre-existing `staticfiles.W004` warning +- [ ] `python manage.py migrate` ran cleanly in local dev (one new migration 0012) +- [ ] `grep -n "New Loan" core/models.py` still finds the DB value in TYPE_CHOICES (left tuple element) → confirms Path A discipline held +- [ ] `grep -rn "{{ adj.type }}" core/templates/` → only identifier contexts remain (data-attrs, CSS class slugs), no visible-text usages +- [ ] `grep -n "bg-success\|bg-warning\|bg-danger" core/templates/core/payroll_dashboard.html` → still finds the "Paid"/"Unpaid"/"Overdue" occurrences (transactional state, correct), but NONE on adjustment-type badges +- [ ] Working tree clean, branch ready to push + +--- + +## What's NOT in this plan (explicit non-goals) + +- Any change to DB values of `PayrollAdjustment.type` +- Any edit to `ADDITIVE_TYPES` / `DEDUCTIVE_TYPES` constants +- Any hardcoded-string comparison in `core/views.py` +- Any test fixture or assertion +- Any badge class rename (`.badge-type-new-loan` stays) +- Any `data-type="..."` attribute value change +- Bootstrap state colours being replaced anywhere (we explicitly keep `bg-success`/`bg-warning`/`bg-danger` for transactional badges) +- Adjustments tab layout or filter changes (Pass A for that shipped earlier) + +Rollback: `git revert ` on any individual commit. No data, schema, or URL-contract impact in any task. + +--- + +## Execution + +Plan complete and saved to `docs/plans/2026-04-24-ux-polish-plan.md`. + +Per the arguments this plan was generated with, auto mode is active and the execution path is: + +**Sub-skill: `superpowers:subagent-driven-development`** + +Controller stays in-session and dispatches fresh subagents per task with spec-compliance + code-quality review after each. Expected ~5 task commits + ~2 fix commits if reviewers find anything = ~7 commits on `ai-dev`.