docs(sprint7): пункт 5 ✓ + empty-state screenshot
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-05-30 11:21:30 +05:00
parent 8d532927e2
commit 6fc74f8db6
2 changed files with 59 additions and 1 deletions

View file

@ -16,7 +16,7 @@
- [x] **2. ConfirmDialog на destructive actions** — общий `<ConfirmDialog>` + хук `useConfirm()`. Применён к 17 страницам + ProductImageGallery. Esc=cancel, focus-on-Cancel, tone='danger'|'warning'. Org-archive уже использует Modal с confirmation-name (не трогали). 2FA UI ещё не существует в web — пропущено. Скриншот стейджа: `tests/e2e/reports/confirm-dialog-1780119970286.png`. - [x] **2. ConfirmDialog на destructive actions** — общий `<ConfirmDialog>` + хук `useConfirm()`. Применён к 17 страницам + ProductImageGallery. Esc=cancel, focus-on-Cancel, tone='danger'|'warning'. Org-archive уже использует Modal с confirmation-name (не трогали). 2FA UI ещё не существует в web — пропущено. Скриншот стейджа: `tests/e2e/reports/confirm-dialog-1780119970286.png`.
- [x] **3. Toast-система ошибок** — собственная `lib/toast.ts` + `<Toaster>`. Axios interceptor: 4xx/5xx → error toast (humanizeError() читает ProblemDetails: errors.X[0] / detail / message / title). 401 — refresh-flow, без toast. Success — глобальный mutation onSuccess (через `meta.successMessage`): useCatalogMutations + 36 мутаций на doc-edit pages. Top-right, autoclose 5s, дедуп, ручное закрытие X. Скриншот: `tests/e2e/reports/toast-error-*.png`. - [x] **3. Toast-система ошибок** — собственная `lib/toast.ts` + `<Toaster>`. Axios interceptor: 4xx/5xx → error toast (humanizeError() читает ProblemDetails: errors.X[0] / detail / message / title). 401 — refresh-flow, без toast. Success — глобальный mutation onSuccess (через `meta.successMessage`): useCatalogMutations + 36 мутаций на doc-edit pages. Top-right, autoclose 5s, дедуп, ручное закрытие X. Скриншот: `tests/e2e/reports/toast-error-*.png`.
- [x] **4. Loading skeletons**`Skeleton.tsx` экспортирует `<Skeleton variant>`, `<TableSkeleton>`, `<FormSkeleton>`. DataTable рендерит TableSkeleton при isLoading. 9 doc-edit pages + OrganizationSettingsPage показывают FormSkeleton пока тащат документ. DashboardPage график → Skeleton block. Скриншот: `tests/e2e/reports/skeleton-table-*.png`. - [x] **4. Loading skeletons**`Skeleton.tsx` экспортирует `<Skeleton variant>`, `<TableSkeleton>`, `<FormSkeleton>`. DataTable рендерит TableSkeleton при isLoading. 9 doc-edit pages + OrganizationSettingsPage показывают FormSkeleton пока тащат документ. DashboardPage график → Skeleton block. Скриншот: `tests/e2e/reports/skeleton-table-*.png`.
- [ ] **5. Empty states с CTA** — list-страницы при `items.length === 0` показывают центрированный блок с иконкой, текстом и кнопкой «Создать первый …». - [x] **5. Empty states с CTA**`EmptyState.tsx` (icon + title + description + action/secondary). Применён к 14 list-страницам (Products/Counterparties/Enters/Losses/Transfers/Inventories/Demands/SupplierReturns/Supplies/RetailSales + 4 отчёта). Показывается только когда нет поиска/фильтров. Скриншот: `tests/e2e/reports/empty-state-products-*.png`.
- [ ] **6. Breadcrumbs** — на edit-страницах Reusable `<Breadcrumbs items={...}>`. - [ ] **6. Breadcrumbs** — на edit-страницах Reusable `<Breadcrumbs items={...}>`.
- [ ] **7. Keyboard shortcuts** — edit: Ctrl+S = save, Esc = cancel/back; list: `/` = focus search, `n` = create. Hint в footer / `?` overlay. - [ ] **7. Keyboard shortcuts** — edit: Ctrl+S = save, Esc = cancel/back; list: `/` = focus search, `n` = create. Hint в footer / `?` overlay.
@ -60,3 +60,12 @@
- DashboardPage: график выручки — `Skeleton block h-72`. - DashboardPage: график выручки — `Skeleton block h-72`.
- Скриншот: `tests/e2e/reports/skeleton-table-1780121234164.png` — shimmer на /catalog/products при искусственной 3-сек задержке через page.route(). - Скриншот: `tests/e2e/reports/skeleton-table-1780121234164.png` — shimmer на /catalog/products при искусственной 3-сек задержке через page.route().
- Коммит: `faa1352 feat(web): loading skeletons`. - Коммит: `faa1352 feat(web): loading skeletons`.
### 2026-05-30 — пункт 5 ✓
- `EmptyState.tsx`: 16×16 круглая иконка из Lucide, заголовок, описание, основная кнопка + secondary link.
- 10 list-страниц + 4 reports: Products / Counterparties / Enters / Losses / Transfers / Inventories / Demands / SupplierReturns / Supplies / RetailSales + 4 отчёта (Sales / Stock / Profit / Abc).
- Тексты — объясняют что за сущность («Списания фиксируют выбытие — просрочка, брак»). CTA на навигацию `/sales/demands/new`, на Counterparties — открыть create-modal.
- Гард: показываем только когда `!search && filters.empty`. На фильтрованном результате — обычный «Нет данных».
- Скриншот: `tests/e2e/reports/empty-state-products-1780122048413.png`.
- Коммит: `8d53292 feat(web): Empty states с CTA`.

View file

@ -0,0 +1,49 @@
/**
* Sprint 7 item 5 визуально подтверждаем EmptyState на свежей организации.
* Без seed-demo, просто заходим в /catalog/products и видим «Здесь пока пусто».
*/
import { chromium } from 'playwright'
import { makeClient, login } from '../lib/api.js'
const BASE = process.env.E2E_ADMIN_URL ?? 'https://test.admin.food-market.kz'
const TS = Date.now()
const EMAIL = `empty-shot-${TS}@food-market.local`
const PASS = 'EmptyShot12345!'
async function ensureSession() {
const api = makeClient()
const r = await api.post('/api/auth/signup', {
email: EMAIL, password: PASS,
organizationName: `EmptyShot ${TS}`, phone: '+77011190001', plan: 'start',
})
if (r.status !== 200) throw new Error(`signup ${r.status}: ${JSON.stringify(r.data)}`)
return login(EMAIL, PASS)
}
async function main() {
const sess = await ensureSession()
console.log(`[shot] session ${sess.email}`)
const browser = await chromium.launch({ headless: true })
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 800 } })
const page = await ctx.newPage()
await page.goto(`${BASE}/`)
await page.evaluate(({ token }) => localStorage.setItem('fm.access_token', token), { token: sess.accessToken })
await page.goto(`${BASE}/catalog/products`, { waitUntil: 'domcontentloaded' })
await page.waitForLoadState('networkidle')
// Жде EmptyState — он содержит текст «Здесь пока пусто»
await page.waitForSelector('text=Здесь пока пусто', { timeout: 8000 })
await page.screenshot({ path: `reports/empty-state-products-${TS}.png` })
console.log(`[shot] products empty → reports/empty-state-products-${TS}.png`)
// Также проверим SuppliesPage
await page.goto(`${BASE}/purchases/supplies`, { waitUntil: 'domcontentloaded' })
await page.waitForLoadState('networkidle')
await page.waitForSelector('text=Приёмок пока нет', { timeout: 8000 })
await page.screenshot({ path: `reports/empty-state-supplies-${TS}.png` })
console.log(`[shot] supplies empty → reports/empty-state-supplies-${TS}.png`)
await browser.close()
}
main().catch(err => { console.error(err); process.exit(1) })