Files
shop-server/docs/superpowers/plans/2026-05-28-admin-orders-improvements.md
T
2026-05-28 21:20:35 +05:00

447 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Admin Orders UX Improvements 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:** Добавить в списке заказов маркер `Цена не подтверждена` и заменить смену статуса в деталке на быстрые кнопки допустимых переходов.
**Architecture:** Изменения ограничены фронтендом (`client`) и опираются на текущие поля заказа (`status`, `deliveryType`, `deliveryFeeLocked`) и текущую логику переходов `getAdminNextOrderStatuses`. Серверные API и контракты не меняются, синхронизация данных остается через существующую инвалидацию React Query.
**Tech Stack:** React, TypeScript, MUI, TanStack React Query, Vitest, Testing Library.
---
## File Structure
**Create:**
- `client/src/shared/lib/order-requires-price-approval.ts`
- `client/src/shared/lib/__tests__/order-requires-price-approval.test.ts`
- `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`
- `client/src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx`
**Modify:**
- `client/src/pages/admin-orders/ui/AdminOrdersPage.tsx`
- `client/src/features/order-detail/ui/OrderDetailContent.tsx`
---
### Task 1: Вычисление признака "цена не подтверждена"
**Files:**
- Create: `client/src/shared/lib/order-requires-price-approval.ts`
- Test: `client/src/shared/lib/__tests__/order-requires-price-approval.test.ts`
- [ ] **Step 1: Write the failing unit test**
```ts
import { describe, expect, it } from 'vitest'
import { orderRequiresPriceApproval } from '../order-requires-price-approval'
describe('orderRequiresPriceApproval', () => {
it('returns true for delivery pending payment with unlocked delivery fee', () => {
expect(
orderRequiresPriceApproval({
status: 'PENDING_PAYMENT',
deliveryType: 'delivery',
deliveryFeeLocked: false,
}),
).toBe(true)
})
it('returns false when delivery fee is already locked', () => {
expect(
orderRequiresPriceApproval({
status: 'PENDING_PAYMENT',
deliveryType: 'delivery',
deliveryFeeLocked: true,
}),
).toBe(false)
})
it('returns false for pickup even if payment is pending', () => {
expect(
orderRequiresPriceApproval({
status: 'PENDING_PAYMENT',
deliveryType: 'pickup',
deliveryFeeLocked: false,
}),
).toBe(false)
})
it('returns false for non-pending statuses', () => {
expect(
orderRequiresPriceApproval({
status: 'PAID',
deliveryType: 'delivery',
deliveryFeeLocked: false,
}),
).toBe(false)
})
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd client && npm test -- src/shared/lib/__tests__/order-requires-price-approval.test.ts`
Expected: FAIL with module/function not found.
- [ ] **Step 3: Write minimal implementation**
```ts
type PriceApprovalOrder = {
status: string
deliveryType: 'delivery' | 'pickup'
deliveryFeeLocked: boolean
}
export function orderRequiresPriceApproval(order: PriceApprovalOrder): boolean {
return order.status === 'PENDING_PAYMENT' && order.deliveryType === 'delivery' && order.deliveryFeeLocked === false
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd client && npm test -- src/shared/lib/__tests__/order-requires-price-approval.test.ts`
Expected: PASS (4 tests).
- [ ] **Step 5: Commit**
```bash
git add client/src/shared/lib/order-requires-price-approval.ts client/src/shared/lib/__tests__/order-requires-price-approval.test.ts
git commit -m "test: add price approval predicate for admin orders"
```
---
### Task 2: Маркер "Цена не подтверждена" в списке заказов
**Files:**
- Modify: `client/src/pages/admin-orders/ui/AdminOrdersPage.tsx`
- Test: `client/src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx`
- [ ] **Step 1: Write the failing component test for chip visibility**
```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { AdminOrdersPage } from '../AdminOrdersPage'
const fetchAdminOrdersMock = vi.fn()
vi.mock('@/entities/order/api/admin-order-api', () => ({
fetchAdminOrders: fetchAdminOrdersMock,
fetchAdminOrder: vi.fn(),
}))
describe('AdminOrdersPage price approval marker', () => {
it('shows "Цена не подтверждена" for eligible order', async () => {
fetchAdminOrdersMock.mockResolvedValueOnce({
items: [
{
id: 'order-1',
status: 'PENDING_PAYMENT',
deliveryType: 'delivery',
deliveryFeeLocked: false,
totalCents: 10000,
currency: 'RUB',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
user: { id: 'u1', email: 'a@example.com' },
itemsCount: 1,
},
],
total: 1,
page: 1,
pageSize: 20,
})
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<AdminOrdersPage />
</QueryClientProvider>,
)
expect(await screen.findByText('Цена не подтверждена')).toBeInTheDocument()
})
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd client && npm test -- src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx`
Expected: FAIL because chip text is not rendered yet.
- [ ] **Step 3: Implement marker in `AdminOrdersPage`**
```tsx
import Chip from '@mui/material/Chip'
import { orderRequiresPriceApproval } from '@/shared/lib/order-requires-price-approval'
// ...
{group.items.map((o) => {
const needsPriceApproval = orderRequiresPriceApproval({
status: o.status,
deliveryType: o.deliveryType,
deliveryFeeLocked: o.deliveryFeeLocked,
})
return (
<TableRow key={o.id} hover>
<TableCell>
<Stack direction="row" spacing={1} alignItems="center">
<Box component="span">{o.id.slice(-8)}</Box>
{needsPriceApproval && <Chip size="small" color="warning" label="Цена не подтверждена" />}
</Stack>
</TableCell>
{/* ... */}
</TableRow>
)
})}
```
- [ ] **Step 4: Add negative case and rerun test**
```tsx
it('does not show marker for non-eligible order', async () => {
fetchAdminOrdersMock.mockResolvedValueOnce({
items: [
{
id: 'order-2',
status: 'PENDING_PAYMENT',
deliveryType: 'delivery',
deliveryFeeLocked: true,
totalCents: 10000,
currency: 'RUB',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
user: { id: 'u2', email: 'b@example.com' },
itemsCount: 2,
},
],
total: 1,
page: 1,
pageSize: 20,
})
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<AdminOrdersPage />
</QueryClientProvider>,
)
expect(await screen.findByText('order-2'.slice(-8))).toBeInTheDocument()
expect(screen.queryByText('Цена не подтверждена')).not.toBeInTheDocument()
})
```
Run: `cd client && npm test -- src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add client/src/pages/admin-orders/ui/AdminOrdersPage.tsx client/src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx
git commit -m "feat: show price approval marker in admin orders list"
```
---
### Task 3: Быстрые кнопки смены статуса в деталке
**Files:**
- Modify: `client/src/features/order-detail/ui/OrderDetailContent.tsx`
- Test: `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`
- [ ] **Step 1: Write failing tests for quick actions**
```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { OrderDetailContent } from '../OrderDetailContent'
import type { AdminOrderDetailResponse } from '@/entities/order/api/admin-order-api'
const setAdminOrderStatusMock = vi.fn(async () => undefined)
vi.mock('@/entities/order/api/admin-order-api', async () => {
const actual = await vi.importActual<object>('@/entities/order/api/admin-order-api')
return {
...actual,
setAdminOrderStatus: setAdminOrderStatusMock,
postAdminOrderMessage: vi.fn(async () => undefined),
}
})
function buildDetail(patch: Partial<AdminOrderDetailResponse['item']>): AdminOrderDetailResponse['item'] {
return {
id: 'o1',
status: 'PENDING_PAYMENT',
deliveryType: 'delivery',
deliveryCarrier: null,
paymentMethod: 'online',
itemsSubtotalCents: 10000,
deliveryFeeCents: 500,
deliveryFeeLocked: false,
totalCents: 10500,
currency: 'RUB',
addressSnapshotJson: null,
comment: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
user: {
id: 'u1',
email: 'a@example.com',
displayName: null,
avatar: null,
avatarStyle: null,
},
items: [],
messages: [],
...patch,
}
}
describe('OrderDetailContent quick status actions', () => {
it('renders quick action buttons for next statuses', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<OrderDetailContent detail={buildDetail({ status: 'PENDING_PAYMENT', deliveryType: 'delivery' })} orderId="o1" />
</QueryClientProvider>,
)
expect(screen.getByRole('button', { name: /Оплачен/i })).toBeInTheDocument()
})
it('calls setAdminOrderStatus on click', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<OrderDetailContent detail={buildDetail({ status: 'PENDING_PAYMENT', deliveryType: 'delivery' })} orderId="o1" />
</QueryClientProvider>,
)
fireEvent.click(screen.getByRole('button', { name: /Оплачен/i }))
expect(setAdminOrderStatusMock).toHaveBeenCalledWith('o1', 'PAID')
})
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd client && npm test -- src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`
Expected: FAIL because old `Select` UI is still used.
- [ ] **Step 3: Replace select with quick action buttons**
```tsx
<Stack spacing={1}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
Быстрый переход статуса
</Typography>
{nextStatuses.length === 0 ? (
<Typography variant="body2" color="text.secondary">
Статус финальный, смена недоступна
</Typography>
) : (
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>
{nextStatuses.map((nextStatus) => {
const isCancel = nextStatus === 'CANCELLED'
return (
<Button
key={nextStatus}
variant={isCancel ? 'outlined' : 'contained'}
color={isCancel ? 'error' : 'primary'}
onClick={() => statusMut.mutate(nextStatus)}
disabled={statusMut.isPending}
>
{ORDER_STATUS_MAP[nextStatus] ?? nextStatus}
</Button>
)
})}
</Stack>
)}
</Stack>
```
- [ ] **Step 4: Add pending/empty-state assertions and rerun tests**
```tsx
it('disables all quick action buttons while mutation is pending', () => {
const qc = new QueryClient()
setAdminOrderStatusMock.mockImplementationOnce(() => new Promise(() => {}))
render(
<QueryClientProvider client={qc}>
<OrderDetailContent detail={buildDetail({ status: 'PENDING_PAYMENT', deliveryType: 'delivery' })} orderId="o1" />
</QueryClientProvider>,
)
const paidButton = screen.getByRole('button', { name: /Оплачен/i })
fireEvent.click(paidButton)
expect(paidButton).toBeDisabled()
})
it('shows final state note when no transitions available', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<OrderDetailContent detail={buildDetail({ status: 'DONE', deliveryType: 'delivery' })} orderId="o1" />
</QueryClientProvider>,
)
expect(screen.getByText('Статус финальный, смена недоступна')).toBeInTheDocument()
})
```
Run: `cd client && npm test -- src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add client/src/features/order-detail/ui/OrderDetailContent.tsx client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx
git commit -m "feat: replace admin order status select with quick actions"
```
---
### Task 4: Регрессия, линт и финальная проверка
**Files:**
- Modify (if needed): `client/src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx`
- Modify (if needed): `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`
- Modify (if needed): `client/src/shared/lib/__tests__/order-requires-price-approval.test.ts`
- [ ] **Step 1: Run focused test suite for changed units**
Run:
`cd client && npm test -- src/shared/lib/__tests__/order-requires-price-approval.test.ts src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`
Expected: PASS.
- [ ] **Step 2: Run frontend lint**
Run: `cd client && npm run lint`
Expected: PASS with no new lint errors.
- [ ] **Step 3: Run format check**
Run: `cd client && npm run format:check`
Expected: PASS, no formatting violations.
- [ ] **Step 4: Fix issues if any and re-run exact failed command**
Run (example): `cd client && npm run lint`
Expected: PASS after fixes.
- [ ] **Step 5: Final commit**
```bash
git add client/src/pages/admin-orders/ui/AdminOrdersPage.tsx client/src/features/order-detail/ui/OrderDetailContent.tsx client/src/shared/lib/order-requires-price-approval.ts client/src/shared/lib/__tests__/order-requires-price-approval.test.ts client/src/pages/admin-orders/ui/__tests__/AdminOrdersPage.test.tsx client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx
git commit -m "feat: improve admin orders flow for price approval and status updates"
```