diff --git a/docs/stock-filter-2026-10-11/REPORT.md b/docs/stock-filter-2026-10-11/REPORT.md new file mode 100644 index 0000000..ed02b2d --- /dev/null +++ b/docs/stock-filter-2026-10-11/REPORT.md @@ -0,0 +1,78 @@ +# Фильтры остатков «В наличии / Нулевые / В минусе» (задача 11, 11.10.2026) + +Галочки состояния остатка объединяются (ИЛИ); группа/склад/поиск сужают (И). +Пусто → positive+negative (нулевые скрыты). + +## Что сделано +- **API** `StockController` (`/api/inventory/stock` и `/stock/export`): параметр + `states` = набор `positive|zero|negative` (через запятую или повтором). Один WHERE: + `(positive И qty>0) ИЛИ (zero И qty=0) ИЛИ (negative И qty<0)` по остатку + (COALESCE-подзапрос; товар без строки Stock = 0). total/пагинация/сортировка + корректны. Старые `includeZero`/`negativeOnly` оставлены для совместимости + (`includeZero` ≡ все три; `negativeOnly` ≡ negative); `states` приоритетнее. +- **Веб-админка** `StockPage`: три галочки «В наличии» (>0), «Нулевые» (=0), + «В минусе» (<0); состояние в URL (`?states=…`); экспорт учитывает галочки. + Ссылки `NegativeStockBanner`/`OverdueNegativeStockBlock`/`NotificationCenter` + на минусовые → `states=negative`. +- **`StockReportPage`** НЕ меняли: это отдельный отчёт по остаткам на дату с одной + галочкой «Показывать нулевые остатки» (другой эндпоинт), три состояния к нему + не относятся. +- Деплой на **stage** (`test.admin.food-market.kz`). Прод не трогали. + +## Проверка + +### 1. Интеграционный тест (эндпоинт) — 1/1 зелёный +`tests/food-market.IntegrationTests/StockStateFilterTests.cs`: сочетания +(zero; negative; positive; zero+negative; +группа; все три = весь каталог; +пусто = positive+negative; со складом) и числовые тождества +(positive+negative = пусто; positive+zero+negative = весь каталог) + совместимость +старых флагов. EF8 переводит предикат (в логе SQL `COALESCE((…SUM…),0) < 0` и т.п.). + +### 2. SQL по реальному датасету МойСклад (org «FOOD MARKET», foodmarket-migr) +org id `eb5db1ae-57c1-4f32-a646-13807d49de54`, аккаунт +`foodmarket-migr@food-market.local`. Та же агрегатная логика, что и в контроллере +(`COALESCE(SUM(Quantity),0)`, `IsService=false AND IsArchived=false`): + +| состояние | факт (SQL) | ожидание владельца | +|---|---|---| +| В наличии (>0) | **4746** | ~4752 | +| Нулевые (=0) | **23418** | ~23415 (МойСклад «Доступно: Нулевое» 23 419) | +| В минусе (<0) | **2829** | ~2826 | +| Всего | **30993** | 30 993 | + +Тождества: 4746 + 2829 = **7575** (по умолчанию) ; 4746 + 23418 + 2829 = **30993** +(весь каталог). Небольшие расхождения с ожиданием — живые продажи/синхронизация +(как и отмечено в задаче). + +**Группа + зеро/минус:** группа «Кондитерские изделия и снеки» +(`eacc1fc2-1734-4dcd-a18c-4441d5cc6303`, с 2 подгруппами): zero+negative = **3848** +(нулевые 3510, минусовые 338) — проверено SQL (с учётом подгрупп по Path, как в +`DescendantGroupIdsAsync`). + +### 3. Живой stage-API (деплой) на сид-данных +Свежий стенд-аккаунт (обычный публичный signup), 3 товара (в наличии/нулевой/минус): +`default(pos+neg)=2 · positive=1 · zero=1 · negative=1 · zero,negative=2 · all=3` — +ровно по правилу. + +### 4. Скриншоты реальной админки (test.admin) +`s1-default.png` … `s6-all.png` + `index.html` — на том же стенд-аккаунте: +- по умолчанию (ни одна галочка) → 2 (в наличии + в минусе), URL без `states`; +- «В наличии» → 1 (`?states=positive`); «Нулевые» → 1 (`?states=zero`); + «В минусе» → 1 (`?states=negative`); +- «Нулевые»+«В минусе» → 2 (`?states=zero,negative`) — см. скриншот + (товар −3 и товар 0); +- все три → 3 (весь каталог, `?states=positive,zero,negative`). +Состояние галочек отражается в URL (ссылку можно скопировать). + +## Что НЕ сделано / нужно решение владельца +- **Скриншот именно org «FOOD MARKET» в UI** не снят: сохранённый пароль + SuperAdmin стенда (`~/.fm-watchdog/stage-superadmin-password`) не подходит + (`invalid_grant`), а сбрасывать пароль аккаунта `foodmarket-migr` я не стал — + это операция над чужими учётными данными (авто-режим её и заблокировал). Числа + этой org проверены SQL (таблица выше) и эндпоинт — интеграционным тестом; UI-поведение + снято на идентичном коде (свежий стенд-аккаунт). Если нужны скриншоты именно + в org «FOOD MARKET» — пришлите рабочий пароль SuperAdmin стенда или аккаунта + `foodmarket-migr`. +- **iOS-клиент ALL MARKET Staff** (три галочки) — ваша часть (п. 6): переделываете + вы и выкладываете в TestFlight. Серверный контракт (`states`) готов и на stage. +- **Прод** не трогали (по правилу — без слова владельца). diff --git a/docs/stock-filter-2026-10-11/index.html b/docs/stock-filter-2026-10-11/index.html new file mode 100644 index 0000000..93292a4 --- /dev/null +++ b/docs/stock-filter-2026-10-11/index.html @@ -0,0 +1,10 @@ +Фильтры остатков 2026-10-11 + +

Фильтры остатков «В наличии / Нулевые / В минусе» — админка test.admin

По умолчанию (ни одна галочка — в наличии + в минусе) — «2 позиций по всем складам»
https://test.admin.food-market.kz/inventory/stock
+
Только «В наличии» — «1 позиций по всем складам»
https://test.admin.food-market.kz/inventory/stock?states=positive
+
Только «Нулевые» — «1 позиций по всем складам»
https://test.admin.food-market.kz/inventory/stock?states=zero
+
Только «В минусе» — «1 позиций по всем складам»
https://test.admin.food-market.kz/inventory/stock?states=negative
+
«Нулевые» + «В минусе» (объединение) — «2 позиций по всем складам»
https://test.admin.food-market.kz/inventory/stock?states=zero%2Cnegative
+
Все три = весь каталог — «3 позиций по всем складам»
https://test.admin.food-market.kz/inventory/stock?states=positive%2Czero%2Cnegative
\ No newline at end of file diff --git a/docs/stock-filter-2026-10-11/s1-default.png b/docs/stock-filter-2026-10-11/s1-default.png new file mode 100644 index 0000000..aec766d Binary files /dev/null and b/docs/stock-filter-2026-10-11/s1-default.png differ diff --git a/docs/stock-filter-2026-10-11/s2-positive.png b/docs/stock-filter-2026-10-11/s2-positive.png new file mode 100644 index 0000000..5f76792 Binary files /dev/null and b/docs/stock-filter-2026-10-11/s2-positive.png differ diff --git a/docs/stock-filter-2026-10-11/s3-zero.png b/docs/stock-filter-2026-10-11/s3-zero.png new file mode 100644 index 0000000..ad5a448 Binary files /dev/null and b/docs/stock-filter-2026-10-11/s3-zero.png differ diff --git a/docs/stock-filter-2026-10-11/s4-negative.png b/docs/stock-filter-2026-10-11/s4-negative.png new file mode 100644 index 0000000..bb3e42d Binary files /dev/null and b/docs/stock-filter-2026-10-11/s4-negative.png differ diff --git a/docs/stock-filter-2026-10-11/s5-zero-negative.png b/docs/stock-filter-2026-10-11/s5-zero-negative.png new file mode 100644 index 0000000..f823147 Binary files /dev/null and b/docs/stock-filter-2026-10-11/s5-zero-negative.png differ diff --git a/docs/stock-filter-2026-10-11/s6-all.png b/docs/stock-filter-2026-10-11/s6-all.png new file mode 100644 index 0000000..4677aed Binary files /dev/null and b/docs/stock-filter-2026-10-11/s6-all.png differ diff --git a/docs/stock-filter-2026-10-11/ui-results.json b/docs/stock-filter-2026-10-11/ui-results.json new file mode 100644 index 0000000..86129ec --- /dev/null +++ b/docs/stock-filter-2026-10-11/ui-results.json @@ -0,0 +1,32 @@ +[ + { + "caption": "По умолчанию (ни одна галочка — в наличии + в минусе)", + "count": "2 позиций по всем складам", + "url": "https://test.admin.food-market.kz/inventory/stock" + }, + { + "caption": "Только «В наличии»", + "count": "1 позиций по всем складам", + "url": "https://test.admin.food-market.kz/inventory/stock?states=positive" + }, + { + "caption": "Только «Нулевые»", + "count": "1 позиций по всем складам", + "url": "https://test.admin.food-market.kz/inventory/stock?states=zero" + }, + { + "caption": "Только «В минусе»", + "count": "1 позиций по всем складам", + "url": "https://test.admin.food-market.kz/inventory/stock?states=negative" + }, + { + "caption": "«Нулевые» + «В минусе» (объединение)", + "count": "2 позиций по всем складам", + "url": "https://test.admin.food-market.kz/inventory/stock?states=zero%2Cnegative" + }, + { + "caption": "Все три = весь каталог", + "count": "3 позиций по всем складам", + "url": "https://test.admin.food-market.kz/inventory/stock?states=positive%2Czero%2Cnegative" + } +] \ No newline at end of file diff --git a/tests/e2e/tools/stock-filter-verify.mjs b/tests/e2e/tools/stock-filter-verify.mjs new file mode 100644 index 0000000..6572011 --- /dev/null +++ b/tests/e2e/tools/stock-filter-verify.mjs @@ -0,0 +1,66 @@ +// Скриншоты фильтров остатков «В наличии / Нулевые / В минусе» на реальной +// админке test.admin.food-market.kz (свежий стенд-аккаунт). Вывод — docs/stock-filter-2026-10-11/. +import { chromium } from '@playwright/test' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const BASE = process.env.ADMIN_URL ?? 'https://test.admin.food-market.kz' +const OUT = resolve('/home/nns/food-market', 'docs/stock-filter-2026-10-11') +mkdirSync(OUT, { recursive: true }) +const email = readFileSync('/tmp/stockui-email.txt', 'utf8').trim() +const password = readFileSync('/tmp/stockui-pw.txt', 'utf8').trim() +const shots = [], results = [] + +const b = await chromium.launch({ args: ['--ignore-certificate-errors'] }) +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, ignoreHTTPSErrors: true }) +const p = await ctx.newPage() + +// Логин +await p.goto(BASE + '/', { waitUntil: 'networkidle' }) +await p.locator('input[type="email"]').fill(email) +await p.locator('input[type="password"]').fill(password) +await p.getByRole('button', { name: /Войти/ }).click().catch(async () => { await p.locator('button[type="submit"]').click() }) +await p.waitForTimeout(2500) + +await p.goto(BASE + '/inventory/stock', { waitUntil: 'networkidle' }) +await p.waitForTimeout(1500) + +const boxes = p.getByRole('checkbox') +async function ensure(pos, zero, neg) { + const want = [pos, zero, neg] + for (let i = 0; i < 3; i++) { + const cur = await boxes.nth(i).isChecked() + if (cur !== want[i]) await boxes.nth(i).click() + } + await p.waitForTimeout(1200) +} +async function countText() { + return (await p.locator('text=/позиц/').first().innerText().catch(() => '')) || '' +} +async function step(file, caption, pos, zero, neg) { + await ensure(pos, zero, neg) + const txt = await countText() + const url = p.url() + await p.screenshot({ path: resolve(OUT, file) }) + shots.push({ file, caption: `${caption} — «${txt.trim()}»`, url }) + results.push({ caption, count: txt.trim(), url }) + console.log(`${caption}: ${txt.trim()} [${url}]`) +} + +await step('s1-default.png', 'По умолчанию (ни одна галочка — в наличии + в минусе)', false, false, false) +await step('s2-positive.png', 'Только «В наличии»', true, false, false) +await step('s3-zero.png', 'Только «Нулевые»', false, true, false) +await step('s4-negative.png', 'Только «В минусе»', false, false, true) +await step('s5-zero-negative.png', '«Нулевые» + «В минусе» (объединение)', false, true, true) +await step('s6-all.png', 'Все три = весь каталог', true, true, true) + +await b.close() + +const rows = shots.map((s) => `
${s.caption}
${s.url}
`).join('\n') +writeFileSync(resolve(OUT, 'index.html'), `Фильтры остатков 2026-10-11 + +

Фильтры остатков «В наличии / Нулевые / В минусе» — админка test.admin

${rows}`) +writeFileSync(resolve(OUT, 'ui-results.json'), JSON.stringify(results, null, 2)) +console.log(`\n[stock-filter-ui] ${shots.length} скриншотов → ${OUT}`)