diff --git a/docs/superpowers/plans/2026-07-01-stage-1-7-ticket-experience-visual-redesign.md b/docs/superpowers/plans/2026-07-01-stage-1-7-ticket-experience-visual-redesign.md new file mode 100644 index 0000000..4c16454 --- /dev/null +++ b/docs/superpowers/plans/2026-07-01-stage-1-7-ticket-experience-visual-redesign.md @@ -0,0 +1,620 @@ +# Stage 1.7 Ticket Experience Visual Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Redesign ticket list, detail, and create pages into a polished Chinese enterprise support experience using shadcn/ui, with minimal backend support for pagination, filters, display users, and transition remarks. + +**Architecture:** Backend keeps the existing Django Ninja ticket router and adds a paginated response shape plus richer filters and display user serialization. Frontend keeps the Vite SPA and current app shell, then decomposes ticket pages into small shadcn-composed components for metrics, filters, metadata, and transition dialogs. + +**Tech Stack:** Python 3.12, Django, Django Ninja, pytest, ruff, React, Vite, TypeScript, Tailwind CSS v3, shadcn/ui, Radix UI, npm. + +--- + +## Source Spec + +- `docs/superpowers/specs/2026-07-01-stage-1-7-ticket-experience-visual-redesign-design.md` + +## File Structure + +Backend: + +- Modify: `backend/apps/tickets/schemas.py` +- Modify: `backend/apps/tickets/api.py` +- Modify: `backend/apps/tickets/services.py` +- Modify: `backend/apps/tickets/tests/test_ticket_api.py` + +Frontend: + +- Modify: `frontend/src/features/tickets/types.ts` +- Modify: `frontend/src/features/tickets/api.ts` +- Modify: `frontend/src/features/tickets/TicketListPage.tsx` +- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` +- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` +- Modify: `frontend/src/features/tickets/TicketComments.tsx` +- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx` +- Create: `frontend/src/features/tickets/TicketMetricCards.tsx` +- Create: `frontend/src/features/tickets/TicketFilterBar.tsx` +- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx` +- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx` + +## Task 1: Backend Ticket List Support + +**Files:** +- Modify: `backend/apps/tickets/schemas.py` +- Modify: `backend/apps/tickets/api.py` +- Modify: `backend/apps/tickets/services.py` +- Modify: `backend/apps/tickets/tests/test_ticket_api.py` + +- [ ] **Step 1: Write failing pagination/filter/display tests** + +Add these tests to `backend/apps/tickets/tests/test_ticket_api.py`: + +```python +from django.contrib.auth import get_user_model + + +def test_list_tickets_returns_paginated_response_with_display_users(admin_client, db): + user_model = get_user_model() + requester = user_model.objects.create_user(username="requester", first_name="请求人") + assignee = user_model.objects.create_user(username="assignee", first_name="处理人") + Ticket.objects.create( + ticket_no="TK202607010100", + title="Payment failed", + description="A", + requester=requester, + assignee=assignee, + related_system="CRM", + ) + + response = admin_client.get("/api/tickets?page=1&page_size=20") + + assert response.status_code == 200 + payload = response.json() + assert payload["page"] == 1 + assert payload["page_size"] == 20 + assert payload["total"] == 1 + assert payload["total_pages"] == 1 + assert payload["items"][0]["requester"]["display_name"] == "请求人" + assert payload["items"][0]["assignee"]["display_name"] == "处理人" + + +def test_list_tickets_filters_by_priority_related_system_and_view(admin_client, db): + user_model = get_user_model() + current_user = user_model.objects.get(username="admin") + Ticket.objects.create( + ticket_no="TK202607010101", + title="Mine urgent CRM", + description="A", + priority="urgent", + related_system="CRM", + assignee=current_user, + created_by=current_user, + ) + Ticket.objects.create( + ticket_no="TK202607010102", + title="Other medium ERP", + description="B", + priority="medium", + related_system="ERP", + ) + + response = admin_client.get( + "/api/tickets?priority=urgent&related_system=CRM&view=assigned_to_me" + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["total"] == 1 + assert payload["items"][0]["ticket_no"] == "TK202607010101" + + +def test_transition_ticket_requires_non_empty_remark(admin_client, db): + ticket = Ticket.objects.create( + ticket_no="TK202607010103", + title="A", + description="A", + status="pending", + ) + + response = admin_client.post( + f"/api/tickets/{ticket.id}/status", + data={"status": "in_progress", "remark": ""}, + content_type="application/json", + ) + + assert response.status_code == 400 +``` + +- [ ] **Step 2: Run tests to verify RED** + +Run: + +```bash +cd backend +python -m pytest apps/tickets/tests/test_ticket_api.py -v +``` + +Expected: fails because list response is currently a list, display users are missing, advanced filters are missing, and empty transition remark is allowed. + +- [ ] **Step 3: Implement schemas and serialization** + +Add schemas to `backend/apps/tickets/schemas.py`: + +```python +class TicketUserOut(Schema): + id: int + username: str + display_name: str + + +class TicketListOut(Schema): + items: list[TicketOut] + page: int + page_size: int + total: int + total_pages: int +``` + +Extend `TicketOut` with: + +```python +requester_id: int | None +requester: TicketUserOut | None = None +assignee: TicketUserOut | None = None +``` + +Implement serializer helpers in `backend/apps/tickets/api.py`: + +```python +def serialize_ticket_user(user): + if user is None: + return None + return { + "id": user.id, + "username": user.username, + "display_name": user.get_full_name() or user.username, + } + + +def serialize_ticket(ticket: Ticket) -> dict: + return { + "id": ticket.id, + "ticket_no": ticket.ticket_no, + "title": ticket.title, + "description": ticket.description, + "status": ticket.status, + "priority": ticket.priority, + "impact_scope": ticket.impact_scope, + "environment": ticket.environment, + "related_system": ticket.related_system, + "requester_id": ticket.requester_id, + "assignee_id": ticket.assignee_id, + "requester": serialize_ticket_user(ticket.requester), + "assignee": serialize_ticket_user(ticket.assignee), + "created_at": ticket.created_at, + "updated_at": ticket.updated_at, + } +``` + +- [ ] **Step 4: Implement paginated list and filters** + +Change `list_tickets` to return `TicketListOut` and accept: + +```python +page: int = 1 +page_size: int = 20 +status: str | None = None +priority: str | None = None +q: str | None = None +related_system: str | None = None +assignee_id: int | None = None +view: str = "all" +``` + +Filter rules: + +- `status`: exact status. +- `priority`: exact priority. +- `q`: `ticket_no`, `title`, or `description` contains keyword. +- `related_system`: contains text. +- `assignee_id`: exact assignee. +- `view=assigned_to_me`: `assignee=request.user`. +- `view=created_by_me`: `created_by=request.user`. +- `view=high_priority`: `priority in ["high", "urgent"]`. + +Clamp: + +```python +page = max(page, 1) +page_size = min(max(page_size, 1), 100) +``` + +- [ ] **Step 5: Require transition remarks** + +In `update_ticket_status`, reject blank remarks: + +```python +if not payload.remark.strip(): + raise HttpError(400, "Transition remark is required") +``` + +Import `HttpError` from `ninja.errors`. + +- [ ] **Step 6: Update existing tests for new list shape** + +Change existing list assertions in `test_list_tickets_filters_by_status`: + +```python +payload = response.json() +assert payload["total"] == 1 +assert payload["items"][0]["ticket_no"] == "TK202606300001" +``` + +- [ ] **Step 7: Verify GREEN** + +Run: + +```bash +cd backend +python -m pytest apps/tickets/tests/test_ticket_api.py apps/tickets/tests/test_ticket_export.py apps/accounts/tests/test_permission_enforcement.py -v +python -m ruff check . +``` + +Expected: all selected tests pass and ruff has no errors. + +- [ ] **Step 8: Commit** + +```bash +git add backend/apps/tickets +git commit -m "feat: add ticket list visual support api" +``` + +## Task 2: Frontend Ticket API And List Redesign + +**Files:** +- Modify: `frontend/src/features/tickets/types.ts` +- Modify: `frontend/src/features/tickets/api.ts` +- Modify: `frontend/src/features/tickets/TicketListPage.tsx` +- Create: `frontend/src/features/tickets/TicketMetricCards.tsx` +- Create: `frontend/src/features/tickets/TicketFilterBar.tsx` + +- [ ] **Step 1: Refresh shadcn docs** + +Run: + +```bash +cd frontend +npx shadcn@latest docs tabs table card badge select input pagination skeleton sonner +``` + +Use installed components first. Do not overwrite existing locally patched components. + +- [ ] **Step 2: Update ticket frontend types** + +In `frontend/src/features/tickets/types.ts`, add: + +```ts +export type TicketUser = { + id: number + username: string + display_name: string +} + +export type TicketListResponse = { + items: Ticket[] + page: number + page_size: number + total: number + total_pages: number +} +``` + +Extend `Ticket`: + +```ts +requester_id: number | null +requester: TicketUser | null +assignee: TicketUser | null +``` + +Add list query type: + +```ts +export type TicketListParams = { + page?: number + page_size?: number + status?: string + priority?: string + q?: string + related_system?: string + assignee_id?: number + view?: 'all' | 'assigned_to_me' | 'created_by_me' | 'high_priority' +} +``` + +- [ ] **Step 3: Update ticket API** + +In `frontend/src/features/tickets/api.ts`, update `listTickets` to return `TicketListResponse` and include all query fields: + +```ts +export function listTickets(params: TicketListParams = {}) { + const query = new URLSearchParams() + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + query.set(key, String(value)) + } + }) + const suffix = query.toString() ? `?${query}` : '' + return apiRequest(`/tickets${suffix}`) +} +``` + +- [ ] **Step 4: Create TicketMetricCards** + +Create `frontend/src/features/tickets/TicketMetricCards.tsx` with compact shadcn `Card` metrics for: + +```text +待处理 +处理中 +待确认 +高优先级 +``` + +Props: + +```ts +type TicketMetricCardsProps = { + tickets: Ticket[] +} +``` + +- [ ] **Step 5: Create TicketFilterBar** + +Create `frontend/src/features/tickets/TicketFilterBar.tsx` using `AppFilterBar`, `Input`, `Select`, and `Button`. + +Props: + +```ts +type TicketFilterBarProps = { + q: string + status: string + priority: string + relatedSystem: string + onQChange: (value: string) => void + onStatusChange: (value: string) => void + onPriorityChange: (value: string) => void + onRelatedSystemChange: (value: string) => void + onReset: () => void +} +``` + +- [ ] **Step 6: Redesign TicketListPage** + +Update `TicketListPage`: + +- Use shadcn `Tabs` for `全部工单`, `我的待办`, `我创建的`, `高优先级`. +- Use `TicketMetricCards`. +- Use `TicketFilterBar`. +- Use backend pagination fields. +- Use `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationPrevious`, `PaginationNext`. +- Table columns must be Chinese and include requester/assignee display when available. +- Empty/error/loading states must be Chinese. + +- [ ] **Step 7: Verify frontend GREEN** + +Run: + +```bash +cd frontend +npm run typecheck +npm run build +``` + +Expected: both pass. + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/features/tickets +git commit -m "feat: redesign ticket list experience" +``` + +## Task 3: Frontend Ticket Detail Redesign + +**Files:** +- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx` +- Modify: `frontend/src/features/tickets/TicketComments.tsx` +- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx` +- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx` +- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx` + +- [ ] **Step 1: Refresh shadcn docs** + +Run: + +```bash +cd frontend +npx shadcn@latest docs dialog textarea card badge separator tabs sonner +``` + +- [ ] **Step 2: Create TicketTransitionDialog** + +Create a dialog component with: + +```ts +type TicketTransitionDialogProps = { + open: boolean + status: TicketStatus | null + onOpenChange: (open: boolean) => void + onSubmit: (status: TicketStatus, remark: string) => Promise +} +``` + +Visible text: + +```text +处理说明 +请说明本次流转的原因、处理动作或待确认事项。 +取消 +确认提交 +``` + +The submit button is disabled until `remark.trim()` is non-empty. + +- [ ] **Step 3: Create TicketMetaPanel** + +Create `TicketMetaPanel` with shadcn `Card`, `Badge`, `Separator`, `AppField`, `AppStatusBadge`, and `AppPriorityBadge`. + +Display: + +```text +当前状态 +优先级 +处理人 +请求人 +关联系统 +运行环境 +影响范围 +``` + +- [ ] **Step 4: Update TicketStatusTimeline** + +Make every event label Chinese: + +```ts +const EVENT_LABELS: Record = { + created: '创建工单', + updated: '更新工单', + status_change: '状态变更', + commented: '新增评论', + attachment_added: '新增附件', +} +``` + +- [ ] **Step 5: Update TicketComments** + +Use Chinese labels and distinguish: + +```text +内部备注 +用户可见回复 +``` + +Use shadcn `Tabs` or `Select` to choose visibility. Existing API can still receive: + +```ts +{ comment_type: 'internal_note' | 'user_reply', visibility: 'internal' | 'public' } +``` + +- [ ] **Step 6: Redesign TicketDetailPage** + +Update page: + +- Loading title: `正在加载工单` +- Error title: `无法加载工单` +- Header actions open `TicketTransitionDialog`. +- Main column contains `问题描述`, `处理记录`, `评论与协作`. +- Side column uses `TicketMetaPanel`, `SolutionLinkPanel`, and restrained AI placeholder. + +- [ ] **Step 7: Verify frontend GREEN** + +Run: + +```bash +cd frontend +npm run typecheck +npm run build +``` + +Expected: both pass. + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/features/tickets +git commit -m "feat: redesign ticket detail experience" +``` + +## Task 4: Frontend Ticket Create Redesign And Final Verification + +**Files:** +- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` + +- [ ] **Step 1: Refresh shadcn docs** + +Run: + +```bash +cd frontend +npx shadcn@latest docs card input select textarea badge alert +``` + +- [ ] **Step 2: Redesign create page** + +Update `TicketCreatePage` into grouped sections: + +```text +基础信息 +影响与优先级 +环境与系统 +问题描述 +提交确认 +``` + +Visible copy must be Chinese. Use `Card`, `Input`, `Select`, `Textarea`, `Badge`, and `Button`. + +- [ ] **Step 3: Use Chinese error feedback** + +On create failure, show: + +```text +创建工单失败,请检查必填信息或稍后重试。 +``` + +- [ ] **Step 4: Run full verification** + +Run: + +```bash +cd backend +python -m pytest apps/tickets/tests apps/accounts/tests/test_permission_enforcement.py -v +python -m ruff check . + +cd ../frontend +npm run typecheck +npm run build +npx shadcn@latest info +``` + +Expected: all commands pass. + +- [ ] **Step 5: Browser visual smoke test** + +With local backend/frontend running: + +- Login as seeded admin. +- Open `#/tickets` and verify Chinese tabs, metrics, filters, table, and pagination. +- Open a ticket detail page and verify transition dialog requires handling note. +- Open `#/tickets/new` and verify grouped create form has no obvious overlap. + +- [ ] **Step 6: Commit** + +```bash +git add frontend/src/features/tickets +git commit -m "feat: redesign ticket create experience" +``` + +## Final Review Checklist + +- [ ] Ticket list uses shadcn tabs, cards, table, filters, and pagination. +- [ ] Ticket detail uses a Chinese transition dialog and no English loading/error copy. +- [ ] Ticket comments distinguish internal notes and user-visible replies. +- [ ] Ticket create page uses grouped Chinese form sections. +- [ ] Backend ticket list supports pagination and visual filters. +- [ ] Ticket responses include requester and assignee display fields. +- [ ] Empty transition remarks are rejected. +- [ ] Backend ticket tests pass. +- [ ] Backend ruff passes. +- [ ] Frontend typecheck passes. +- [ ] Frontend build passes. +- [ ] shadcn info still reports Vite/Tailwind v3 and installed components.