Plan 4 plans across 3 waves covering UIE-01..05: form separation (Plan 01), printer edit modal (Plan 02), theme+i18n toggles (Plan 03), and client detail page (Plan 04). Wave 0 test scaffolds included in Plan 01. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
302 lines
11 KiB
Markdown
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">← 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>
|