ывав
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
# 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"
|
||||
```
|
||||
Reference in New Issue
Block a user