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