Files
2026-04-15 17:57:12 +02:00

302 lines
11 KiB
Markdown

---
phase: 11-ui-enhancements
plan: "04"
type: execute
wave: 3
depends_on:
- "11-02"
- "11-03"
files_modified:
- imptune/api/pages.py
- imptune/templates/client_detail.html
- imptune/templates/partials/client_list.html
- imptune/templates/partials/printer_list.html
autonomous: true
requirements:
- UIE-03
must_haves:
truths:
- "Every client name in the printer list group headers is a clickable link to /clients/{id}"
- "Every client name in the client list table is a clickable link to /clients/{id}"
- "GET /clients/{id} returns 200 with the client name as page title and only that client's printers listed"
- "GET /clients/9999 returns 404"
- "Edit and Delete actions on /clients/{id} work the same as on /printers"
artifacts:
- path: "imptune/templates/client_detail.html"
provides: "Per-client page showing client name + filtered printer list"
min_lines: 15
- path: "imptune/api/pages.py"
provides: "GET /clients/{client_id} route"
exports: ["client_detail"]
- path: "imptune/templates/partials/client_list.html"
provides: "Client names wrapped in <a href='/clients/{c.id}'>"
contains: "/clients/"
- path: "imptune/templates/partials/printer_list.html"
provides: "Group headers with client name as <a href='/clients/{client_id}'>"
contains: "/clients/"
key_links:
- from: "imptune/templates/partials/client_list.html"
to: "/clients/{c.id}"
via: "<a href='/clients/{{ c.id }}'>{{ c.name }}</a>"
pattern: "href.*clients.*c\\.id"
- from: "imptune/templates/partials/printer_list.html"
to: "/clients/{client_id}"
via: "<a href='/clients/{id}'>{{ client_name }}</a> in group header <h3>"
pattern: "href.*clients"
- from: "imptune/api/pages.py client_detail"
to: "printer_list partial"
via: "grouped = {client.name: list(query)} passed to client_detail.html which includes printer_list"
pattern: "grouped"
---
<objective>
Make client names clickable everywhere they appear, and add the per-client printer page at GET /clients/{id}.
Purpose: UIE-03 — Users managing multiple clients need a quick way to see only one client's printers. A clickable client name in the list/headers navigates to a filtered view without any extra search UI.
Output: GET /clients/{client_id} route, client_detail.html template, updated client_list.html and printer_list.html with client name links.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/11-ui-enhancements/11-CONTEXT.md
@.planning/phases/11-ui-enhancements/11-RESEARCH.md
@.planning/phases/11-ui-enhancements/11-02-SUMMARY.md
@.planning/phases/11-ui-enhancements/11-03-SUMMARY.md
<interfaces>
<!-- Key patterns extracted from codebase. -->
From imptune/api/pages.py — existing clients_page route (for reference):
```python
@router.get("/clients", response_class=HTMLResponse)
def clients_page(request: Request):
from imptune.db.models import Client
clients = list(Client.select().order_by(Client.name))
return templates.TemplateResponse(
request=request,
name="clients.html",
context={"clients": clients},
)
```
New route to add:
```python
@router.get("/clients/{client_id}", response_class=HTMLResponse)
def client_detail(request: Request, client_id: int):
# Query client, 404 if not found
# Query printers filtered by client_id
# grouped = {client.name: list(query)} -- single-key dict for printer_list.html reuse
# driver_data for modal
# clients list for modal client dropdown
...
```
From imptune/templates/partials/printer_list.html — current group header (to update):
```html
{% for client_name, printers in grouped.items() %}
<section>
<h3>{{ client_name }}</h3>
...
```
Update to:
```html
<h3><a href="/clients/{{ client_id_map[client_name] }}">{{ client_name }}</a></h3>
```
OR: Pass grouped as dict of {client_id -> (client_name, printers)} — refactor the grouped structure.
RECOMMENDED APPROACH: Since printer_list.html receives `grouped` as {client_name: [printers]}, and `p.client` is available in each loop, extract the client_id from the first printer in the group: `{% set client_id = (printers[0].client_id if printers) %}`. This avoids changing the grouped data structure.
For "Unassigned" group: client_id will be None/empty — no link, just plain text.
From imptune/templates/partials/client_list.html — current client name cell:
```html
<td>{{ c.name }}</td>
```
Update to:
```html
<td><a href="/clients/{{ c.id }}">{{ c.name }}</a></td>
```
From tests/test_printer_crud.py — scaffolded tests (RED from Plan 01, now going GREEN):
- test_client_detail_returns_200
- test_client_detail_not_found
- test_client_links_in_printer_list
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: GET /clients/{id} route + client_detail.html template</name>
<files>imptune/api/pages.py, imptune/templates/client_detail.html</files>
<behavior>
- GET /clients/{id} (existing client with printers) returns 200, contains client name, contains printer names assigned to that client
- GET /clients/{id} (existing client with no printers) returns 200, contains client name
- GET /clients/9999 returns 404
These tests are the RED scaffolds from Plan 01 (test_client_detail_returns_200, test_client_detail_not_found)
</behavior>
<action>
**Step 1 — Add GET /clients/{client_id} route to imptune/api/pages.py:**
Add after the existing clients_page route:
```python
@router.get("/clients/{client_id}", response_class=HTMLResponse)
def client_detail(request: Request, client_id: int):
from imptune.db.models import Client, Driver, Printer
import json
client = Client.get_or_none(Client.id == client_id)
if client is None:
return HTMLResponse(
content="<h1>404 Not Found</h1><p>Client not found.</p>",
status_code=404,
)
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.where(Printer.client == client_id)
.order_by(Printer.name)
)
grouped = {client.name: list(query)}
clients = list(Client.select().order_by(Client.name))
all_drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = [
{"driver": d, "names": json.loads(d.driver_desc) if d.driver_desc else []}
for d in all_drivers
]
return templates.TemplateResponse(
request=request,
name="client_detail.html",
context={
"client": client,
"grouped": grouped,
"clients": clients,
"driver_data": driver_data,
},
)
```
**Step 2 — Create imptune/templates/client_detail.html:**
```html
{% extends "base.html" %}
{% block content %}
<h1>{{ client.name }}</h1>
<p><a href="/clients">&larr; All Clients</a></p>
<section>
<h2>Printers</h2>
{% include "partials/printer_list.html" %}
</section>
{% endblock %}
```
This template reuses printer_list.html which already handles the Edit and Delete actions (from Plan 02). The `grouped`, `clients`, and `driver_data` context vars are all passed from the route handler so the printer list and edit modals work identically to /printers.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/test_printer_crud.py -x -q -k "client_detail or client_not_found" 2>&1 | tail -10</automated>
</verify>
<done>test_client_detail_returns_200 and test_client_detail_not_found both GREEN.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Client name links in printer_list.html and client_list.html</name>
<files>
imptune/templates/partials/printer_list.html,
imptune/templates/partials/client_list.html
</files>
<behavior>
- GET /printers with a printer assigned to a client contains `href="/clients/{client_id}"` in the response HTML
- Client names in the client table are wrapped in anchor tags pointing to /clients/{id}
- "Unassigned" group header in printer list is plain text (no link — no client ID to link to)
This test is the RED scaffold from Plan 01 (test_client_links_in_printer_list)
</behavior>
<action>
**Step 1 — Update imptune/templates/partials/printer_list.html group headers:**
Change the `<h3>{{ client_name }}</h3>` to:
```html
{% set group_client_id = printers[0].client_id if printers else None %}
{% if group_client_id %}
<h3><a href="/clients/{{ group_client_id }}">{{ client_name }}</a></h3>
{% else %}
<h3>{{ client_name }}</h3>
{% endif %}
```
This extracts the client_id from the first printer in the group. For "Unassigned" (client_id=None), the condition is False and plain text is rendered.
Verify: the Jinja2 template uses `{% for client_name, printers in grouped.items() %}` — `printers` is available as the inner list, so `printers[0].client_id` is accessible. The `client_id` attribute is a Peewee FK field that returns the raw integer when accessed as `p.client_id` (not the FK object).
**Step 2 — Update imptune/templates/partials/client_list.html:**
Change:
```html
<td>{{ c.name }}</td>
```
to:
```html
<td><a href="/clients/{{ c.id }}">{{ c.name }}</a></td>
```
That's the only change needed in client_list.html.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/test_printer_crud.py -x -q -k "client_links" 2>&1 | tail -10</automated>
Also full suite: pytest tests/ -x -q --ignore=tests/e2e
</verify>
<done>
- test_client_links_in_printer_list GREEN
- Client names in client_list.html are wrapped in anchor tags
- Group headers in printer_list.html link to /clients/{id} for assigned clients
- "Unassigned" group header remains plain text
- All non-E2E tests GREEN
</done>
</task>
</tasks>
<verification>
Run full non-E2E suite:
```
cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/ -x -q --ignore=tests/e2e
```
Expected: all GREEN — all UIE-01/02/03 scaffolded tests now GREEN.
Run full E2E suite:
```
cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pytest tests/e2e/ -q
```
Expected: all E2E tests GREEN (port_autofill + printer_edit + theme_toggle + i18n_toggle).
Verify UIE-03 integration test coverage:
```
pytest tests/test_printer_crud.py -v -k "client_detail or client_not_found or client_links"
```
Expected: 3 tests GREEN.
</verification>
<success_criteria>
- GET /clients/{id} returns 200 with client name and filtered printer list
- GET /clients/9999 returns 404
- Client names in printer list group headers link to /clients/{id}
- Client names in clients table link to /clients/{id}
- "Unassigned" group header is plain text (no dead link)
- Edit and Delete actions on /clients/{id} page work via the reused printer_list partial
- All Phase 11 integration tests GREEN (UIE-01/02/03 scaffolds fully resolved)
- Full test suite (including E2E) GREEN
</success_criteria>
<output>
After completion, create `.planning/phases/11-ui-enhancements/11-04-SUMMARY.md`
</output>