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

14 KiB

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

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
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
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

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
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
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
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

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
<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
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
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
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"