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>
This commit is contained in:
parent
b43892f712
commit
9aba9b8fb8
356
docs/plans/2026-04-24-ux-polish-design.md
Normal file
356
docs/plans/2026-04-24-ux-polish-design.md
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
# 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.
|
||||||
Loading…
x
Reference in New Issue
Block a user