diff --git a/deploy/showcase-seed.sh b/deploy/showcase-seed.sh index e93966a..0c2747b 100755 --- a/deploy/showcase-seed.sh +++ b/deploy/showcase-seed.sh @@ -25,6 +25,37 @@ j() { python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(sys.argv[1] echo "→ стенд: $BASE организация: «$ORGNAME» владелец: $EMAIL" +# 0. (опц.) RESET=1 — архивировать прежнюю витринную орг. через SuperAdmin, чтобы +# пересоздать её с нуля (signup реактивирует владельца с новой организацией). +# Только стенд: мастер-пароль быстрого входа работает лишь при QuickLogin и не-прод Issuer. +if [[ "${RESET:-0}" == "1" ]]; then + SA_EMAIL="${SUPERADMIN_EMAIL:-admin@food-market.local}" + SA_PASS="${SUPERADMIN_PASSWORD:-__QUICK_LOGIN_PWLESS__}" + echo "→ RESET: архивирую прежнюю «$ORGNAME» (SuperAdmin $SA_EMAIL)…" + SATOKEN=$(curl -sk -m 30 -X POST "$BASE/connect/token" -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode grant_type=password --data-urlencode "username=$SA_EMAIL" --data-urlencode "password=$SA_PASS" \ + --data-urlencode client_id=food-market-web --data-urlencode 'scope=openid profile email roles api' | j access_token) + if [[ -n "$SATOKEN" ]]; then + # orgId прежнего владельца (если он ещё существует и активен). + OLDTOKEN=$(curl -sk -m 30 -X POST "$BASE/connect/token" -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode grant_type=password --data-urlencode "username=$EMAIL" --data-urlencode "password=$PASSWORD" \ + --data-urlencode client_id=food-market-web --data-urlencode 'scope=openid profile email roles api' | j access_token) + if [[ -n "$OLDTOKEN" ]]; then + OLDORG=$(curl -sk -m 30 "$BASE/api/me" -H "Authorization: Bearer $OLDTOKEN" | j orgId) + if [[ -n "$OLDORG" ]]; then + code=$(curl -sk -m 30 -o /dev/null -w '%{http_code}' -X POST "$BASE/api/super-admin/organizations/$OLDORG/archive" \ + -H "Authorization: Bearer $SATOKEN" -H 'Content-Type: application/json' \ + -d "{\"confirmationName\":\"$ORGNAME\"}") + echo " archive $OLDORG → HTTP $code" + fi + else + echo " прежний владелец не логинится (орг уже архивна?) — пропускаю." + fi + else + echo " ⚠ не удалось получить SuperAdmin-токен — пропускаю RESET." + fi +fi + # 1. Регистрация организации (идемпотентно: если e-mail занят — просто логинимся). echo "→ signup…" signup=$(curl -sk -m 60 -X POST "$BASE/api/auth/signup" \ diff --git a/docs/site-screenshots-progress.md b/docs/site-screenshots-progress.md index 70990b6..1e52688 100644 --- a/docs/site-screenshots-progress.md +++ b/docs/site-screenshots-progress.md @@ -1,26 +1,42 @@ # Обновление скриншотов публичного сайта (ALL MARKET) -Владелец 08.10: «скриншоты нужно ещё обновить». Сейчас +Владелец 08.10: «скриншоты нужно ещё обновить». Старые `src/food-market.public/public/screenshots/*.png` — от 26.04, старый интерфейс со старым -логотипом Food Market. Нужны свежие скриншоты интерфейса ALL MARKET на правдоподобных -витринных данных (без e2e/test-мусора). +логотипом Food Market. Переснято в интерфейсе ALL MARKET на витринной организации +«Магазин у дома» (чистые данные, без e2e/test-мусора). ## План / статус -1. [ ] Витринная организация на стенде «Магазин у дома» (простой режим): ~200 реальных - товаров (цены/штрихкоды/группы), 2–3 поставщика, неделя продаж, приёмки/списания, - инвентаризация, счета (касса/Kaspi/эквайринг), сотрудники с обычными именами. - Повторяемый скрипт — `deploy/showcase-seed.*`. -2. [ ] Переснять `scripts/screenshots.mjs` в интерфейсе ALL MARKET (1440×900 @2x, светлая тема, - без тостов/отладки, без чужих данных): дашборд, каталог, карточка товара, приёмка, - остатки, инвентаризация, деньги/смены, отчёты, контрагенты. -3. [ ] Касса: свежие скрины новой светлой кассы (продажа/оплата). Мобильное iOS — если есть - `~/.fm-watchdog/brand/ios-screens/`. -4. [ ] Подставить на сайт, оптимизировать (webp/png ≤300КБ, srcset), alt по-русски. OG — проверить. -5. [ ] Выкладку публичного сайта добавить в `~/deploy-stage.sh` (через локальное зеркало образов). -6. [ ] Выложить, проверить test.food-market.kz, галерея `docs/site-screenshots/index.html`. +1. [x] Витринная организация на стенде «Магазин у дома» (простой режим): 200 реальных + товаров (без Article-маркеров, реальные EAN-13, группы), 3 поставщика, ~30 дней + продаж (856 чеков), 6 приёмок (стартовый завоз всех товаров + 5 текущих), 2 списания, + завершённая инвентаризация, счета (касса/Kaspi/эквайринг), сотрудники с обычными именами. + Сидер `ShowcaseSeeder` + эндпоинт `POST /api/admin/seed-demo?showcase=1`. + Повторяемый скрипт `deploy/showcase-seed.sh` (signup + seed; `RESET=1` пересоздаёт с нуля). +2. [x] Пересняты `scripts/screenshots.mjs` в ALL MARKET (1440×900 @2x, светлая тема, баннеры + стенда/«что нового»/MoM-дельта скрыты): дашборд, каталог, карточка товара, приёмки (список + + документ), приёмка по фото, остатки, инвентаризация, деньги/счета, отчёты (продажи, + прибыль), контрагенты. Логин — витринный владелец (без super-admin override). +3. [x] Касса: новая светлая касса (продажа + оплата) — макет + `scripts/pos-mockup/pos-mockup.html` (ALL MARKET, «Магазин у дома», кассир Виктория Ким), + рендер в screenshots.mjs → `pos-sale`/`pos-pay`. На pos.astro и на главной. + Мобильное iOS — НЕ использовано: в `~/.fm-watchdog/brand/ios-screens/` бейдж «Демо-магазин» + и на home-экране мусор «Касса (e2e) …», каталог не продуктовый — противоречит требованию + «без Демо/e2e». Блок «Склад в телефоне» отложен до чистых iOS-скринов. +4. [x] На сайте: `index.astro` (дашборд-hero + каталог + новый блок «Касса»), `pos.astro` + (продажа+оплата). WebP + srcset (1x 1440w / 2x 2880w), все ≤300 КБ (max 205 КБ), + alt по-русски. Исходные PNG не коммитятся (`.gitignore`), доставка — только WebP. + OG — уже ALL MARKET (проверено `og/home.png`). +5. [x] Выкладка публичного сайта добавлена в `~/deploy-stage.sh` (функция deploy_public, локальное + зеркало образов). Основа — `~/.fm-watchdog/deploy-public-test.sh`. +6. [~] Выложить, проверить test.food-market.kz, галерея `docs/site-screenshots/index.html` — ниже. ## Что НЕ трогаем Прод (food-market.kz, admin.food-market.kz), shared-postgres, global.json, gateway nginx, sklad. ## Журнал - 08.10 — старт: план, обследование текущих скриншотов и страниц сайта. +- 08.10 — ShowcaseSeeder + эндпоинт + deploy/showcase-seed.sh; деплой API на стенд. +- 08.10 — витринная орг. «Магазин у дома» (200 товаров/856 продаж/счета/сотрудники), чистые данные. +- 08.10 — screenshots.mjs переписан на стенд+витрину (14 экранов) + optimize-screenshots.mjs (WebP). +- 08.10 — фикс сидера: стартовый завоз всех товаров → нет отрицательных остатков; RESET пересоздаёт орг. +- 08.10 — сайт: index.astro + pos.astro на новые WebP, POS-мокап ALL MARKET. diff --git a/src/food-market.api/Seed/ShowcaseSeeder.cs b/src/food-market.api/Seed/ShowcaseSeeder.cs index 3d53faa..8603985 100644 --- a/src/food-market.api/Seed/ShowcaseSeeder.cs +++ b/src/food-market.api/Seed/ShowcaseSeeder.cs @@ -215,26 +215,27 @@ public async Task SeedAsync(Guid orgId, CancellationToken ct = defa // Накопитель движений (bulk в конце, один пересчёт агрегата Stocks). var movements = new List(); - // ── Приёмки: 6 поставок за последние 30 дней ───────────────────────── - var supplyCount = 6; + // ── Приёмки ────────────────────────────────────────────────────────── + // Первая — стартовый завоз ВСЕХ товаров (чтобы ни один остаток не ушёл + // в минус после продаж), затем 5 текущих поставок по случайным позициям. var supplies = new List(); - for (int i = 0; i < supplyCount; i++) + int supplyNo = 1; + + void AddSupply(DateTime date, IReadOnlyList picked, Func qtyFn) { - var date = now.AddDays(-28 + i * 4 - _rng.NextDouble()); - var picked = products.OrderBy(_ => _rng.Next()).Take(_rng.Next(30, 60)).ToList(); var supply = new Supply { OrganizationId = orgId, - Number = $"П-{yearNow}-{i + 1:D6}", + Number = $"П-{yearNow}-{supplyNo++:D6}", Date = date, Status = SupplyStatus.Posted, PostedAt = date, - SupplierId = suppliers[i % suppliers.Count].Id, + SupplierId = suppliers[(supplyNo) % suppliers.Count].Id, StoreId = mainStore.Id, CurrencyId = currency.Id, }; decimal total = 0; int order = 0; foreach (var p in picked) { - var qty = _rng.Next(20, 80); + var qty = qtyFn(); var price = Math.Round(p.ReferencePrice!.Value * 0.6m, 2); var lt = qty * price; supply.Lines.Add(new SupplyLine @@ -257,6 +258,16 @@ public async Task SeedAsync(Guid orgId, CancellationToken ct = defa supplies.Add(supply); } + // Стартовый завоз: все товары, 120–240 ед. — покроет месяц продаж. + AddSupply(now.AddDays(-33), products, () => _rng.Next(120, 240)); + // 5 текущих поставок за последние 28 дней. + for (int i = 0; i < 5; i++) + { + var date = now.AddDays(-24 + i * 5 - _rng.NextDouble()); + var picked = products.OrderBy(_ => _rng.Next()).Take(_rng.Next(30, 60)).ToList(); + AddSupply(date, picked, () => _rng.Next(40, 120)); + } + // ── Продажи: ~30 дней, 18-38 чеков в день ──────────────────────────── var sales = new List(); int saleNo = 1; diff --git a/src/food-market.public/.gitignore b/src/food-market.public/.gitignore index 8ee464f..f06773e 100644 --- a/src/food-market.public/.gitignore +++ b/src/food-market.public/.gitignore @@ -1 +1,2 @@ scripts/fonts/ +public/screenshots/*.png diff --git a/src/food-market.public/public/screenshots/analytics.png b/src/food-market.public/public/screenshots/analytics.png deleted file mode 100644 index 177f173..0000000 Binary files a/src/food-market.public/public/screenshots/analytics.png and /dev/null differ diff --git a/src/food-market.public/public/screenshots/catalog.png b/src/food-market.public/public/screenshots/catalog.png deleted file mode 100644 index b789ecb..0000000 Binary files a/src/food-market.public/public/screenshots/catalog.png and /dev/null differ diff --git a/src/food-market.public/public/screenshots/catalog.webp b/src/food-market.public/public/screenshots/catalog.webp new file mode 100644 index 0000000..165f13f Binary files /dev/null and b/src/food-market.public/public/screenshots/catalog.webp differ diff --git a/src/food-market.public/public/screenshots/catalog@2x.webp b/src/food-market.public/public/screenshots/catalog@2x.webp new file mode 100644 index 0000000..fe4ed59 Binary files /dev/null and b/src/food-market.public/public/screenshots/catalog@2x.webp differ diff --git a/src/food-market.public/public/screenshots/counterparties.png b/src/food-market.public/public/screenshots/counterparties.png deleted file mode 100644 index 0883d6c..0000000 Binary files a/src/food-market.public/public/screenshots/counterparties.png and /dev/null differ diff --git a/src/food-market.public/public/screenshots/counterparties.webp b/src/food-market.public/public/screenshots/counterparties.webp new file mode 100644 index 0000000..3619e8b Binary files /dev/null and b/src/food-market.public/public/screenshots/counterparties.webp differ diff --git a/src/food-market.public/public/screenshots/counterparties@2x.webp b/src/food-market.public/public/screenshots/counterparties@2x.webp new file mode 100644 index 0000000..c575e62 Binary files /dev/null and b/src/food-market.public/public/screenshots/counterparties@2x.webp differ diff --git a/src/food-market.public/public/screenshots/dashboard-hero.png b/src/food-market.public/public/screenshots/dashboard-hero.png deleted file mode 100644 index 177f173..0000000 Binary files a/src/food-market.public/public/screenshots/dashboard-hero.png and /dev/null differ diff --git a/src/food-market.public/public/screenshots/dashboard-hero.webp b/src/food-market.public/public/screenshots/dashboard-hero.webp new file mode 100644 index 0000000..764dd02 Binary files /dev/null and b/src/food-market.public/public/screenshots/dashboard-hero.webp differ diff --git a/src/food-market.public/public/screenshots/dashboard-hero@2x.webp b/src/food-market.public/public/screenshots/dashboard-hero@2x.webp new file mode 100644 index 0000000..61c70e7 Binary files /dev/null and b/src/food-market.public/public/screenshots/dashboard-hero@2x.webp differ diff --git a/src/food-market.public/public/screenshots/inventory.webp b/src/food-market.public/public/screenshots/inventory.webp new file mode 100644 index 0000000..34e76d8 Binary files /dev/null and b/src/food-market.public/public/screenshots/inventory.webp differ diff --git a/src/food-market.public/public/screenshots/inventory@2x.webp b/src/food-market.public/public/screenshots/inventory@2x.webp new file mode 100644 index 0000000..e8c2252 Binary files /dev/null and b/src/food-market.public/public/screenshots/inventory@2x.webp differ diff --git a/src/food-market.public/public/screenshots/money.webp b/src/food-market.public/public/screenshots/money.webp new file mode 100644 index 0000000..fc6d109 Binary files /dev/null and b/src/food-market.public/public/screenshots/money.webp differ diff --git a/src/food-market.public/public/screenshots/money@2x.webp b/src/food-market.public/public/screenshots/money@2x.webp new file mode 100644 index 0000000..7a2b55c Binary files /dev/null and b/src/food-market.public/public/screenshots/money@2x.webp differ diff --git a/src/food-market.public/public/screenshots/pos-pay.webp b/src/food-market.public/public/screenshots/pos-pay.webp new file mode 100644 index 0000000..8bd6402 Binary files /dev/null and b/src/food-market.public/public/screenshots/pos-pay.webp differ diff --git a/src/food-market.public/public/screenshots/pos-pay@2x.webp b/src/food-market.public/public/screenshots/pos-pay@2x.webp new file mode 100644 index 0000000..9c2b26f Binary files /dev/null and b/src/food-market.public/public/screenshots/pos-pay@2x.webp differ diff --git a/src/food-market.public/public/screenshots/pos-sale.webp b/src/food-market.public/public/screenshots/pos-sale.webp new file mode 100644 index 0000000..2f9b2d4 Binary files /dev/null and b/src/food-market.public/public/screenshots/pos-sale.webp differ diff --git a/src/food-market.public/public/screenshots/pos-sale@2x.webp b/src/food-market.public/public/screenshots/pos-sale@2x.webp new file mode 100644 index 0000000..8a3fa6c Binary files /dev/null and b/src/food-market.public/public/screenshots/pos-sale@2x.webp differ diff --git a/src/food-market.public/public/screenshots/product-card.png b/src/food-market.public/public/screenshots/product-card.png deleted file mode 100644 index 126fc37..0000000 Binary files a/src/food-market.public/public/screenshots/product-card.png and /dev/null differ diff --git a/src/food-market.public/public/screenshots/product-card.webp b/src/food-market.public/public/screenshots/product-card.webp new file mode 100644 index 0000000..50480f9 Binary files /dev/null and b/src/food-market.public/public/screenshots/product-card.webp differ diff --git a/src/food-market.public/public/screenshots/product-card@2x.webp b/src/food-market.public/public/screenshots/product-card@2x.webp new file mode 100644 index 0000000..8de9ebb Binary files /dev/null and b/src/food-market.public/public/screenshots/product-card@2x.webp differ diff --git a/src/food-market.public/public/screenshots/report-profit.webp b/src/food-market.public/public/screenshots/report-profit.webp new file mode 100644 index 0000000..62b0e61 Binary files /dev/null and b/src/food-market.public/public/screenshots/report-profit.webp differ diff --git a/src/food-market.public/public/screenshots/report-profit@2x.webp b/src/food-market.public/public/screenshots/report-profit@2x.webp new file mode 100644 index 0000000..3d547d9 Binary files /dev/null and b/src/food-market.public/public/screenshots/report-profit@2x.webp differ diff --git a/src/food-market.public/public/screenshots/report-sales.webp b/src/food-market.public/public/screenshots/report-sales.webp new file mode 100644 index 0000000..681d5fa Binary files /dev/null and b/src/food-market.public/public/screenshots/report-sales.webp differ diff --git a/src/food-market.public/public/screenshots/report-sales@2x.webp b/src/food-market.public/public/screenshots/report-sales@2x.webp new file mode 100644 index 0000000..a194e74 Binary files /dev/null and b/src/food-market.public/public/screenshots/report-sales@2x.webp differ diff --git a/src/food-market.public/public/screenshots/stock.webp b/src/food-market.public/public/screenshots/stock.webp new file mode 100644 index 0000000..d8ada73 Binary files /dev/null and b/src/food-market.public/public/screenshots/stock.webp differ diff --git a/src/food-market.public/public/screenshots/stock@2x.webp b/src/food-market.public/public/screenshots/stock@2x.webp new file mode 100644 index 0000000..f0625df Binary files /dev/null and b/src/food-market.public/public/screenshots/stock@2x.webp differ diff --git a/src/food-market.public/public/screenshots/supply-detail.webp b/src/food-market.public/public/screenshots/supply-detail.webp new file mode 100644 index 0000000..48029ea Binary files /dev/null and b/src/food-market.public/public/screenshots/supply-detail.webp differ diff --git a/src/food-market.public/public/screenshots/supply-detail@2x.webp b/src/food-market.public/public/screenshots/supply-detail@2x.webp new file mode 100644 index 0000000..056d1a4 Binary files /dev/null and b/src/food-market.public/public/screenshots/supply-detail@2x.webp differ diff --git a/src/food-market.public/public/screenshots/supply-form.png b/src/food-market.public/public/screenshots/supply-form.png deleted file mode 100644 index cd946a6..0000000 Binary files a/src/food-market.public/public/screenshots/supply-form.png and /dev/null differ diff --git a/src/food-market.public/public/screenshots/supply-form.webp b/src/food-market.public/public/screenshots/supply-form.webp new file mode 100644 index 0000000..a80117a Binary files /dev/null and b/src/food-market.public/public/screenshots/supply-form.webp differ diff --git a/src/food-market.public/public/screenshots/supply-form@2x.webp b/src/food-market.public/public/screenshots/supply-form@2x.webp new file mode 100644 index 0000000..9c60ac5 Binary files /dev/null and b/src/food-market.public/public/screenshots/supply-form@2x.webp differ diff --git a/src/food-market.public/public/screenshots/supply-scan.webp b/src/food-market.public/public/screenshots/supply-scan.webp new file mode 100644 index 0000000..a72e339 Binary files /dev/null and b/src/food-market.public/public/screenshots/supply-scan.webp differ diff --git a/src/food-market.public/public/screenshots/supply-scan@2x.webp b/src/food-market.public/public/screenshots/supply-scan@2x.webp new file mode 100644 index 0000000..b9337fc Binary files /dev/null and b/src/food-market.public/public/screenshots/supply-scan@2x.webp differ diff --git a/src/food-market.public/scripts/optimize-screenshots.mjs b/src/food-market.public/scripts/optimize-screenshots.mjs new file mode 100644 index 0000000..30fc3ba --- /dev/null +++ b/src/food-market.public/scripts/optimize-screenshots.mjs @@ -0,0 +1,58 @@ +// Оптимизация скриншотов для сайта: PNG (2880×1800 @2x) → WebP с ресайзом и srcset. +// Без внешних зависимостей (sharp/cwebp недоступны на dev-vm) — кодируем через +// canvas.toDataURL('image/webp') в headless-Chromium (Playwright уже установлен). +// +// Для каждого исходного PNG из public/screenshots делает: +// .webp — 1440w (служит 2x для ~720px-слота), качество 0.86 +// @2x.webp — 2880w (для srcset на крупных экранах), качество 0.82 +// Все файлы ≤ 300 КБ (UI-скриншоты сжимаются отлично). +// +// Запуск: node scripts/optimize-screenshots.mjs +import { chromium } from 'playwright' +import path from 'node:path' +import fs from 'node:fs' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const DIR = path.resolve(__dirname, '..', 'public', 'screenshots') +const CHROME = process.env.CHROME ?? '/usr/bin/google-chrome' + +const pngs = fs.readdirSync(DIR).filter((f) => f.endsWith('.png')) + +const browser = await chromium.launch({ + headless: true, executablePath: CHROME, args: ['--no-sandbox', '--disable-dev-shm-usage'], +}) +const page = await browser.newPage() + +async function encode(srcPng, outName, targetW, quality) { + const dataUrl = 'data:image/png;base64,' + fs.readFileSync(path.join(DIR, srcPng)).toString('base64') + const b64 = await page.evaluate(async ({ url, targetW, quality }) => { + const img = new Image() + await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = url }) + const scale = Math.min(1, targetW / img.naturalWidth) + const w = Math.round(img.naturalWidth * scale) + const h = Math.round(img.naturalHeight * scale) + const c = document.createElement('canvas') + c.width = w; c.height = h + const ctx = c.getContext('2d') + ctx.imageSmoothingEnabled = true + ctx.imageSmoothingQuality = 'high' + ctx.drawImage(img, 0, 0, w, h) + return c.toDataURL('image/webp', quality).split(',')[1] + }, { url: dataUrl, targetW, quality }) + const buf = Buffer.from(b64, 'base64') + fs.writeFileSync(path.join(DIR, outName), buf) + return buf.length +} + +for (const png of pngs) { + const base = png.replace(/\.png$/, '') + const s1 = await encode(png, `${base}.webp`, 1440, 0.86) + const s2 = await encode(png, `${base}@2x.webp`, 2880, 0.82) + const kb = (n) => (n / 1024).toFixed(0) + 'KB' + const warn = (s1 > 300 * 1024 || s2 > 300 * 1024) ? ' ⚠ >300KB' : '' + console.log(`${base}: 1x ${kb(s1)}, 2x ${kb(s2)}${warn}`) +} + +await browser.close() +console.log('done') diff --git a/src/food-market.public/scripts/pos-mockup/pos-mockup.html b/src/food-market.public/scripts/pos-mockup/pos-mockup.html new file mode 100644 index 0000000..d1eb299 --- /dev/null +++ b/src/food-market.public/scripts/pos-mockup/pos-mockup.html @@ -0,0 +1,219 @@ + + + + +Касса — макет + + + + + +
+
+ +
+
Магазин у дома · Точка Касса 1
+
+
Кассир Виктория Ким
+
+
Смена С-2026-000012 открыта с 09:00
+
На связи · очередь пуста
+
+ +
+
+ + +
Избранное
+
+
Хлеб «Семейный»
280 ₸
+
Молоко 2,5%
450 ₸
+
Яблоки (вес)
690 ₸/кг
+
Картофель (вес)
280 ₸/кг
+
Пакет-майка
20 ₸
+
Вода 0,5 л
150 ₸
+
+ +
+
Товар
Количество
Сумма
+
Хлеб «Семейный» нарезной
280 ₸ за шт
+
−2 шт+
560 ₸
+
+
Молоко 2,5% 1 л
450 ₸ за шт
+
−1 шт+
450 ₸
+
+
Яблоки «Апорт»
690 ₸ за кг
+
−1,235 кг+
852 ₸
+
+
Сахар-песок 1 кг
520 ₸ за шт · скидка 60 ₸ (1 560 → 1 500 ₸)
+
−3 шт+
1 500 ₸
+
+
+
+ + +
+ +
+ F1ПомощьF2ПоискF4СкидкаF7Отложить + F9ВозвратF10СменаF12ОплатаDelУдалить строкуEscОтмена +
+
+ +
+ +
+ + diff --git a/src/food-market.public/scripts/screenshots.mjs b/src/food-market.public/scripts/screenshots.mjs index 0644de9..776f98d 100644 --- a/src/food-market.public/scripts/screenshots.mjs +++ b/src/food-market.public/scripts/screenshots.mjs @@ -1,19 +1,51 @@ +// Скриншоты интерфейса ALL MARKET для публичного сайта. +// +// Снимает СТЕНД (test.admin.food-market.kz) под витринной организацией +// «Магазин у дома» (создаётся deploy/showcase-seed.sh — чистые данные, без +// e2e/test-мусора). Светлая тема, 1440×900 @2x. Плюс рендерит макет новой +// светлой кассы (scripts/pos-mockup/pos-mockup.html) → продажа + оплата. +// +// Предусловие: на стенде поднят API с ShowcaseSeeder и выполнен showcase-seed. +// bash deploy/showcase-seed.sh +// +// Запуск (dev-vm, системный chrome + no-sandbox — иначе sandbox убивает chrome): +// node src/food-market.public/scripts/screenshots.mjs +// +// Переменные окружения: +// APP — админка (по умолч. https://test.admin.food-market.kz) +// SHOWCASE_EMAIL — владелец витринной орг. (vladelec@magazin-u-doma.kz) +// SHOWCASE_PASSWORD — пароль (MagazinUDoma2026!) +// CHROME — путь к chrome (по умолч. /usr/bin/google-chrome) import { chromium } from 'playwright' import path from 'node:path' -import { fileURLToPath } from 'node:url' +import { fileURLToPath, pathToFileURL } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const OUT = path.resolve(__dirname, '..', 'public', 'screenshots') -const APP = 'https://admin.food-market.kz' -const EMAIL = 'admin@food-market.local' -const PASSWORD = 'Admin12345!' +const APP = process.env.APP ?? 'https://test.admin.food-market.kz' +const EMAIL = process.env.SHOWCASE_EMAIL ?? 'vladelec@magazin-u-doma.kz' +const PASSWORD = process.env.SHOWCASE_PASSWORD ?? 'MagazinUDoma2026!' +const CHROME = process.env.CHROME ?? '/usr/bin/google-chrome' -const browser = await chromium.launch({ headless: true }) -const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1600, height: 1000 } }) +const VP = { width: 1440, height: 900 } + +const browser = await chromium.launch({ + headless: true, + executablePath: CHROME, + args: ['--no-sandbox', '--disable-dev-shm-usage', '--force-color-profile=srgb'], +}) +const ctx = await browser.newContext({ + ignoreHTTPSErrors: true, + viewport: VP, + deviceScaleFactor: 2, + colorScheme: 'light', + locale: 'ru-RU', +}) const page = await ctx.newPage() -console.log('login →', APP) +// ── Логин витринного владельца ────────────────────────────────────────────── +console.log('login →', APP, 'as', EMAIL) await page.goto(`${APP}/login`, { waitUntil: 'networkidle' }) await page.fill('input[type="email"]', EMAIL) await page.fill('input[type="password"]', PASSWORD) @@ -22,90 +54,138 @@ await page.waitForURL((u) => !u.pathname.startsWith('/login'), { timeout: 30000 await page.waitForTimeout(1500) console.log('logged in →', page.url()) -// Override tenant: переходим на /super-admin/organizations → клик «Открыть» -// у строки FOOD MARKET (иконка-стрелка, title=«Открыть как…»). -console.log('tenant override → FOOD MARKET') -await page.goto(`${APP}/super-admin/organizations`, { waitUntil: 'networkidle' }) -await page.waitForTimeout(1000) -// Клик на ряд FOOD MARKET — берём первую строку с этим текстом и в ней -// первую кнопку-ссылку (login-icon). -const rows = await page.$$('table tbody tr, tbody tr, [role="row"]') -let opened = false -for (const r of rows) { - const txt = (await r.innerText()).trim() - if (/FOOD MARKET/i.test(txt)) { - const btn = await r.$('a, button') - if (btn) { - await btn.click() - opened = true - break +// id первого товара — для карточки товара (надёжнее, чем клик по гриду). +const token = await page.evaluate(() => localStorage.getItem('fm.access_token')) +// Для карточки берём фотогеничный штучный товар (поиск по имени — list-эндпоинт +// пагинируется, нужный товар может быть не на первой странице). +let cardProductId = null +try { + cardProductId = await page.evaluate(async (app) => { + const auth = { Authorization: `Bearer ${localStorage.getItem('fm.access_token')}` } + for (const q of ['Молоко 3.2% 1л', 'Кола 0.5л', 'Хлеб ржаной 600г']) { + const r = await fetch(`${app}/api/catalog/products?search=${encodeURIComponent(q)}&take=3`, { headers: auth }) + const d = await r.json() + const arr = Array.isArray(d) ? d : (d.items ?? d.data ?? []) + const hit = arr.find((p) => p.name === q) ?? arr[0] + if (hit) return hit.id } - } + return null + }, APP) +} catch (e) { + console.warn('product id fetch failed:', e.message) } -if (!opened) { - // Fallback: клик по последней login-стрелке (там обычно FOOD MARKET). - const arrows = await page.$$('a[href*="/api/super-admin/organizations/"][href*="open"], button[title*="ткрыт"]') - if (arrows.length) await arrows[arrows.length - 1].click() +console.log('cardProductId =', cardProductId) + +// Карточка товара: group-селект (AsyncSelect) грузит подпись только при открытии, +// иначе показывает «…». Открываем и закрываем, чтобы подставилась «Молочные продукты». +// AsyncSelect грузит подпись только при открытии (иначе «…»). Открываем нужный +// селект и закрываем кликом по нейтральному заголовку (НЕ Escape — на карточках +// Escape = «назад к списку»). +async function resolveSelect(p, fieldLabel, neutralText) { + try { + const btn = p.locator(`label:has-text("${fieldLabel}") button`).first() + await btn.click({ timeout: 4000 }) + await p.waitForTimeout(1000) + await p.locator(`text=${neutralText}`).first().click({ timeout: 3000 }).catch(() => {}) + await p.waitForTimeout(400) + } catch (e) { console.warn(` resolve «${fieldLabel}» skipped:`, e.message) } } -await page.waitForTimeout(2000) -await page.waitForLoadState('networkidle') -console.log('after override →', page.url()) +const resolveGroupLabel = (p) => resolveSelect(p, 'Группа', 'Основное') +const resolveSupplier = (p) => resolveSelect(p, 'Поставщик', 'Реквизиты документа') -const shots = [ - { name: 'dashboard-hero.png', path: '/dashboard', vp: { width: 1600, height: 1000 } }, - { name: 'analytics.png', path: '/dashboard', vp: { width: 1600, height: 1000 }, scrollY: 500 }, - { name: 'catalog.png', path: '/catalog/products', vp: { width: 1600, height: 1000 } }, - { name: 'product-card.png', path: '/catalog/products', vp: { width: 1200, height: 900 }, openFirst: true }, - { name: 'counterparties.png', path: '/catalog/counterparties', vp: { width: 1200, height: 900 } }, - { name: 'supply-form.png', path: '/purchases/supplies/new', vp: { width: 1200, height: 900 } }, -] +// Проведённая приёмка (заполненный документ выглядит лучше пустой формы). +let supplyId = null +try { + supplyId = await page.evaluate(async (app) => { + const r = await fetch(`${app}/api/purchases/supplies?take=5&skip=0`, { + headers: { Authorization: `Bearer ${localStorage.getItem('fm.access_token')}` }, + }) + const d = await r.json() + const arr = Array.isArray(d) ? d : (d.items ?? d.data ?? []) + return arr[0]?.id ?? null + }, APP) +} catch (e) { + console.warn('supply id fetch failed:', e.message) +} +console.log('supplyId =', supplyId) -// CSS, скрывающий override-warning баннер (оранжевую полосу сверху). -const HIDE_OVERRIDE_CSS = ` - [class*="bg-orange"], [class*="bg-amber"], [class*="bg-yellow-4"], - [class*="ENV"], header [class*="orange"], div[class*="режиме"], - div:has(> *:not(:empty):where(:scope:contains("СУПЕР-АДМИНА"))) { display: none !important; } +// CSS: прячем тосты, отладочные/override-баннеры, скроллбары. +const HIDE_CSS = ` + [role="status"], [data-sonner-toaster], .toast, [class*="Toaster"], + [class*="toast"], [class*="override"], [class*="devbar"], + [data-testid="stage-banner"], [role="alert"] { display:none !important; } + *::-webkit-scrollbar { width:0 !important; height:0 !important; } ` -for (const s of shots) { - console.log('shot', s.name, '←', s.path) - await page.setViewportSize(s.vp) - await page.goto(`${APP}${s.path}`, { waitUntil: 'networkidle' }) - await page.waitForTimeout(2000) - // Убираем оранжевый override-баннер по тексту. - await page.evaluate(() => { - document.querySelectorAll('div, header').forEach((el) => { - const t = el.textContent || '' - if (/В РЕЖИМЕ СУПЕР-АДМИНА/i.test(t) && el.children.length < 6 && el.offsetHeight < 80) { - el.style.display = 'none' - } - }) - }) - if (s.scrollY) { - await page.evaluate((y) => window.scrollTo(0, y), s.scrollY) - await page.waitForTimeout(500) - } - if (s.openFirst) { - const cellLink = await page.$('table tbody tr td a, table tbody tr td button') - if (cellLink) { - try { - await cellLink.click() - await page.waitForLoadState('networkidle', { timeout: 10000 }) - await page.waitForTimeout(2000) - } catch (e) { - console.warn(' openFirst click failed:', e.message) - } - } else { - // Fallback: клик в первую ячейку строки. - const firstCell = await page.$('table tbody tr') - if (firstCell) { - try { await firstCell.click(); await page.waitForTimeout(2000) } catch {} +// Прячет верхние баннеры (тестовый стенд, «Появились новые функции»). +async function hideBanners(p) { + await p.addStyleTag({ content: HIDE_CSS }).catch(() => {}) + await p.evaluate(() => { + const kill = (re) => { + for (const el of Array.from(document.querySelectorAll('div, header, section'))) { + const t = (el.textContent || '').trim() + if (re.test(t) && t.length < 200) { el.style.display = 'none' } } } - } - await page.screenshot({ path: path.join(OUT, s.name), fullPage: false }) - console.log(' ok →', s.name, s.vp.width + 'x' + s.vp.height) + kill(/ТЕСТОВЫЙ СТЕНД/i) + kill(/Появились новые функции/i) + kill(/СУПЕР-АДМИН|режиме просмотра/i) + // MoM-дельта: на свежих данных «месяц к месяцу» неинформативна (текущий + // календарный месяц ещё не полон) — убираем красный −%, чтобы не пугал. + kill(/^[-+±].*прошлому месяцу$/i) + }) } +const shots = [ + { name: 'dashboard-hero.png', path: '/dashboard' }, + { name: 'catalog.png', path: '/catalog/products' }, + { name: 'product-card.png', path: cardProductId ? `/catalog/products/${cardProductId}` : '/catalog/products', wait: 3000, afterLoad: resolveGroupLabel }, + { name: 'supply-detail.png', path: supplyId ? `/purchases/supplies/${supplyId}` : '/purchases/supplies/new', wait: 3000, afterLoad: resolveSupplier }, + { name: 'supply-form.png', path: '/purchases/supplies' }, + { name: 'supply-scan.png', path: '/purchases/supplies/scan' }, + { name: 'stock.png', path: '/inventory/stock' }, + { name: 'inventory.png', path: '/inventory/inventories' }, + { name: 'money.png', path: '/money/accounts' }, + { name: 'report-sales.png', path: '/reports/sales' }, + { name: 'report-profit.png', path: '/reports/profit' }, + { name: 'counterparties.png', path: '/catalog/counterparties' }, +] + +for (const s of shots) { + try { + console.log('shot', s.name, '←', s.path) + await page.goto(`${APP}${s.path}`, { waitUntil: 'networkidle' }) + await page.waitForTimeout(s.wait ?? 2200) + if (s.afterLoad) await s.afterLoad(page) + await hideBanners(page) + await page.waitForTimeout(200) + await page.screenshot({ path: path.join(OUT, s.name), fullPage: false }) + console.log(' ok →', s.name) + } catch (e) { + console.warn(' FAILED', s.name, e.message) + } +} + +// ── Макет новой светлой кассы (локальный HTML) ────────────────────────────── +const posHtml = pathToFileURL(path.resolve(__dirname, 'pos-mockup', 'pos-mockup.html')).href +const posCtx = await browser.newContext({ + viewport: { width: 1920, height: 1080 }, + deviceScaleFactor: 1, + colorScheme: 'light', +}) +const posPage = await posCtx.newPage() +console.log('pos mockup →', posHtml) +// Экран оплаты (модалка видна). +await posPage.goto(posHtml, { waitUntil: 'networkidle' }) +await posPage.waitForTimeout(400) +await posPage.screenshot({ path: path.join(OUT, 'pos-pay.png') }) +console.log(' ok → pos-pay.png') +// Экран продажи (прячем модалку через body.nomodal). +await posPage.evaluate(() => document.body.classList.add('nomodal')) +await posPage.waitForTimeout(200) +await posPage.screenshot({ path: path.join(OUT, 'pos-sale.png') }) +console.log(' ok → pos-sale.png') +await posCtx.close() + await browser.close() console.log('done') diff --git a/src/food-market.public/src/pages/index.astro b/src/food-market.public/src/pages/index.astro index ef7ea9c..cea621d 100644 --- a/src/food-market.public/src/pages/index.astro +++ b/src/food-market.public/src/pages/index.astro @@ -52,7 +52,13 @@ const integrations = [ - Дашборд ALL MARKET + Дашборд ALL MARKET: выручка за день, неделю и месяц, график продаж за 30 дней и последние чеки @@ -87,7 +93,39 @@ const integrations = [ app.food-market.kz / catalog / products - Каталог товаров ALL MARKET — 29 540 SKU + Каталог товаров ALL MARKET: список товаров с группами, штрихкодами, себестоимостью и розничной ценой + + + + {/* 3b. Касса */} +
+
+
+ Касса +

Светлая касса, в которой приятно работать

+

Продажа и оплата — за пару действий. Поиск по штрихкоду и названию, избранные товары, скидки, оплата наличными, картой или смешанная, расчёт сдачи. Горячие клавиши для всей смены.

+
    +
  • ✓ Нативная поддержка торговых весов — без драйверов
  • +
  • ✓ Офлайн-режим: чеки сохраняются и подтянутся в облако
  • +
  • ✓ Фискальные чеки ОФД РК, Kaspi Pay, программы лояльности
  • +
+ Подробнее о кассе → +
+
+ Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши +
diff --git a/src/food-market.public/src/pages/pos.astro b/src/food-market.public/src/pages/pos.astro index 7959fdc..e216c09 100644 --- a/src/food-market.public/src/pages/pos.astro +++ b/src/food-market.public/src/pages/pos.astro @@ -45,13 +45,29 @@ const steps = [
-
-
- - - -
- Окно приёмки в ALL MARKET +

Светлая, быстрая касса

+

Продажа и оплата — за пару действий. Крупные цифры, горячие клавиши, оплата наличными, картой или смешанная, расчёт сдачи.

+
+
+ Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши +
Экран продажи
+
+
+ Окно оплаты кассы ALL MARKET: выбор способа оплаты — наличные, карта, смешанная — и расчёт сдачи +
Оплата и расчёт сдачи
+