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

357 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```python
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`:
```django
<!-- 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:
```django
<!-- 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:
```css
/* 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 cause**`custom.css:1988`:
```css
.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:
```css
.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:
```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-<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 `|type_slug` filter pattern or `data-type=` attribute is preserved |
| 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.