38686-vm/docs/plans/2026-04-24-ux-polish-design.md
Konrad du Plessis 9aba9b8fb8 docs(ux): design for UX Polish Pass
Four UX asks bundled in one pass:

1+2. Display-only rename of adjustment types: 'New Loan' (DB) →
     'Loan' (UI), 'Advance Payment' → 'Advance', 'Advance Repayment'
     → 'Advance Repaid'. DB values preserved forever — zero data
     migration, zero formula / constant / CSS / test changes.
3. Unify badge colours across all payroll tabs using the existing
   .badge-type-* semantic palette. Recolour Pending "With loans"
   flag to match the Loan type colour.
4. Fix CSS bug in .adj-group-meta (margin-left:auto doesn't work
   in a <td> — make the td a flex container).

Plus: new docs/design-tokens.md as the canonical colour reference,
and a crucial CLAUDE.md section documenting the UI-vs-DB naming
drift so future Claude sessions don't chase ghosts when writing
formulas / filters / ORM queries that reference the display label
instead of the DB value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:32:23 +02:00

15 KiB
Raw Blame History

UX Polish Pass — Design (24 Apr 2026)

Origin

Konrad, after the Perf Quick-Wins Pass shipped:

1. I named Loans — "New Loan" because in the dropdown for adjustments, I wanted to log a new loan. But now throughout the app it says "New Loan". Can we change it everywhere to just "Loan". (How Will this handle historic data and formulas? Make sure we do not break the app)

2. Is it Possible to change Advance Payment to Adv Pay, Advance Repayment to Adv Repay. It will take less space in the tables (all tables) — refined in the brainstorm to "Advance" / "Advance Repaid" for readability

3. Can we change the colors for flags/tags in Pending Payments, Payment History, Loans and Advances to the colors we decided on in Adjustments (colors for Loans, advances, bonusses etc). Uniform colors throughout the app. Please make a note in some file regarding the colors for easy reference in the future.

4. The group summary Column in Adjustments is a bit narrow, can we have it a bit wider?

Four independent UX polish asks — no behavioural change, no schema change, no formula change.

Goal

Tighten the visual vocabulary of the payroll area:

  1. Cleaner, shorter adjustment type labels in tables
  2. Consistent colour semantics across every payroll tab (one colour per concept, everywhere it appears)
  3. Fix a CSS bug squashing the group-summary row

All four items ship in one pass. Collectively they change how payroll LOOKS without touching how it WORKS.

Who it's for

Konrad — who sees these labels and colours dozens of times a day — and any future viewer of the payroll pages.

Scope decision — Path A: display-only rename (chosen)

Konrad's own words: "How will this handle historic data and formulas? Make sure we do not break the app." That anxiety is the exact scenario where display-only shines. Two paths were on the table:

  • A — display-only: TYPE_CHOICES second tuple value (the "human label") gets shortened; the first tuple value (the DB value) stays forever identical to today. No data migration. No constants touched. No test changes. Every historic row keeps type='New Loan' forever.
  • B — full rename w/ data migration: rename canonical value everywhere; add UPDATE payrolladjustment SET type='Loan' WHERE type='New Loan' migration; touch ~30 source files.

Path A picked. Every Konrad-visible goal is satisfied; every risk ground-sourced in historic data/formulas is eliminated.

1. Display-only rename — the mechanics

core/models.py — the only data-layer change:

TYPE_CHOICES = [
    ('Bonus',             'Bonus'),            # unchanged
    ('Overtime',          'Overtime'),         # unchanged
    ('Deduction',         'Deduction'),        # unchanged
    ('Loan Repayment',    'Loan Repayment'),   # unchanged
    ('New Loan',          'Loan'),             # DB='New Loan', shown='Loan'
    ('Advance Payment',   'Advance'),          # DB='Advance Payment', shown='Advance'
    ('Advance Repayment', 'Advance Repaid'),   # DB='Advance Repayment', shown='Advance Repaid'
]

Django's makemigrations will detect the changed choices metadata and generate a one-op AlterField migration. This is a no-op at the database level — the column type and data are untouched — but Django requires the migration to keep its model state in sync with what it thinks is on disk.

Template switches

Wherever a template currently renders the type as VISIBLE text, swap to the display method:

  • {{ adj.type }}{{ adj.get_type_display }}
  • {% if adj.type == 'New Loan' %}...{% endif %} — these stay on the raw type (DB value), because they're CONTROL FLOW, not display
  • {{ choice.0 }} / {{ choice.1 }} in dropdown iterations — already render the display value by Django convention (the <option> element uses choice.1)

CSS / data-attribute exceptions (MUST NOT change)

Two patterns keep the raw DB value to avoid breaking CSS selectors:

  • <span class="badge-type-{{ adj.type|type_slug }}"> — the type_slug filter converts the DB value "New Loan" to the slug "new-loan", which matches the .badge-type-new-loan CSS class. If we ran get_type_display here it would become badge-type-loan and break every color token.
  • <tr data-type="{{ adj.type }}"> — CSS selectors like .adj-group-header[data-type="New Loan"] and JS code like el.dataset.type === 'New Loan' both read the DB value. The data attribute is an identifier, not a label.

What doesn't change

Thing Keeps current value
DB column payrolladjustment.type ALL historic rows unchanged
ADDITIVE_TYPES / DEDUCTIVE_TYPES constants 'New Loan', 'Advance Payment', 'Advance Repayment'
Every if adj.type == 'New Loan': in views.py Unchanged
Every .badge-type-new-loan, .badge-type-advance-payment CSS class Unchanged
Every test fixture creating type='New Loan' Unchanged
type_slug template filter Unchanged
Dropdown on the "Add Adjustment" modal User sees "Loan" / "Advance" / "Advance Repaid" automatically (Django auto-uses display values in <select>)

Files touched (display-only rename)

  • core/models.py — the 3 display-label edits
  • core/migrations/0012_*.py — auto-generated no-op AlterField
  • core/templates/core/payroll_dashboard.html — swap visible {{ adj.type }} to get_type_display in the type-column cells
  • core/templates/core/_adjustment_row.html — same swap
  • Potentially core/templates/core/payslip_detail.html — if it shows the type (check during implementation)
  • Any PDF templates (report_pdf.html, payslip_pdf.html) — same visible-type check

No test changes — tests use DB value.

2. Badge colour unification

Current state: the Adjustments tab uses the semantic .badge-type-{bonus, overtime, deduction, new-loan, loan-repayment, advance-payment, advance-repayment} classes (14 CSS tokens × 7 types × 2 themes). Pending / History / Loans tabs don't — they still use Bootstrap bg-success / bg-warning / bg-danger.

Type badges (the inconsistency)

Replace the 4-branch conditional at payroll_dashboard.html:360, :369, :453:

<!-- BEFORE -->
<span class="badge {% if adj.type == 'Bonus' or adj.type == 'Overtime' or adj.type == 'Loan Repayment' or adj.type == 'Advance Repayment' %}bg-success{% elif adj.type == 'New Loan' or adj.type == 'Advance Payment' %}bg-warning{% else %}bg-danger{% endif %} adjustment-badge">
    {% 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 }}
</span>

With:

<!-- AFTER -->
<span class="badge badge-type-{{ adj.type|type_slug }} adjustment-badge">
    {% if adj.type in additive_types %}+{% else %}-{% endif %}R{{ adj.amount|floatformat:2 }}
</span>

The sign logic is preserved via additive_types context var (already threaded into _adjustment_row.html for the Adjustments tab — we'll extend the same pattern to Pending/History).

Status flag badges

Pending tab has a "With loans" yellow flag per worker — semantically "this worker HAS an active loan". Recolour to match the adjustment loan colour for consistency:

/* In custom.css — new, near the existing badge-type-* block */
.loan-flag-badge {
    background: var(--badge-loan-bg);
    color: var(--badge-loan-fg);
}

Replace the bg-warning class on the existing flag with .loan-flag-badge.

Not recoloured (deliberately):

  • "Overdue" red flag — this is a WARNING/URGENCY semantic, not loan-related. Stays on a warning red (can adopt --color-danger if we define one, but that's beyond scope here)
  • "Paid #N" green badge on row status — TRANSACTIONAL state, distinct from adjustment type. Stays green
  • "Unpaid" yellow badge on row status — same reasoning, stays yellow

The rule: colour-by-semantic-category, not by "looks nice":

  • Type-of-adjustment (Bonus, Loan, etc.) → --badge-*-bg tokens
  • Transactional state (Paid / Unpaid / Overdue) → Bootstrap state colours
  • The two categories must NOT share colours; otherwise a user glancing at a green badge can't tell if it means "this is a Bonus" or "this is Paid"

3. Wider group summary row (screenshot bug)

Root causecustom.css:1988:

.adj-group-header .adj-group-meta {
    margin-left: auto;  /* <-- only works inside a flex/grid container */
    ...
}

Written as if the parent <td> were a flex container. It isn't. Result: the meta text doesn't push right, AND when the table has a narrow wrapper, the contents wrap into a 5-character column (per Konrad's screenshot).

Fix — make the <td> an explicit flex container + prevent the meta text from wrapping mid-phrase:

.adj-group-header > td {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.adj-group-header .adj-group-meta {
    margin-left: auto;
    white-space: nowrap;
}

Two-line addition. The white-space: nowrap guards against any future narrow-viewport or scroll-wrapper regressions — the meta will either fit or overflow, never wrap into an ugly stub.

4. New file — docs/design-tokens.md

Dedicated reference for the colour palette and its intended usage.

Content outline:

# 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-<type>-bg` / `--badge-<type>-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.

## 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` |

## Where each colour appears

| Semantic | Used by |
|---|---|
| `--badge-loan-*` (Loan yellow) | Adjustments type badge; Adjustments By-Type group header left-border; Pending "With loans" worker flag |
| `--badge-advance-*` (Advance blue) | Adjustments type badge; group header border; Pending/History advance chips |
| `--badge-bonus-*` (Bonus green) | Adjustments type badge; group header border |
| ... | ... |

## How to add a new colour token

1. Define in BOTH the `:root` and `[data-theme="light"]` blocks in
   `static/css/custom.css`
2. Add a row to the mapping table in THIS doc
3. Reference via `var(--badge-*-bg)` in CSS, never hard-code hex
4. If there's a new adjustment type, add an entry to the `.badge-type-*`
   block AND the `.adj-group-header[data-type="..."]` block in the
   same file

## 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.

5. CLAUDE.md — the crucial naming-drift note

A new section near the existing "Schema name-drifts" block, titled "UI-vs-DB naming drift (Apr 2026) — READ ME BEFORE WRITING FORMULAS". Content exactly as proposed during the brainstorm — a table mapping each "what user sees" to "what DB stores", a rule for when to use which, a one-paragraph history of how we got here, and the failure symptom ("code that filters for type='Loan' returns zero rows").

6. Out of scope

  • Bootstrap state colours (bg-success/bg-warning/bg-danger) — we're not replacing those with a custom palette. Transactional state badges stay Bootstrap-default (Paid=green, Overdue=red, Unpaid=yellow). The uniformity we want is within the TYPE-of-adjustment category, not across all badges everywhere.
  • Renaming "Loan Repayment" — not requested; already short.
  • Changing the DB layer / TYPE_CHOICES canonical values — explicitly rejected in favour of Path A.
  • Touching historic CSV exports, production data imports, or any test fixture that references 'New Loan' / 'Advance Payment' / 'Advance Repayment'. All of those continue to work exactly as-is because Path A preserves DB values.

7. Risks + rollback

Risk Mitigation
Template uses adj.type in a CONTEXT that should have been display, OR vice versa Grep pass in implementation — every {{ adj.type }} gets reviewed case-by-case
CSS class / data-attribute touched accidentally Two-step grep: before any edit, confirm the `
Django migration file committed but not run on VM Explicit reminder in PR body — python3 manage.py migrate on VM (same workflow as always, migration is a no-op but must be recorded in Django's migrations table)
Colour change breaks for users with custom browser colour overrides YAGNI — not a platform we support

Rollback: git revert <sha> on any single commit. No data, schema, or URL contract impact on any change in this pass.

8. Implementation plan (short — 5 tasks)

  1. Design tokens doc — create docs/design-tokens.md first. Do this one first so the reference exists before any colour work starts; self-documenting discipline.
  2. CLAUDE.md naming-drift note — add the "UI-vs-DB naming drift" section. Locks in the mental model before code changes so it's searchable from minute one.
  3. Display-only rename — edit TYPE_CHOICES, generate migration, swap visible template references to get_type_display. Run full test suite to confirm zero regressions (expected: 69/69).
  4. Badge colour unification — swap the 4-branch {% if %} to badge-type-{{ adj.type|type_slug }} at all 3 known sites; add .loan-flag-badge class + apply to the "With loans" Pending flag. Visual check (dev server) each tab.
  5. Widen group summary row — 2-line CSS tweak. Visual check.

Expected net change: ~60-120 lines, ~4-5 commits.

9. Next step

Generate an implementation plan via the writing-plans skill (task-by-task, bite-sized steps). Then execute via subagent-driven-development. Auto mode active — proceed continuously, no mid-execution checkpoints.