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>
15 KiB
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:
- Cleaner, shorter adjustment type labels in tables
- Consistent colour semantics across every payroll tab (one colour per concept, everywhere it appears)
- 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_CHOICESsecond 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 keepstype='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 useschoice.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 }}">— thetype_slugfilter converts the DB value"New Loan"to the slug"new-loan", which matches the.badge-type-new-loanCSS class. If we ranget_type_displayhere it would becomebadge-type-loanand break every color token.<tr data-type="{{ adj.type }}">— CSS selectors like.adj-group-header[data-type="New Loan"]and JS code likeel.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 editscore/migrations/0012_*.py— auto-generated no-op AlterFieldcore/templates/core/payroll_dashboard.html— swap visible{{ adj.type }}toget_type_displayin the type-column cellscore/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-dangerif 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-*-bgtokens - 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:
.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)
- Design tokens doc — create
docs/design-tokens.mdfirst. Do this one first so the reference exists before any colour work starts; self-documenting discipline. - 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.
- Display-only rename — edit
TYPE_CHOICES, generate migration, swap visible template references toget_type_display. Run full test suite to confirm zero regressions (expected: 69/69). - Badge colour unification — swap the 4-branch
{% if %}tobadge-type-{{ adj.type|type_slug }}at all 3 known sites; add.loan-flag-badgeclass + apply to the "With loans" Pending flag. Visual check (dev server) each tab. - 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.