Compare commits

..

No commits in common. "5c5f92de52895347c55326051f93741fca69fe01" and "304b72dc0468ea67b87c1aa965b5696e49d4e5db" have entirely different histories.

41 changed files with 185 additions and 921 deletions

View file

@ -1,155 +0,0 @@
# Исправления публичного сайта test.food-market.kz — круг 2 (11.10.2026)
**Деплой:** только тестовый сайт через `~/.fm-watchdog/deploy-public-test.sh`
(commit `6f229eb`, статус `OK`). Прод, gateway nginx, прод-БД — не трогались.
**Образ отката:** `192.168.1.193:5001/food-market-public:test-pre-site-fixes-2`
(создан из текущего `:test` ДО деплоя). Откат: перетегировать его в `:test` и
`docker compose up -d public` на prod-vm.
**Автопроверка (Playwright, как человек):** 44/44 — см. `index.html` (desktop 1440
@2x + mobile 390 @3x, 13 скриншотов), сырые данные — `verify.json`.
---
## Уточнение поручения владельца по брендам (важно)
Было два сообщения владельца. Итоговая (уточнённая) трактовка, по которой сделано:
- **Убрать со всего сайта — ТОЛЬКО чужие СИСТЕМЫ УЧЁТА** рознцы (конкуренты):
«МойСклад» / «Мой склад» / MoySklad, «1С» / 1C. Grep по `dist`: **0 вхождений**
вне `/legal/*` (в `/legal/*` их и нет — проверено).
- **Платёжные системы, банки, маркетплейсы** (Kaspi Pay, Kaspi Magazin, Halyk,
Jusan, Forte, Wildberries, Ozon, Visa, Mastercard) — **НЕ** чужие системы учёта,
удалять не требовалось. Оставлены **только как честная пометка «в разработке»**
на `/integrations` и главной (без дат и обещаний), нигде — как готовая функция.
- Промежуточное (ошибочно расширенное) удаление Kaspi/маркетплейсов со страницы
`/integrations` **откатил** — вернул их в блок «В разработке» в прежнем виде
(Kaspi Pay, Kaspi Magazin, Ozon, Wildberries; ОФД РК).
- Из KB/блога/FAQ обещания уже подключённых способов оплаты/интеграций всё равно
убраны — но по причине «**этого нет в продукте**», а не «чужой бренд».
Где Kaspi/маркетплейсы остались (проверено): `/integrations` (блок «В разработке»)
и главная (FAQ — «в планах, сроков не называем»). Больше нигде.
---
## Сделано — по пунктам круга 2
### 1. Оставшиеся «ложные обещания» / честность текстов
- **`/kb/pos-setup`** — касса для **Windows и Linux**; вход = адрес сервера + логин
+ PIN (не «ключ организации»); установка «скоро»; принтеры «58/80 мм (ESC/POS)».
- **`/blog/quick-start`** — убран дубль `# H1`; «Импорт через API» → «Импорт из
других систем учёта — в разработке»; шаги установки .msi/ключ → сервер+логин+PIN;
меню лояльности/маркетинга → «раздел «Акции и скидки»», «лояльность — в расширенном
режиме»; обложка — скриншот продукта (не чужое стоковое фото).
- **`/blog/launch`** — убран дубль H1; «триал» → «честный бесплатный период»;
«Касса для Windows» → «Windows и Linux … приложение — скоро»; маркетплейсы/оплаты
→ «в разработке» (без дат); «все возможности CRM/лояльность/…» → «без доплат за
модули».
- **`/kb/faq`** — бэкапы «каждые 24 ч» → «регулярные»; «TLS 1.3» → «TLS» (без
версии); формулировка про ЗРК смягчена до «в соответствии с законодательством РК»;
импорт — файлы Excel/CSV, из других систем «в разработке»; **«триал» → «бесплатный
период»** (унифицировано); «с SLA» убрано.
- **Унификация «триал» → «бесплатный период»** по сайту (og.mjs тоже). В `/legal/*`
термин «Триал» оставлен как есть (юр. документы не трогаем).
- **og-картинки** перегенерированы (`node scripts/og.mjs`): без чужих систем учёта;
касса «Windows и Linux»; «90 дней бесплатно».
### 2. React #418 (hydration mismatch) на `/signup`
`SignupForm.tsx`: `?plan=` и параметры конфигуратора теперь читаются в `useEffect`
**после** гидрации, поэтому первый клиентский рендер совпадает с SSR-разметкой.
Проверка консоли на `/`, `/pricing`, `/signup?plan=business&…` и `/signup?plan=business`
— **ошибок нет, #418 отсутствует** (8/8 авто-проверок).
### 3. `/signup` без параметров конфигуратора
- `plan=business` без параметров → выбран **«Бизнес»** с дефолтной конфигурацией
**1/1/1/5 = 10 000 ₸** (показана карточка «Ваша конфигурация»). ✓ проверено.
- `plan=network` → «Сеть»; неизвестный план → «Старт». ✓
- Клик по плитке тарифа обновляет URL (`history.replaceState`).
- Плитка «Старт»: «5 000 ₸» → **«5 000 ₸/мес»**; цены плиток без переноса.
### 4. Типографика KB/блога
- `.prose-md` (h2/h3, списки, таблицы, ссылки, код, цитаты) вынесена в
`global.css` — грузится на всех страницах; `/kb/*` больше **не плоский текст**
(раньше стиль был только на странице блога). ✓ проверено (`h2`, `ul li` в
`.prose-md`).
- Блог: убран дублирующий `<h1>` (в теле статей) — на странице ровно один H1. ✓
### 5. Мелочи
- **sitemap.xml** — канонические URL **со слэшем** (убраны 301), добавлены статьи
KB и блога. ✓
- **Footer** «Status» → **«Статус»**.
- **`/changelog`** — убран внутренний жаргон (Phase N, SuperAdmin, retention
period, tenant, inline, Read-only/Edit-mode) → человеческие формулировки;
заголовок «Что нового». ✓
- **`/pricing`** — на конструкторе «Бизнес» бейдж **«Популярный»** (как на главной)
+ **«90 дней бесплатно»**; у «Старт» убран ложный пункт «Импорт из других систем».
- Шапка ~1050px: десктоп-меню переведено на `xl` (до 1280px — гамбургер), CTA без
переноса → на ~1050px и на мобильном меню/кнопка **не ломаются в 2 строки**. ✓
- Инлайн-ссылки в тексте — увеличенная зона нажатия (padding без сдвига вёрстки).
- Эмодзи-фолбэк-стек в `body` (чтобы эмодзи не были квадратами там, где нет
цветного emoji-шрифта).
### 6. Не трогал
`/legal/*` и контакты-плейсхолдеры («[НАЗВАНИЕ ТОО]», «[БИН]», «+7 (___)») — без
изменений. Бренд ALL MARKET, цвета, шрифты — без изменений.
---
## Дополнение по тесту на Android (Firefox 157) — сделано
1. **Мобильное меню + кнопка «Назад» Android.** При открытии панели кладём запись
в историю (`pushState`), закрываем по `popstate` → системная «Назад» закрывает
меню, а не уводит со страницы. Esc/тап-вне/крестик остались. ✓ проверено
(`m-after-back.png` — после «Назад» панель закрыта, остались на `/`).
2. **Состав мобильной панели.** Добавлены «Интеграции» (в основную навигацию) и
внизу панели вторичные ссылки **«Статус»** и **«Что нового»** (раньше были только
в футере). ✓
3. **«Настроить тариф» (главная)** → `/pricing#configurator` со скроллом к
конструктору (у блока `id="configurator"` + `scroll-mt-20`). ✓ якорь есть.
4. **Ползунки конструктора на тач.** Крупная ручка (28px), `touch-action: pan-y`
на инпуте (вертикальный свайп скроллит страницу, а не двигает значение), и
кнопки **«−/+»** у каждого значения (менять без перетаскивания). ✓ проверено.
5. **`/kb/billing`.** Проверено по коду `src/food-market.*`: биллинг/подписки в
продукте **нет** (в вебе явная заглушка «до Phase 4 «Подписки»»; единственная
сущность `Subscription` — это `WebPushSubscription`, push-уведомления). Поэтому
убраны несуществующие: «Настройки → Подписка → История» (скачивание счетов/чеков),
e-mail-напоминания, баннер в админке, формула кредита при понижении, SLA для
«Сеть». Переписано честно: оплата на раннем доступе — вручную через поддержку,
онлайн-оплата и личный кабинет «в разработке». Цены «тг» → **«₸»**. ✓
6. **`/status`.** Убраны фиктивные зелёные «● Работает» (мониторинга нет) — честная
страница «в разработке» + контакты для инцидентов (реальный `/health` не
подключал: это требует серверной части, вне правки статического сайта). ✓
---
## Решения, требующие внимания владельца
1. **Контракт API регистрации.** Форма отправляет `plan`, а для «Бизнес» ещё и
`shops/cashes/warehouses/employees` (вперёд-совместимо). **Текущий серверный
контракт `SignupInput` принимает только `plan`** — конфигурация конструктора на
бэкенде пока игнорируется. Нужно решение: принимать ли её на сервере (тогда —
отдельная задача по API) или оставить как сейчас.
2. **Оборудование на `/integrations`.** В «Уже работает» вернул конкретику «весы
CAS, Масса-К и совместимые» (это производители **весов**, не системы учёта, —
под уточнённое поручение не попадают). Если хотите — заменю на нейтральное
«популярные протоколы».
3. **Юр. документы / реквизиты.** В `/legal/*` чужих систем учёта и брендов нет —
список пуст, ничего выносить не требуется. Плейсхолдеры реквизитов не трогал.
---
## Проверка (как в задаче)
| Критерий | Результат |
|---|---|
| Консоль чистая на `/`, `/pricing`, `/signup` (вариации), React #418 нет | ✓ |
| `plan=business` без параметров → «Бизнес» 10 000 (1/1/1/5) | ✓ |
| `dist`/живой сайт: 0 «МойСклад/Мой склад/moysklad/1С/1C» вне `/legal/*` | ✓ |
| Kaspi/маркетплейсы — только «в разработке» на `/integrations` и главной | ✓ |
| Унификация «бесплатный период»; «тг» → «₸» в KB | ✓ |
| Типографика KB (h2/списки) + один H1 в блоге | ✓ |
| sitemap — канонические URL со слэшем + KB/блог | ✓ |
| Моб.: «Назад» закрывает меню; состав панели; ползунки −/+ | ✓ |
| `/status` без фиктивного статуса; `/changelog` без жаргона; `/kb/billing` честный | ✓ |
Итог авто-проверки: **44/44**.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

View file

@ -1,65 +0,0 @@
<!doctype html><meta charset="utf-8"><title>Исправления сайта 2026-10-11 (круг 2)</title>
<style>body{font:15px system-ui;margin:0;padding:24px;background:#f8fafc;color:#0f172a;max-width:1100px;margin:auto}
h1{font-size:22px} figure{margin:0 0 24px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
figcaption{padding:8px 12px;font-size:13px;color:#475569;background:#f1f5f9} img{display:block;width:100%}
ul{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:14px 32px} li{margin:4px 0}
.ok{color:#16a34a} .no{color:#dc2626;font-weight:700} .dt{color:#64748b;font-size:12px}</style>
<h1>Исправления публичного сайта test.food-market.kz — круг 2 (2026-10-10)</h1>
<p>Проверка после деплоя (2026-10-10T20:27:26.438Z). Пройдено: 44/44.</p>
<h2>Автопроверки</h2><ul><li class="ok">✓ консоль чистая: главная</li>
<li class="ok">✓ нет React #418 (hydration): главная</li>
<li class="ok">✓ консоль чистая: /pricing</li>
<li class="ok">✓ нет React #418 (hydration): /pricing</li>
<li class="ok">✓ консоль чистая: /signup?plan=business+params</li>
<li class="ok">✓ нет React #418 (hydration): /signup?plan=business+params</li>
<li class="ok">✓ консоль чистая: /signup?plan=business (без параметров)</li>
<li class="ok">✓ нет React #418 (hydration): /signup?plan=business (без параметров)</li>
<li class="ok">✓ plan=business без параметров: выбран «Бизнес»</li>
<li class="ok">✓ plan=business по умолчанию: цена 10 000 — <span class="dt">Вашаконфигурация«Бизнес»10000₸/месМагази</span></li>
<li class="ok">✓ plan=business по умолчанию: Магазинов 1 / Складов 1 / Сотрудников 5</li>
<li class="ok">✓ plan=network: выбран «Сеть»</li>
<li class="ok">✓ plan=xyz: откат на «Старт»</li>
<li class="ok">✓ плитка «Старт»: цена «5 000 ₸/мес» — <span class="dt">Старт5000₸/мес</span></li>
<li class="ok">✓ KB: markdown отрисован (h2 в .prose-md)</li>
<li class="ok">✓ KB: списки отрисованы (ul li)</li>
<li class="ok">✓ KB billing: нет «Настройки → Подписка → История»</li>
<li class="ok">✓ KB billing: нет «тг», цены в «₸»</li>
<li class="ok">✓ блог quick-start: ровно один <h1> — <span class="dt">h1=1</span></li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): главная</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /integrations</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /import</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /kb/faq</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /blog/launch</li>
<li class="ok">✓ /integrations: Kaspi присутствует (в блоке «в разработке»)</li>
<li class="ok">✓ /integrations: блок «В разработке» есть</li>
<li class="ok">✓ /pricing: бейдж «Популярный» на конструкторе</li>
<li class="ok">✓ /pricing: «90 дней бесплатно» в конструкторе</li>
<li class="ok">✓ /pricing: якорь #configurator есть</li>
<li class="ok">✓ /pricing: ползунки с кнопками −/+</li>
<li class="ok">✓ /changelog: без жаргона (Phase/SuperAdmin/retention/tenant)</li>
<li class="ok">✓ /status: нет фиктивного «● Работает»</li>
<li class="ok">✓ /status: честная пометка «в разработке»</li>
<li class="ok">✓ sitemap: канонические URL со слэшем</li>
<li class="ok">✓ sitemap: статьи KB включены</li>
<li class="ok">✓ sitemap: статьи блога включены</li>
<li class="ok">✓ моб. главная: нет горизонтальной прокрутки</li>
<li class="ok">✓ моб. шапка: CTA «Начать бесплатно» в одну строку — <span class="dt">h=44px</span></li>
<li class="ok">✓ моб. меню открывается</li>
<li class="ok">✓ моб. меню: есть «Интеграции»</li>
<li class="ok">✓ моб. меню: есть «Статус»</li>
<li class="ok">✓ моб. меню: есть «Что нового» (changelog)</li>
<li class="ok">✓ моб. «Назад» закрывает меню (не уводит со страницы) — <span class="dt">https://test.food-market.kz/</span></li>
<li class="ok">✓ моб. /pricing: кнопки −/+ у ползунков</li></ul>
<h2>Скриншоты</h2><figure><figcaption>Десктоп 1440: /signup?plan=business без параметров — «Бизнес» с дефолтом 1/1/1/5 = 10 000 ₸</figcaption><img src="d-signup-business-default.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: KB «Тарифы и оплата» — markdown-типографика, честный текст (без несуществующего биллинг-UI), ₸</figcaption><img src="d-kb-billing.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: KB FAQ — типографика, «бесплатный период», TLS без версии</figcaption><img src="d-kb-faq.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: Блог «Быстрый старт» — один заголовок, без чужих систем учёта</figcaption><img src="d-blog-quickstart.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /integrations — платёжки/маркетплейсы только в «В разработке»; «уже работает» без чужих систем учёта</figcaption><img src="d-integrations.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /pricing — конструктор «Бизнес»: бейдж «Популярный», «90 дней бесплатно», кнопки −/+</figcaption><img src="d-pricing.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /changelog («Что нового») — человеческие формулировки без внутреннего жаргона</figcaption><img src="d-changelog.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /status — без фиктивных зелёных индикаторов, честно «в разработке» + контакты</figcaption><img src="d-status.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. главная 390px — CTA не переносится, гамбургер в шапке</figcaption><img src="m-home-closed.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. меню — полный состав + Статус/Что нового внизу</figcaption><img src="m-menu-open.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. после системной «Назад» — панель закрыта, остались на главной</figcaption><img src="m-after-back.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. конструктор — крупные ручки + кнопки −/+ (значение меняется кнопкой, не случайным свайпом)</figcaption><img src="m-pricing-configurator.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. /signup?plan=business без параметров — «Бизнес» с дефолтом 10 000 ₸</figcaption><img src="m-signup-business-default.png" loading="lazy"></figure>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

View file

@ -1,291 +0,0 @@
{
"results": [
{
"name": "консоль чистая: главная",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): главная",
"ok": true,
"detail": ""
},
{
"name": "консоль чистая: /pricing",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): /pricing",
"ok": true,
"detail": ""
},
{
"name": "консоль чистая: /signup?plan=business+params",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): /signup?plan=business+params",
"ok": true,
"detail": ""
},
{
"name": "консоль чистая: /signup?plan=business (без параметров)",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): /signup?plan=business (без параметров)",
"ok": true,
"detail": ""
},
{
"name": "plan=business без параметров: выбран «Бизнес»",
"ok": true,
"detail": ""
},
{
"name": "plan=business по умолчанию: цена 10 000",
"ok": true,
"detail": "Вашаконфигурация«Бизнес»10000₸/месМагази"
},
{
"name": "plan=business по умолчанию: Магазинов 1 / Складов 1 / Сотрудников 5",
"ok": true,
"detail": ""
},
{
"name": "plan=network: выбран «Сеть»",
"ok": true,
"detail": ""
},
{
"name": "plan=xyz: откат на «Старт»",
"ok": true,
"detail": ""
},
{
"name": "плитка «Старт»: цена «5 000 ₸/мес»",
"ok": true,
"detail": "Старт5000₸/мес"
},
{
"name": "KB: markdown отрисован (h2 в .prose-md)",
"ok": true,
"detail": ""
},
{
"name": "KB: списки отрисованы (ul li)",
"ok": true,
"detail": ""
},
{
"name": "KB billing: нет «Настройки → Подписка → История»",
"ok": true,
"detail": ""
},
{
"name": "KB billing: нет «тг», цены в «₸»",
"ok": true,
"detail": ""
},
{
"name": "блог quick-start: ровно один <h1>",
"ok": true,
"detail": "h1=1"
},
{
"name": "нет чужих систем учёта (МойСклад/1С): главная",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /integrations",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /import",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /kb/faq",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /blog/launch",
"ok": true,
"detail": ""
},
{
"name": "/integrations: Kaspi присутствует (в блоке «в разработке»)",
"ok": true,
"detail": ""
},
{
"name": "/integrations: блок «В разработке» есть",
"ok": true,
"detail": ""
},
{
"name": "/pricing: бейдж «Популярный» на конструкторе",
"ok": true,
"detail": ""
},
{
"name": "/pricing: «90 дней бесплатно» в конструкторе",
"ok": true,
"detail": ""
},
{
"name": "/pricing: якорь #configurator есть",
"ok": true,
"detail": ""
},
{
"name": "/pricing: ползунки с кнопками −/+",
"ok": true,
"detail": ""
},
{
"name": "/changelog: без жаргона (Phase/SuperAdmin/retention/tenant)",
"ok": true,
"detail": ""
},
{
"name": "/status: нет фиктивного «● Работает»",
"ok": true,
"detail": ""
},
{
"name": "/status: честная пометка «в разработке»",
"ok": true,
"detail": ""
},
{
"name": "sitemap: канонические URL со слэшем",
"ok": true,
"detail": ""
},
{
"name": "sitemap: статьи KB включены",
"ok": true,
"detail": ""
},
{
"name": "sitemap: статьи блога включены",
"ok": true,
"detail": ""
},
{
"name": "моб. главная: нет горизонтальной прокрутки",
"ok": true,
"detail": ""
},
{
"name": "моб. шапка: CTA «Начать бесплатно» в одну строку",
"ok": true,
"detail": "h=44px"
},
{
"name": "моб. меню открывается",
"ok": true,
"detail": ""
},
{
"name": "моб. меню: есть «Интеграции»",
"ok": true,
"detail": ""
},
{
"name": "моб. меню: есть «Статус»",
"ok": true,
"detail": ""
},
{
"name": "моб. меню: есть «Что нового» (changelog)",
"ok": true,
"detail": ""
},
{
"name": "моб. «Назад» закрывает меню (не уводит со страницы)",
"ok": true,
"detail": "https://test.food-market.kz/"
},
{
"name": "моб. /pricing: кнопки −/+ у ползунков",
"ok": true,
"detail": ""
}
],
"shots": [
{
"file": "d-signup-business-default.png",
"caption": "/signup?plan=business без параметров — «Бизнес» с дефолтом 1/1/1/5 = 10 000 ₸",
"kind": "Десктоп 1440"
},
{
"file": "d-kb-billing.png",
"caption": "KB «Тарифы и оплата» — markdown-типографика, честный текст (без несуществующего биллинг-UI), ₸",
"kind": "Десктоп 1440"
},
{
"file": "d-kb-faq.png",
"caption": "KB FAQ — типографика, «бесплатный период», TLS без версии",
"kind": "Десктоп 1440"
},
{
"file": "d-blog-quickstart.png",
"caption": "Блог «Быстрый старт» — один заголовок, без чужих систем учёта",
"kind": "Десктоп 1440"
},
{
"file": "d-integrations.png",
"caption": "/integrations — платёжки/маркетплейсы только в «В разработке»; «уже работает» без чужих систем учёта",
"kind": "Десктоп 1440"
},
{
"file": "d-pricing.png",
"caption": "/pricing — конструктор «Бизнес»: бейдж «Популярный», «90 дней бесплатно», кнопки −/+",
"kind": "Десктоп 1440"
},
{
"file": "d-changelog.png",
"caption": "/changelog («Что нового») — человеческие формулировки без внутреннего жаргона",
"kind": "Десктоп 1440"
},
{
"file": "d-status.png",
"caption": "/status — без фиктивных зелёных индикаторов, честно «в разработке» + контакты",
"kind": "Десктоп 1440"
},
{
"file": "m-home-closed.png",
"caption": "Моб. главная 390px — CTA не переносится, гамбургер в шапке",
"kind": "Моб. 390"
},
{
"file": "m-menu-open.png",
"caption": "Моб. меню — полный состав + Статус/Что нового внизу",
"kind": "Моб. 390"
},
{
"file": "m-after-back.png",
"caption": "Моб. после системной «Назад» — панель закрыта, остались на главной",
"kind": "Моб. 390"
},
{
"file": "m-pricing-configurator.png",
"caption": "Моб. конструктор — крупные ручки + кнопки −/+ (значение меняется кнопкой, не случайным свайпом)",
"kind": "Моб. 390"
},
{
"file": "m-signup-business-default.png",
"caption": "Моб. /signup?plan=business без параметров — «Бизнес» с дефолтом 10 000 ₸",
"kind": "Моб. 390"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -17,9 +17,9 @@ const logoUri = `data:image/png;base64,${logoPng.toString('base64')}`
const pages = [ const pages = [
{ file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами и импортом каталога. Бесплатно 90 дней.' }, { file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами и импортом каталога. Бесплатно 90 дней.' },
{ file: 'pos.png', title: 'Касса для Windows и Linux с поддержкой торговых весов', sub: 'Офлайн-режим. Торговые весы и стандартное оборудование.' }, { file: 'pos.png', title: 'Касса для Windows с поддержкой торговых весов', sub: 'Установка за 5 минут. Офлайн-режим. Стандартное оборудование.' },
{ file: 'pricing.png', title: 'Прозрачные тарифы ALL MARKET', sub: 'От 5 000 ₸/мес. Все возможности на любом тарифе. 90 дней бесплатно.' }, { file: 'pricing.png', title: 'Прозрачные тарифы ALL MARKET', sub: 'От 5 000 ₸/мес. Все возможности на любом тарифе. 90 дней триал.' },
{ file: 'import.png', title: 'Перенесите каталог за один клик', sub: 'Импорт каталога из файлов Excel и CSV. До 100 000 товаров. Бесплатно.' }, { file: 'import.png', title: 'Перенесите каталог за один клик', sub: 'Excel, CSV, 1С, МойСклад. До 100 000 товаров. Бесплатно.' },
{ file: 'about.png', title: 'О ALL MARKET', sub: 'Программа учёта розничной торговли, созданная в Казахстане.' }, { file: 'about.png', title: 'О ALL MARKET', sub: 'Программа учёта розничной торговли, созданная в Казахстане.' },
{ file: 'for-grocery.png', title: 'ALL MARKET для продуктового магазина', sub: 'Весовой товар, скоропорт, штрихкоды, учёт остатков.' }, { file: 'for-grocery.png', title: 'ALL MARKET для продуктового магазина', sub: 'Весовой товар, скоропорт, штрихкоды, учёт остатков.' },
{ file: 'for-pharmacy.png', title: 'ALL MARKET для аптеки', sub: 'Серии, сроки годности, рецептурный отпуск, контроль ЛП.' }, { file: 'for-pharmacy.png', title: 'ALL MARKET для аптеки', sub: 'Серии, сроки годности, рецептурный отпуск, контроль ЛП.' },

View file

@ -10,24 +10,16 @@ interface SliderProps {
label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string
} }
function Slider({ label, value, min, max, onChange, suffix }: SliderProps) { function Slider({ label, value, min, max, onChange, suffix }: SliderProps) {
const dec = () => onChange(Math.max(min, value - 1))
const inc = () => onChange(Math.min(max, value + 1))
const btn = 'w-9 h-9 shrink-0 rounded-md border border-slate-300 text-xl leading-none text-slate-700 flex items-center justify-center hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-transparent'
return ( return (
<div> <div>
<div className="flex items-center justify-between gap-3"> <div className="flex items-baseline justify-between">
<label className="text-sm font-medium">{label}</label> <label className="text-sm font-medium">{label}</label>
<div className="flex items-center gap-2"> <span className="text-2xl font-bold tabular-nums">{value}{suffix && <span className="text-sm text-slate-500">{suffix}</span>}</span>
<button type="button" onClick={dec} disabled={value <= min} aria-label={`Уменьшить: ${label}`} className={btn}>−</button>
<span className="text-2xl font-bold tabular-nums w-10 text-center">{value}{suffix && <span className="text-sm text-slate-500">{suffix}</span>}</span>
<button type="button" onClick={inc} disabled={value >= max} aria-label={`Увеличить: ${label}`} className={btn}>+</button>
</div>
</div> </div>
<input <input
type="range" min={min} max={max} value={value} type="range" min={min} max={max} value={value}
onChange={(e) => onChange(Number(e.target.value))} onChange={(e) => onChange(Number(e.target.value))}
className="fm-range w-full mt-2 accent-[#00B207]" className="w-full mt-2 accent-[#00B207]"
style={{ touchAction: 'pan-y' }}
/> />
<div className="flex justify-between text-xs text-slate-400 mt-1"><span>{min}</span><span>{max}</span></div> <div className="flex justify-between text-xs text-slate-400 mt-1"><span>{min}</span><span>{max}</span></div>
</div> </div>
@ -46,11 +38,10 @@ export default function BusinessTariffBuilder() {
const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}` const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}`
return ( return (
<div className="relative rounded-xl border-2 border-brand bg-white p-6"> <div className="rounded-xl border-2 border-brand bg-white p-6">
<span className="absolute -top-3 left-1/2 -translate-x-1/2 bg-brand text-white text-xs px-2 py-0.5 rounded-full">Популярный</span>
<div className="flex items-baseline justify-between mb-6"> <div className="flex items-baseline justify-between mb-6">
<div> <div>
<h3 className="text-lg font-bold">Конструктор «Бизнес»</h3> <h3 class="text-lg font-bold">Конструктор «Бизнес»</h3>
<p className="text-xs text-slate-500 mt-0.5">База 10 000 ₸ + ваши параметры</p> <p className="text-xs text-slate-500 mt-0.5">База 10 000 ₸ + ваши параметры</p>
</div> </div>
<div className="text-right"> <div className="text-right">
@ -72,7 +63,6 @@ export default function BusinessTariffBuilder() {
<a href={signupHref} className="block text-center mt-6 px-4 py-3 bg-brand text-white rounded-md font-semibold hover:bg-[#009305]"> <a href={signupHref} className="block text-center mt-6 px-4 py-3 bg-brand text-white rounded-md font-semibold hover:bg-[#009305]">
Выбрать конфигурацию · {fmt.format(total)} ₸/мес Выбрать конфигурацию · {fmt.format(total)} ₸/мес
</a> </a>
<p className="text-xs text-center text-slate-500 mt-2">90 дней бесплатно, без карты</p>
</div> </div>
) )
} }

View file

@ -25,7 +25,7 @@ import Logo from './Logo.astro'
<ul class="space-y-1.5"> <ul class="space-y-1.5">
<li><a href="/kb" class="block py-2 text-slate-700 hover:text-brand">База знаний</a></li> <li><a href="/kb" class="block py-2 text-slate-700 hover:text-brand">База знаний</a></li>
<li><a href="/blog" class="block py-2 text-slate-700 hover:text-brand">Блог</a></li> <li><a href="/blog" class="block py-2 text-slate-700 hover:text-brand">Блог</a></li>
<li><a href="/status" class="block py-2 text-slate-700 hover:text-brand">Статус</a></li> <li><a href="/status" class="block py-2 text-slate-700 hover:text-brand">Status</a></li>
<li><a href="/changelog" class="block py-2 text-slate-700 hover:text-brand">Changelog</a></li> <li><a href="/changelog" class="block py-2 text-slate-700 hover:text-brand">Changelog</a></li>
</ul> </ul>
</div> </div>

View file

@ -6,33 +6,27 @@ const nav = [
{ href: '/pos', label: 'Касса' }, { href: '/pos', label: 'Касса' },
{ href: '/for-grocery', label: 'Для продуктовых' }, { href: '/for-grocery', label: 'Для продуктовых' },
{ href: '/import', label: 'Импорт' }, { href: '/import', label: 'Импорт' },
{ href: '/integrations', label: 'Интеграции' },
{ href: '/pricing', label: 'Тарифы' }, { href: '/pricing', label: 'Тарифы' },
{ href: '/kb', label: 'База знаний' }, { href: '/kb', label: 'База знаний' },
{ href: '/blog', label: 'Блог' }, { href: '/blog', label: 'Блог' },
] ]
// Вторичные ссылки — внизу мобильной панели (в десктоп-навигацию не выносим).
const secondary = [
{ href: '/status', label: 'Статус' },
{ href: '/changelog', label: 'Что нового' },
]
--- ---
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-slate-200"> <header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-slate-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
<a href="/" class="flex items-center gap-2"><Logo /></a> <a href="/" class="flex items-center gap-2"><Logo /></a>
<nav class="hidden xl:flex items-center gap-1 text-sm"> <nav class="hidden lg:flex items-center gap-1 text-sm">
{nav.map((i) => <a href={i.href} class="px-3 py-2 hover:text-brand transition">{i.label}</a>)} {nav.map((i) => <a href={i.href} class="px-3 py-2 hover:text-brand transition">{i.label}</a>)}
</nav> </nav>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2">
<a href={`${APP_URL}/login`} class="hidden sm:flex px-3 min-h-[44px] items-center text-sm text-slate-700 hover:text-brand whitespace-nowrap">Войти</a> <a href={`${APP_URL}/login`} class="px-3 min-h-[44px] flex items-center text-sm text-slate-700 hover:text-brand">Войти</a>
<a href="/signup" class="px-3 min-h-[44px] flex items-center text-sm font-medium bg-brand text-white rounded-md hover:bg-brand-hover whitespace-nowrap">Начать бесплатно</a> <a href="/signup" class="px-3 min-h-[44px] flex items-center text-sm font-medium bg-brand text-white rounded-md hover:bg-brand-hover">Начать бесплатно</a>
<!-- Гамбургер: планшет/мобильный и средний десктоп (полное меню — от xl) --> <!-- Гамбургер: только на планшете/мобильном (десктоп-меню скрыто < lg) -->
<button <button
id="mobile-menu-btn" id="mobile-menu-btn"
type="button" type="button"
class="xl:hidden inline-flex items-center justify-center w-11 h-11 -mr-1 rounded-md text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-brand" class="lg:hidden inline-flex items-center justify-center w-11 h-11 -mr-1 rounded-md text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-brand"
aria-label="Меню" aria-label="Меню"
aria-controls="mobile-menu" aria-controls="mobile-menu"
aria-expanded="false" aria-expanded="false"
@ -51,9 +45,6 @@ const secondary = [
<a href={`${APP_URL}/login`} class="flex items-center justify-center min-h-[44px] px-3 rounded-md text-sm font-medium border border-slate-300 text-slate-700 hover:bg-slate-50">Войти</a> <a href={`${APP_URL}/login`} class="flex items-center justify-center min-h-[44px] px-3 rounded-md text-sm font-medium border border-slate-300 text-slate-700 hover:bg-slate-50">Войти</a>
<a href="/signup" class="flex items-center justify-center min-h-[44px] px-3 rounded-md text-sm font-medium bg-brand text-white hover:bg-brand-hover">Начать бесплатно</a> <a href="/signup" class="flex items-center justify-center min-h-[44px] px-3 rounded-md text-sm font-medium bg-brand text-white hover:bg-brand-hover">Начать бесплатно</a>
</div> </div>
<div class="mt-2 pt-2 border-t border-slate-100 flex flex-wrap gap-x-4">
{secondary.map((i) => <a href={i.href} class="flex items-center min-h-[44px] px-3 rounded-md text-sm text-slate-500 hover:text-brand">{i.label}</a>)}
</div>
</div> </div>
</nav> </nav>
</header> </header>
@ -64,32 +55,16 @@ const secondary = [
const iconOpen = document.getElementById('mobile-menu-icon-open') const iconOpen = document.getElementById('mobile-menu-icon-open')
const iconClose = document.getElementById('mobile-menu-icon-close') const iconClose = document.getElementById('mobile-menu-icon-close')
if (btn && menu) { if (btn && menu) {
// Пока панель открыта — в истории висит наша запись, чтобы системная кнопка const setOpen = (open: boolean) => {
// «Назад» (Android) закрывала меню, а не уводила со страницы.
let pushed = false
const setOpen = (open: boolean, fromPop = false) => {
menu.classList.toggle('hidden', !open) menu.classList.toggle('hidden', !open)
btn.setAttribute('aria-expanded', String(open)) btn.setAttribute('aria-expanded', String(open))
iconOpen?.classList.toggle('hidden', open) iconOpen?.classList.toggle('hidden', open)
iconClose?.classList.toggle('hidden', !open) iconClose?.classList.toggle('hidden', !open)
if (open) {
if (!pushed) { history.pushState({ menu: true }, ''); pushed = true }
} else if (pushed && !fromPop) {
// закрыли сами (Esc/крестик/тап/resize) — снимаем запись из истории
pushed = false
history.back()
} else {
pushed = false
}
} }
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation() e.stopPropagation()
setOpen(menu.classList.contains('hidden')) setOpen(menu.classList.contains('hidden'))
}) })
// системная кнопка «Назад» / popstate — закрыть панель
window.addEventListener('popstate', () => {
if (!menu.classList.contains('hidden')) setOpen(false, true)
})
// закрытие по тапу вне меню // закрытие по тапу вне меню
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (menu.classList.contains('hidden')) return if (menu.classList.contains('hidden')) return
@ -101,6 +76,6 @@ const secondary = [
if (e.key === 'Escape' && !menu.classList.contains('hidden')) { setOpen(false); (btn as HTMLButtonElement).focus() } if (e.key === 'Escape' && !menu.classList.contains('hidden')) { setOpen(false); (btn as HTMLButtonElement).focus() }
}) })
// при переходе к десктопной ширине — сбросить состояние // при переходе к десктопной ширине — сбросить состояние
window.matchMedia('(min-width: 1280px)').addEventListener('change', (m) => { if (m.matches && !menu.classList.contains('hidden')) setOpen(false) }) window.matchMedia('(min-width: 1024px)').addEventListener('change', (m) => { if (m.matches) setOpen(false) })
} }
</script> </script>

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState } from 'react'
import { validateEmail, validatePassword, validatePhone } from '@/lib/validation' import { validateEmail, validatePassword, validatePhone } from '@/lib/validation'
import { PhoneInput } from '@/components/PhoneInput' import { PhoneInput } from '@/components/PhoneInput'
import { PRICING, businessPrice, clamp, fmtKzt, type BusinessConfig } from '@/lib/pricing' import { PRICING, businessPrice, clamp, fmtKzt, type BusinessConfig } from '@/lib/pricing'
@ -42,44 +42,16 @@ interface Props {
type FieldErrors = Partial<Record<'email' | 'password' | 'orgName' | 'phone' | 'agree', string>> type FieldErrors = Partial<Record<'email' | 'password' | 'orgName' | 'phone' | 'agree', string>>
const DEFAULT_CONFIG: BusinessConfig = {
shops: PRICING.shops.min, cashes: PRICING.cashes.min,
warehouses: PRICING.warehouses.min, employees: PRICING.employees.min,
}
export default function SignupForm({ defaultPlan = 'start' }: Props) { export default function SignupForm({ defaultPlan = 'start' }: Props) {
// Первый клиентский рендер ДОЛЖЕН совпасть с SSR-разметкой, иначе React кидает // Разбор ?plan= и конфигурации из URL — один раз при инициализации (клиент).
// #418 (hydration mismatch). Поэтому стартуем из props/дефолтов, а ?plan= и const [sel] = useState(() => readUrlSelection(defaultPlan))
// конфигуратор читаем из URL уже в useEffect — после гидрации. const [config] = useState<BusinessConfig>(sel.config)
const initialPlan: PlanId = (VALID_PLANS as readonly string[]).includes(defaultPlan) ? (defaultPlan as PlanId) : 'start' const hasConfig = sel.hasConfig
const [plan, setPlan] = useState<PlanId>(initialPlan)
const [config, setConfig] = useState<BusinessConfig>(DEFAULT_CONFIG)
useEffect(() => {
const sel = readUrlSelection(defaultPlan)
setPlan(sel.plan)
// Если план «Бизнес» без параметров конфигуратора — показываем дефолтную
// конфигурацию (1/1/1/5 = 10 000 ₸), а не пустой тариф.
setConfig(sel.hasConfig ? sel.config : DEFAULT_CONFIG)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Клик по плитке тарифа — обновляем URL (?plan=…) без перезагрузки, чтобы
// ссылку можно было сохранить/поделиться.
const selectPlan = (id: PlanId) => {
setPlan(id)
if (typeof window !== 'undefined') {
const q = new URLSearchParams(window.location.search)
q.set('plan', id)
const qs = q.toString()
history.replaceState(null, '', `${window.location.pathname}${qs ? '?' + qs : ''}`)
}
}
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [orgName, setOrgName] = useState('') const [orgName, setOrgName] = useState('')
const [phone, setPhone] = useState('') const [phone, setPhone] = useState('')
const [plan, setPlan] = useState<string>(sel.plan)
const businessTotal = businessPrice(config) const businessTotal = businessPrice(config)
const [agree, setAgree] = useState(false) const [agree, setAgree] = useState(false)
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
@ -111,7 +83,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
// принимать ли — отдельно, см. отчёт site-fixes). // принимать ли — отдельно, см. отчёт site-fixes).
body: JSON.stringify({ body: JSON.stringify({
email, password, organizationName: orgName, phone: phone || null, plan, email, password, organizationName: orgName, phone: phone || null, plan,
...(plan === 'business' ...(plan === 'business' && hasConfig
? { shops: config.shops, cashes: config.cashes, warehouses: config.warehouses, employees: config.employees } ? { shops: config.shops, cashes: config.cashes, warehouses: config.warehouses, employees: config.employees }
: {}), : {}),
}), }),
@ -223,18 +195,18 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
<div className="text-sm font-medium mb-2">Тариф</div> <div className="text-sm font-medium mb-2">Тариф</div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{[ {[
{ id: 'start', name: 'Старт', price: `${fmtKzt(PRICING.startPrice)}/мес` }, { id: 'start', name: 'Старт', price: fmtKzt(PRICING.startPrice) },
{ id: 'business', name: 'Бизнес', price: `${fmtKzt(businessTotal)}/мес` }, { id: 'business', name: 'Бизнес', price: hasConfig ? `${fmtKzt(businessTotal)}/мес` : 'от 10 000 ₸' },
{ id: 'network', name: 'Сеть', price: 'по запросу' }, { id: 'network', name: 'Сеть', price: 'по запросу' },
].map((p) => ( ].map((p) => (
<label key={p.id} className={`p-2.5 rounded-md border cursor-pointer text-center text-sm ${plan === p.id ? 'border-brand bg-brand/5' : 'border-slate-200'}`}> <label key={p.id} className={`p-2.5 rounded-md border cursor-pointer text-center text-sm ${plan === p.id ? 'border-brand bg-brand/5' : 'border-slate-200'}`}>
<input type="radio" name="plan" className="hidden" checked={plan === p.id} onChange={() => selectPlan(p.id as PlanId)} /> <input type="radio" name="plan" className="hidden" checked={plan === p.id} onChange={() => setPlan(p.id)} />
<div className="font-semibold">{p.name}</div> <div className="font-semibold">{p.name}</div>
<div className="text-xs text-slate-500 whitespace-nowrap">{p.price}</div> <div className="text-xs text-slate-500">{p.price}</div>
</label> </label>
))} ))}
</div> </div>
{plan === 'business' && ( {plan === 'business' && hasConfig && (
<div className="mt-3 rounded-md border border-brand/30 bg-brand/5 p-3 text-sm"> <div className="mt-3 rounded-md border border-brand/30 bg-brand/5 p-3 text-sm">
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<span className="font-medium">Ваша конфигурация «Бизнес»</span> <span className="font-medium">Ваша конфигурация «Бизнес»</span>

View file

@ -7,6 +7,8 @@ description: "Сегодня мы запускаем ALL MARKET — програ
cover_image: /photos/blog-launch.jpg cover_image: /photos/blog-launch.jpg
--- ---
# ALL MARKET запустился
Сегодня мы открываем публичный доступ к ALL MARKET — программе учёта розничной торговли, созданной командой казахстанских разработчиков и практиков ритейла. Сегодня мы открываем публичный доступ к ALL MARKET — программе учёта розничной торговли, созданной командой казахстанских разработчиков и практиков ритейла.
## Что мы сделали ## Что мы сделали
@ -16,25 +18,25 @@ cover_image: /photos/blog-launch.jpg
- **работать с первого клика** — без долгой настройки и платных консультаций - **работать с первого клика** — без долгой настройки и платных консультаций
- **стоить честно** — без скрытых доплат за модули, которые должны быть в базе - **стоить честно** — без скрытых доплат за модули, которые должны быть в базе
- **поддерживать локальное железо** — торговые весы нативно, без переходников и сторонних драйверов - **поддерживать локальное железо** — торговые весы нативно, без переходников и сторонних драйверов
- **позволять попробовать без риска** — честный бесплатный период, а не «14 дней с привязкой карты» - **позволять попробовать без риска** — настоящий триал, а не «14 дней с привязкой карты»
Мы воплотили эти принципы в ALL MARKET. Мы воплотили эти принципы в ALL MARKET.
## Что вы получаете ## Что вы получаете
- **5 000 ₸/мес** за тариф «Старт» — 1 магазин, 1 касса, 1 склад, без лимита товаров - **5 000 тг/мес** за тариф «Старт» — 1 магазин, 1 касса, 1 склад, без лимита товаров
- **90 дней бесплатно** — без карты и без обязательств - **90 дней триала** без карты, без обязательств, со всеми возможностями
- **Касса для Windows и Linux** с нативной поддержкой торговых весов (приложение для скачивания — скоро) - **Касса для Windows** с нативной поддержкой торговых весов
- **Импорт каталога** из файлов Excel/CSV за один клик - **Импорт каталога** из Excel, CSV или API за один клик
- **Без доплат за модули** — учёт денег, отчёты и работа с клиентами входят в тариф - **Все возможности на любом тарифе** — никаких доплат за CRM, лояльность, финансы или сценарии
## Что дальше ## Что дальше
Сейчас мы открываем регистрацию и принимаем первых клиентов. Будем активно собирать обратную связь и улучшать продукт каждую неделю. Сейчас мы открываем регистрацию и принимаем первых клиентов. Будем активно собирать обратную связь и улучшать продукт каждую неделю.
В ближайших планах: В ближайших планах:
- Приём оплат картой и онлайн-касса (ОФД) — в разработке - Интеграция с Kaspi Magazin
- Маркетплейсы — в разработке - Marketplace интеграции (Wildberries, Ozon — Q4 2026)
- Мобильное приложение для руководителя - Мобильное приложение для руководителя
[Зарегистрироваться →](/signup) [Зарегистрироваться →](/signup)

View file

@ -4,9 +4,11 @@ date: 2026-04-30
author: Команда ALL MARKET author: Команда ALL MARKET
category: guide category: guide
description: "Пошаговая инструкция первого запуска: от регистрации до первой продажи." description: "Пошаговая инструкция первого запуска: от регистрации до первой продажи."
cover_image: /screenshots/catalog.webp cover_image: /photos/blog-quickstart.jpg
--- ---
# Запустите магазин в ALL MARKET за 15 минут
Часто новые SaaS-сервисы требуют дней на настройку и обучение. Мы спроектировали ALL MARKET так, чтобы вы могли начать продавать в день регистрации. Часто новые SaaS-сервисы требуют дней на настройку и обучение. Мы спроектировали ALL MARKET так, чтобы вы могли начать продавать в день регистрации.
Реальное время от пустого экрана до первого пробитого чека — **15 минут**. Реальное время от пустого экрана до первого пробитого чека — **15 минут**.
@ -30,7 +32,9 @@ cover_image: /screenshots/catalog.webp
- Сопоставьте колонки (название, штрихкод, цена) — ALL MARKET подскажет правильное соответствие - Сопоставьте колонки (название, штрихкод, цена) — ALL MARKET подскажет правильное соответствие
- Запустите импорт — 5 000 SKU перенесутся за минуту - Запустите импорт — 5 000 SKU перенесутся за минуту
**Б. Импорт из других систем учёта** — в разработке: появится в личном кабинете магазина. Пока используйте импорт из файла Excel/CSV. **Б. Импорт через API** — если работаете в системе с открытым API:
- «Импорт» → выберите систему → введите API-токен
- Запустите автоматическую синхронизацию
**В. Вручную** — если у вас 10-50 товаров: **В. Вручную** — если у вас 10-50 товаров:
- «Товары» → «Добавить» - «Товары» → «Добавить»
@ -38,10 +42,10 @@ cover_image: /screenshots/catalog.webp
## Шаг 3. Установите кассу (5 минут) ## Шаг 3. Установите кассу (5 минут)
Приложение кассы для Windows и Linux — скоро (появится в разделе «Касса»). Приложение кассы для Windows — скоро (появится в разделе «Касса»).
1. Когда касса будет доступна: укажите адрес сервера и войдите логином 1. Запустите .msi файл — нажмите «Далее» три раза
2. Задайте PIN кассира — касса привяжется к точке продаж 2. Введите ключ организации (показан в админке)
3. Подключите торговые весы через USB-COM 3. Подключите торговые весы через USB-COM
4. Подключите сканер штрихкодов через USB 4. Подключите сканер штрихкодов через USB
5. Подключите чековый принтер 5. Подключите чековый принтер
@ -63,8 +67,8 @@ cover_image: /screenshots/catalog.webp
После первой продажи: После первой продажи:
- Добавьте остальных кассиров — «Сотрудники» → «Добавить» - Добавьте остальных кассиров — «Сотрудники» → «Добавить»
- Запустите акции и скидки — раздел «Акции и скидки» - Настройте программу лояльности — «Лояльность» → «Создать программу»
- Программа лояльности — в расширенном режиме (раздел «Лояльность») - Запустите акции и скидки — «Маркетинг» → «Акции»
Полная документация в нашей [Базе знаний](/kb). Полная документация в нашей [Базе знаний](/kb).

View file

@ -4,41 +4,60 @@ category: Биллинг
order: 4 order: 4
--- ---
Информация о тарифах ALL MARKET и порядке оплаты. Полная информация о подписках ALL MARKET и порядке оплаты.
## Тарифы ## Тарифы
См. полную таблицу на [странице тарифов](/pricing). См. полную таблицу на [странице тарифов](/pricing).
Кратко — первые 90 дней бесплатно, без банковской карты: Кратко:
Все тарифы — первые 90 дней бесплатно, без банковской карты.
- **Старт** — 5 000 ₸/мес, 1 магазин, 1 касса, 1 склад, до 2 сотрудников - **Старт** — 5 000 тг/мес, 1 магазин, 1 касса, 1 склад, до 2 сотрудников
- **Бизнес** — от 10 000 ₸/мес, конструктор (до 3 магазинов / 5 касс / 5 складов) - **Бизнес** — от 10 000 тг/мес, конструктор (до 3 магазинов / 5 касс / 5 складов)
- **Сеть** — по запросу, без ограничений - **Сеть** — по запросу, без ограничений
## Способы оплаты ## Способы оплаты
На время раннего доступа оплату оформляем вручную — напишите на billing@food-market.kz, подскажем удобный способ. Самостоятельная оплата в личном кабинете и приём оплат картой онлайн — в разработке. ### Для физических лиц и ИП
Способ оплаты подписки уточняется — свяжитесь с нами: support@food-market.kz. Приём оплат картой онлайн — в разработке.
### Для юридических лиц ### Для юридических лиц
- Безналичный расчёт по счёту - Безналичный расчёт по счёту-фактуре
- Чтобы выставить счёт, напишите на billing@food-market.kz с реквизитами вашего ТОО - Email счёта-фактуры — billing@food-market.kz с реквизитами вашего ТОО
- Срок оплаты — 5 рабочих дней с даты выставления счёта
- Доступ продлевается после поступления средств на расчётный счёт - Доступ продлевается после поступления средств на расчётный счёт
## Изменение тарифа ## Изменение тарифа
Тариф можно сменить в любой момент — напишите в поддержку (support@food-market.kz), и мы пересчитаем стоимость за оставшийся оплаченный период. Порядок перерасчёта при повышении и понижении тарифа уточняйте в поддержке. В любой момент через «Настройки» → «Подписка».
**Повышение тарифа** (например Старт → Бизнес):
- Доплата за оставшийся период текущего тарифа
- Расчёт: (новая цена − старая цена) × оставшиеся дни / 30
- Доступ к новым возможностям — сразу после оплаты
**Понижение тарифа** (Бизнес → Старт):
- Кредит на следующий месяц
- Расчёт: (старая цена − новая цена) × оставшиеся дни / 30
- Кредит сгорает через 6 месяцев если не использован
## Истечение оплаченного периода ## Истечение оплаченного периода
Мы заранее напомним об окончании оплаченного периода. Если оплата не поступит: За 7 дней до окончания:
- Email-напоминание о необходимости продления
- Баннер в админке
- Доступ к админке сохраняется в режиме просмотра В день окончания (если не оплачено):
- Доступ к админке сохраняется (только просмотр)
- Касса блокируется (нельзя открыть смену) - Касса блокируется (нельзя открыть смену)
- Экспорт данных продолжает работать - Импорт и экспорт работают
Контент сохраняется 30 календарных дней (см. оферту, п. 5.3/11.3), после чего может быть удалён. Через 30 дней без оплаты:
- Email с финальным предупреждением
- Контент сохраняется 30 календарных дней (см. оферту, п. 5.3/11.3), после чего может быть удалён
## Возврат средств ## Возврат средств
@ -46,17 +65,19 @@ order: 4
Заявка: support@food-market.kz с указанием причины и реквизитов для возврата. Заявка: support@food-market.kz с указанием причины и реквизитов для возврата.
## История платежей и документы ## История платежей и счёта
Историю платежей и копии счетов можно запросить в поддержке — billing@food-market.kz. Личный кабинет с историей платежей и скачиванием документов — в разработке. «Настройки» → «Подписка» → «История»:
- Список всех платежей за период
- Скачивание счетов-фактур (для юрлиц)
- Скачивание чеков об оплате (для физлиц)
## Корпоративные клиенты ## Корпоративные клиенты
Для тарифа «Сеть» и сетей с несколькими магазинами: Для тарифа «Сеть» и сетей с >3 магазинами:
- Индивидуальный договор - Индивидуальный договор
- Безналичный расчёт - Безналичный расчёт с НДС
- Персональный менеджер - SLA по доступности и скорости поддержки
- Индивидуальные условия поддержки - Выделенный менеджер
Заявка: sales@food-market.kz Заявка: sales@food-market.kz

View file

@ -4,16 +4,16 @@ category: Общее
order: 5 order: 5
--- ---
## Регистрация и бесплатный период ## Регистрация и триал
### Сколько длится бесплатный период? ### Сколько длится триал?
90 дней с момента регистрации. Без необходимости вводить карту. 90 дней с момента регистрации. Без необходимости вводить карту.
### Что произойдёт после 90 дней? ### Что произойдёт после 90 дней?
Доступ к админке сохранится в режиме просмотра. Касса заблокируется. Если в течение 30 дней не оплатите — данные удаляются. Доступ к админке сохранится в режиме просмотра. Касса заблокируется. Если в течение 30 дней не оплатите — данные удаляются.
### Можно ли активировать бесплатный период повторно? ### Можно ли активировать триал повторно?
Нет, бесплатный период предоставляется один раз на email. После окончания — нужно выбрать платный тариф. Нет, триал предоставляется один раз на email. После окончания — нужно выбрать платный тариф.
### Можно ли пользоваться без email? ### Можно ли пользоваться без email?
Нет, email обязателен — это ваш логин и канал уведомлений. Нет, email обязателен — это ваш логин и канал уведомлений.
@ -21,7 +21,7 @@ order: 5
## Тарифы и оплата ## Тарифы и оплата
### Сколько стоит самый дешёвый тариф? ### Сколько стоит самый дешёвый тариф?
Старт — 5 000 ₸/мес. Включает 1 магазин, 1 кассу, 1 склад, до 2 сотрудников, без лимита товаров. Старт — 5 000 тг/мес. Включает 1 магазин, 1 кассу, 1 склад, до 2 сотрудников, без лимита товаров.
### Что входит в тариф Бизнес? ### Что входит в тариф Бизнес?
Конструктор: до 3 магазинов, до 5 касс, до 5 складов. Цена считается по формуле base 10 000 + 2 000 за магазин + 500 за кассу + 500 за склад. Конструктор: до 3 магазинов, до 5 касс, до 5 складов. Цена считается по формуле base 10 000 + 2 000 за магазин + 500 за кассу + 500 за склад.
@ -52,13 +52,13 @@ order: 5
## Данные и безопасность ## Данные и безопасность
### Где хранятся мои данные? ### Где хранятся мои данные?
На серверах в Казахстане. Делаем регулярные резервные копии. На серверах в Казахстане. Резервные копии каждые 24 часа.
### Можно ли экспортировать данные? ### Можно ли экспортировать данные?
Да, в любой момент. Экспорт каталога, контрагентов, операций в Excel/CSV. Да, в любой момент. Экспорт каталога, контрагентов, операций в Excel/CSV.
### Безопасно ли вводить данные клиентов? ### Безопасно ли вводить данные клиентов?
Да. Данные шифруются при передаче (TLS) и при хранении. Персональные данные обрабатываем в соответствии с законодательством РК. Да. Данные шифруются при передаче (TLS 1.3) и при хранении. Соответствие ЗРК «О персональных данных».
### Что будет с моими данными если я закрою аккаунт? ### Что будет с моими данными если я закрою аккаунт?
30 дней хранятся на случай восстановления, потом удаляются безвозвратно. 30 дней хранятся на случай восстановления, потом удаляются безвозвратно.
@ -66,7 +66,7 @@ order: 5
## Импорт и миграция ## Импорт и миграция
### Можно перенести данные из другой системы учёта? ### Можно перенести данные из другой системы учёта?
Да. Поддерживаем импорт каталога из файла Excel/CSV (название, штрихкод, цена, остаток, группа). Импорт из других систем учёта — в разработке (появится в личном кабинете). Подробнее — на странице [/import](/import). Да. Поддерживаем импорт из файла Excel/CSV, выгрузку 1С (сохранённую в CSV) и перенос из МойСклад по токену. Подробнее — на странице [/import](/import).
### Какой объём каталога переносится? ### Какой объём каталога переносится?
До 100 000 товаров за один импорт. Каталог 10 000 позиций обычно переносится за 5–10 минут. До 100 000 товаров за один импорт. Каталог 10 000 позиций обычно переносится за 5–10 минут.
@ -85,7 +85,7 @@ order: 5
Ежедневно с 09:00 до 18:00 по Астане. В выходные — задержка ответа до 24 часов. Ежедневно с 09:00 до 18:00 по Астане. В выходные — задержка ответа до 24 часов.
### Платная ли поддержка? ### Платная ли поддержка?
Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка — в тарифах Бизнес и Сеть. Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка с SLA — в тарифах Бизнес и Сеть.
### Есть ли обучение и внедрение? ### Есть ли обучение и внедрение?
Большинство клиентов справляются самостоятельно с помощью базы знаний. Если нужна помощь с настройкой или импортом — напишите в поддержку, поможем. Большинство клиентов справляются самостоятельно с помощью базы знаний. Если нужна помощь с настройкой или импортом — напишите в поддержку, поможем.

View file

@ -4,29 +4,38 @@ category: Касса
order: 3 order: 3
--- ---
Касса ALL MARKET — приложение для Windows и Linux. Работает и офлайн. Полная инструкция установки и настройки кассы ALL MARKET для Windows. Время — 15-20 минут.
## Системные требования ## Системные требования
- **ОС:** Windows 10/11 (64-bit) или Linux x64 - **ОС:** Windows 10 (64-bit) или Windows 11
- **CPU:** любой Intel/AMD от 2015 года - **CPU:** любой Intel/AMD от 2015 года
- **RAM:** 4 GB минимум, 8 GB рекомендуется - **RAM:** 4 GB минимум, 8 GB рекомендуется
- **Диск:** 5 GB свободного места - **Диск:** 5 GB свободного места
- **Интернет:** опционально (касса работает офлайн) - **Интернет:** опционально (касса работает офлайн)
## Скачивание установщика
1. Войдите в админку ALL MARKET
2. Откройте раздел «Касса» в боковом меню
3. Приложение кассы для Windows — скоро (кнопка появится в разделе «Касса»)
4. Сохраните файл `food-market-pos-setup.msi` (~80 МБ)
## Установка ## Установка
Приложение кассы для скачивания — **скоро**: кнопка появится в админке в разделе «Касса». 1. Запустите скачанный `.msi` файл
Здесь опишем шаги по мере готовности. Если нужна касса уже сейчас — напишите в поддержку. 2. Если Windows покажет предупреждение — нажмите «Подробнее» → «Выполнить в любом случае» (мы скоро подпишем сертификатом)
3. В мастере установки нажмите «Далее» три раза, затем «Установить»
4. Подождите 1-2 минуты пока завершится установка
5. По завершении — запустите кассу с рабочего стола
## Вход и привязка к точке ## Активация
При первом запуске касса спросит адрес сервера и логин: При первом запуске касса попросит ввести **ключ организации**.
1. Укажите адрес сервера (например, `https://admin.food-market.kz`) 1. В админке ALL MARKET: «Касса» → «Ключ организации» — скопируйте 32-символьный код
2. Войдите логином сотрудника — касса привяжется к точке продаж 2. В кассе — вставьте ключ, нажмите «Активировать»
3. Дальше — быстрый вход по **PIN** кассира на этом устройстве 3. Касса синхронизирует каталог товаров с облаком (~30 секунд для 5000 SKU)
4. Касса загрузит каталог товаров для работы офлайн
## Подключение оборудования ## Подключение оборудования
@ -56,7 +65,7 @@ order: 3
4. Выберите принтер из выпадающего списка 4. Выберите принтер из выпадающего списка
5. Нажмите «Тестовая печать» — должен распечататься тестовый чек 5. Нажмите «Тестовая печать» — должен распечататься тестовый чек
Поддерживаются чековые принтеры 58/80 мм (ESC/POS). Поддерживаемые принтеры: любые ОКП РК-совместимые модели.
## Открытие смены и первая продажа ## Открытие смены и первая продажа

View file

@ -20,7 +20,7 @@ import BaseLayout from '@/layouts/BaseLayout.astro'
<li>Касса с торговыми весами — для Windows и Linux, работает без интернета.</li> <li>Касса с торговыми весами — для Windows и Linux, работает без интернета.</li>
<li>Склад: приёмки, инвентаризация, списание, оприходование, перемещение.</li> <li>Склад: приёмки, инвентаризация, списание, оприходование, перемещение.</li>
<li>Отчёты по продажам, остаткам и прибыли.</li> <li>Отчёты по продажам, остаткам и прибыли.</li>
<li>Перенос каталога из файлов Excel/CSV.</li> <li>Перенос каталога из МойСклад и из файла CSV (в том числе выгрузки 1С).</li>
</ul> </ul>
<h2>Что в разработке</h2> <h2>Что в разработке</h2>

View file

@ -24,3 +24,19 @@ const fmt = post.data.date.toLocaleDateString('ru-KZ', { day: '2-digit', month:
<div class="prose-md mt-8"><Content /></div> <div class="prose-md mt-8"><Content /></div>
</article> </article>
</BaseLayout> </BaseLayout>
<style is:global>
.prose-md { font-size: 16px; line-height: 1.75; color: #1e293b; }
.prose-md h2 { font-size: 1.5rem; font-weight: 700; margin-top: 2rem; margin-bottom: 0.75rem; }
.prose-md h3 { font-size: 1.125rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.prose-md p { margin: 1rem 0; }
.prose-md ul, .prose-md ol { margin: 1rem 0 1rem 1.5rem; }
.prose-md li { margin: 0.35rem 0; }
.prose-md a { color: #00B207; text-decoration: underline; }
.prose-md blockquote { border-left: 3px solid #00B207; background: #E8F5E9; padding: 0.75rem 1rem; margin: 1.25rem 0; font-style: italic; color: #1e293b; }
.prose-md code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
.prose-md hr { margin: 2rem 0; border: 0; border-top: 1px solid #e2e8f0; }
.prose-md strong { font-weight: 600; }
.prose-md table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.95rem; }
.prose-md th, .prose-md td { border: 1px solid #e2e8f0; padding: 0.5rem 0.75rem; text-align: left; }
.prose-md th { background: #f8fafc; font-weight: 600; }
</style>

View file

@ -1,17 +1,17 @@
--- ---
import BaseLayout from '@/layouts/BaseLayout.astro' import BaseLayout from '@/layouts/BaseLayout.astro'
const releases = [ const releases = [
{ date: '2026-04', title: 'Публичный сайт и регистрация', items: ['Сайт ALL MARKET','Самостоятельная регистрация магазина','Тарифы: Старт, конструктор «Бизнес», Сеть'] }, { date: '2026-04', title: 'Phase 6 — Публичный сайт', items: ['Маркетинговый сайт food-market.kz','Регистрация без админа платформы','Тарифы Старт / Бизнес-конструктор / Сеть'] },
{ date: '2026-04', title: 'Управление и безопасность', items: ['Управление организациями','Журнал действий','Режим просмотра «открыть как…»','Правки с указанием причины и историей изменений','Настраиваемый срок хранения данных'] }, { date: '2026-04', title: 'Phase 4 — SuperAdmin консоль', items: ['Управление организациями','Журнал действий','Read-only «открыть как…»','Edit-mode с reason + audit-trail','Настраиваемый retention period'] },
{ date: '2026-04', title: 'Цены, сотрудники и роли', items: ['Гибкие типы цен (розница, опт)','Сотрудники и права доступа','Готовые роли: администратор и кассир'] }, { date: '2026-04', title: 'Phase 3 — Цены и роли', items: ['Расширенная модель цен','Сотрудники и роли','Системные роли (Администратор, Кассир)'] },
{ date: '2026-04', title: 'Закупки и приёмки', items: ['Приёмка товара со сканером','Автоматический расчёт себестоимости','Быстрое добавление позиций'] }, { date: '2026-04', title: 'Phase 2 — Закупки и приёмки', items: ['Документ приёмки со сканером','Скользящее среднее себестоимости','Inline quick-add позиций'] },
{ date: '2026-03', title: 'Каталог и учёт по магазинам', items: ['Данные каждого магазина изолированы друг от друга','Товары, группы, штрихкоды, остатки','Импорт каталога из файлов Excel/CSV'] }, { date: '2026-03', title: 'Phase 1 — Каталог и tenant-изоляция', items: ['Multi-tenant архитектура','Товары, группы, штрихкоды, остатки','Импорт из других систем'] },
] ]
--- ---
<BaseLayout title="Что нового" description="История обновлений ALL MARKET: что нового в продукте."> <BaseLayout title="Changelog" description="История релизов ALL MARKET: что нового в продукте.">
<section class="max-w-3xl mx-auto px-4 sm:px-6 py-14"> <section class="max-w-3xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold">Что нового</h1> <h1 class="text-4xl font-extrabold">Changelog</h1>
<p class="text-slate-500 mt-2">История обновлений ALL MARKET.</p> <p class="text-slate-500 mt-2">История релизов ALL MARKET.</p>
<div class="mt-10 space-y-8"> <div class="mt-10 space-y-8">
{releases.map((r) => ( {releases.map((r) => (
<article class="rounded-xl border border-slate-200 bg-white p-5"> <article class="rounded-xl border border-slate-200 bg-white p-5">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '@/layouts/BaseLayout.astro' import BaseLayout from '@/layouts/BaseLayout.astro'
const sections = [ const sections = [
{ id: 'catalog', icon: '📦', title: 'Товары и каталог', items: ['Группы и подгруппы','Штрихкоды EAN-13, EAN-8, Code 128','Цены: розничная и оптовая','Автогенерация штрихкода для весовых товаров','Импорт каталога из файлов Excel/CSV','Фото товаров'] }, { id: 'catalog', icon: '📦', title: 'Товары и каталог', items: ['Группы и подгруппы','Штрихкоды EAN-13, EAN-8, Code 128','Цены: розничная и оптовая','Автогенерация штрихкода для весовых товаров','Импорт из CSV, 1С и МойСклад','Фото товаров'] },
{ id: 'sales', icon: '💳', title: 'Продажи и касса', items: ['Касса для Windows и Linux','Работа без интернета','Поддержка торговых весов','USB-сканер штрихкодов','Печать чека (58/80 мм)','Скидки и акции','Возвраты на кассе'] }, { id: 'sales', icon: '💳', title: 'Продажи и касса', items: ['Касса для Windows и Linux','Работа без интернета','Поддержка торговых весов','USB-сканер штрихкодов','Печать чека (58/80 мм)','Скидки и акции','Возвраты на кассе'] },
{ id: 'stock', icon: '🏬', title: 'Склад и остатки', items: ['Несколько складов','Приёмка со сканером','Себестоимость считается автоматически','Инвентаризация','Списание (брак/просрочка)','Оприходование','Перемещения между складами','История движений'] }, { id: 'stock', icon: '🏬', title: 'Склад и остатки', items: ['Несколько складов','Приёмка со сканером','Себестоимость считается автоматически','Инвентаризация','Списание (брак/просрочка)','Оприходование','Перемещения между складами','История движений'] },
{ id: 'purchase', icon: '🚚', title: 'Закупки и поставщики', items: ['База поставщиков','Документы приёмки','Себестоимость по приёмкам','Возвраты поставщику'] }, { id: 'purchase', icon: '🚚', title: 'Закупки и поставщики', items: ['База поставщиков','Документы приёмки','Себестоимость по приёмкам','Возвраты поставщику'] },

View file

@ -2,38 +2,38 @@
import BaseLayout from '@/layouts/BaseLayout.astro' import BaseLayout from '@/layouts/BaseLayout.astro'
const formats = [ const formats = [
{ icon: '📊', title: 'Файл Excel/CSV', text: 'Выгрузите каталог из старой системы в CSV (Excel умеет «Сохранить как CSV»). ALL MARKET разложит данные по колонкам. Нужны колонки: название, штрихкод, цена — остальное по желанию.' }, { icon: '📊', title: 'Файл CSV', text: 'Выгрузите каталог из старой системы в CSV (Excel умеет «Сохранить как CSV»). ALL MARKET разложит данные по колонкам. Нужны колонки: название, штрихкод, цена — остальное по желанию.' },
{ icon: '✍️', title: 'Вручную', text: 'Если товаров немного — заводите карточки прямо в админке: «Товары» → «Добавить». Удобно для 10–50 позиций.' }, { icon: '🔌', title: 'Из МойСклад', text: 'Введите токен МойСклад — ALL MARKET подтянет товары, контрагентов, остатки и цены.' },
{ icon: '🧩', title: 'Из других систем учёта', text: 'Импорт напрямую из других систем учёта — в разработке. Появится в личном кабинете магазина.' }, { icon: '📦', title: 'Из 1С', text: 'Сохраните типовую выгрузку 1С (Бухгалтерия, УТ, Розница) в CSV и загрузите файлом. Каталог, цены, штрихкоды, группы.' },
] ]
const matrix: [string, string][] = [ const matrix: [string, string, string][] = [
['Товары (название, штрихкоды, цены)', '✅'], ['Товары (название, штрихкоды, цены)', '✅', '✅'],
['Группы товаров', '✅'], ['Группы товаров', '✅', '✅'],
['Единицы измерения', '✅'], ['Единицы измерения', '✅', '✅'],
['Контрагенты (поставщики, клиенты)', '⚠️ Отдельным файлом'], ['Контрагенты (поставщики, клиенты)', '⚠️ Отдельным файлом', '✅'],
['Текущие остатки по складам', '⚠️ Отдельным файлом'], ['Текущие остатки по складам', '⚠️ Отдельным файлом', '✅'],
['Цены по типам (розница, опт)', '⚠️ Отдельным файлом'], ['Цены по типам (розница, опт)', '⚠️ Отдельным файлом', '✅'],
] ]
const steps = [ const steps = [
{ title: 'Зарегистрируйтесь в ALL MARKET', text: 'Заполните короткую форму: email, название магазина, пароль. 90 дней бесплатно, без карты.' }, { title: 'Зарегистрируйтесь в ALL MARKET', text: 'Заполните короткую форму: email, название магазина, пароль. 90 дней бесплатно, без карты.' },
{ title: 'Подготовьте файл', text: 'Выгрузите каталог из старой системы в CSV (или Excel «Сохранить как CSV»). Нужны колонки: название, штрихкод, цена.' }, { title: 'Подготовьте данные', text: 'CSV: выгрузите каталог из старой системы в CSV. Из МойСклад: получите токен в настройках МойСклад.' },
{ title: 'Запустите импорт', text: 'В админке: «Импорт» → «Из файла» → загрузите файл → сопоставьте колонки → нажмите «Запустить».' }, { title: 'Запустите импорт', text: 'В админке: «Импорт» → выберите формат → загрузите файл или введите токен → нажмите «Запустить».' },
{ title: 'Проверьте результат', text: 'Откройте «Товары» — каталог должен быть полным. Проверьте 5–10 случайных товаров: цены, штрихкоды, остатки.' }, { title: 'Проверьте результат', text: 'Откройте «Товары» — каталог должен быть полным. Проверьте 5–10 случайных товаров: цены, штрихкоды, остатки.' },
{ title: 'Начните вести учёт', text: 'Каталог, остатки и отчёты — сразу в браузере. Касса для Windows и Linux появится для скачивания скоро.' }, { title: 'Начните вести учёт', text: 'Каталог, остатки и отчёты — сразу в браузере. Касса для Windows и Linux появится для скачивания скоро.' },
] ]
const faq = [ const faq = [
{ q: 'Сохранятся ли данные в исходной системе?', a: 'Да. Импорт работает с копией вашего файла — он ничего не меняет в исходной системе. Ваш текущий учёт остаётся нетронутым.' }, { q: 'Сохранятся ли данные в исходной системе?', a: 'Да. Импорт из МойСклад только читает данные — ничего не удаляет и не меняет. Импорт из файла работает с копией. Ваш текущий учёт остаётся нетронутым.' },
{ q: 'Можно ли работать в двух системах параллельно?', a: 'Технически — да, в бесплатный период. Но синхронизации в реальном времени нет — это создаст путаницу с остатками. Лучше выбрать одну систему как основную.' }, { q: 'Можно ли работать в обеих системах параллельно?', a: 'Технически — да, в бесплатный период. Но синхронизации в реальном времени нет — это создаст путаницу с остатками. Лучше выбрать одну систему как основную.' },
{ q: 'Что если у меня кастомные поля или специфичные настройки?', a: 'Напишите нам перед импортом — посмотрим. В большинстве случаев всё переносится. Если что-то нестандартное — настроим вручную.' }, { q: 'Что если у меня кастомные поля или специфичные настройки?', a: 'Напишите нам перед импортом — посмотрим. В большинстве случаев всё переносится. Если что-то нестандартное — настроим вручную.' },
{ q: 'Сколько времени занимает импорт?', a: 'Каталог 1 000 товаров — около 1 минуты. 10 000 товаров — 5–10 минут. 50 000+ — до часа. Прогресс отображается в реальном времени.' }, { q: 'Сколько времени занимает импорт?', a: 'Каталог 1 000 товаров — около 1 минуты. 10 000 товаров — 5–10 минут. 50 000+ — до часа. Прогресс отображается в реальном времени.' },
{ q: 'Сколько стоит импорт?', a: 'Бесплатно. Никаких сборов за импорт, настройку или консультацию.' }, { q: 'Сколько стоит импорт?', a: 'Бесплатно. Никаких сборов за импорт, настройку или консультацию.' },
{ q: 'Что если возникли проблемы?', a: 'Напишите на support@food-market.kz — поможем разобраться и при необходимости настроим импорт вручную.' }, { q: 'Что если возникли проблемы?', a: 'Напишите на support@food-market.kz — поможем разобраться и при необходимости настроим импорт вручную.' },
] ]
--- ---
<BaseLayout title="Импорт каталога" description="Перенесите товары, остатки и штрихкоды из файла Excel/CSV за минуты — без ручного ввода." ogImage="/og/import.png"> <BaseLayout title="Импорт каталога" description="Перенесите товары, контрагентов, остатки и штрихкоды из вашей системы за минуты. CSV, 1С, МойСклад." ogImage="/og/import.png">
<section class="relative overflow-hidden"> <section class="relative overflow-hidden">
<img src="/screenshots/catalog.webp" srcset="/screenshots/catalog.webp 1440w, /screenshots/catalog@2x.webp 2880w" sizes="100vw" width="1440" height="900" alt="Каталог товаров в ALL MARKET" class="absolute inset-0 w-full h-full object-cover" /> <img src="/screenshots/catalog.webp" srcset="/screenshots/catalog.webp 1440w, /screenshots/catalog@2x.webp 2880w" sizes="100vw" width="1440" height="900" alt="Каталог товаров в ALL MARKET" class="absolute inset-0 w-full h-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-r from-slate-900/85 via-slate-900/60 to-slate-900/30"></div> <div class="absolute inset-0 bg-gradient-to-r from-slate-900/85 via-slate-900/60 to-slate-900/30"></div>
@ -73,14 +73,16 @@ const faq = [
<thead class="bg-slate-50 border-b border-slate-200"> <thead class="bg-slate-50 border-b border-slate-200">
<tr> <tr>
<th class="px-5 py-3 text-left font-semibold">Сущность</th> <th class="px-5 py-3 text-left font-semibold">Сущность</th>
<th class="px-5 py-3 text-center font-semibold">Импорт из файла Excel/CSV</th> <th class="px-5 py-3 text-center font-semibold">CSV (файл)</th>
<th class="px-5 py-3 text-center font-semibold">МойСклад</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{matrix.map(([entity, csv]) => ( {matrix.map(([entity, csv, api]) => (
<tr class="border-b border-slate-100 last:border-b-0"> <tr class="border-b border-slate-100 last:border-b-0">
<td class="px-5 py-3.5">{entity}</td> <td class="px-5 py-3.5">{entity}</td>
<td class="px-5 py-3.5 text-center">{csv}</td> <td class="px-5 py-3.5 text-center">{csv}</td>
<td class="px-5 py-3.5 text-center">{api}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View file

@ -3,7 +3,7 @@ import BaseLayout from '@/layouts/BaseLayout.astro'
import FAQ from '@/components/FAQ.tsx' import FAQ from '@/components/FAQ.tsx'
const modules = [ const modules = [
{ icon: '📦', title: 'Товары и каталог', text: 'Группы, штрихкоды, цены, остатки. Перенос каталога из файла (Excel/CSV).' }, { icon: '📦', title: 'Товары и каталог', text: 'Группы, штрихкоды, цены, остатки. Перенос каталога файлом или из МойСклад.' },
{ icon: '💳', title: 'Продажи и касса', text: 'Касса с весами, сканером и печатью чека. Работает на Windows и Linux, в том числе без интернета.' }, { icon: '💳', title: 'Продажи и касса', text: 'Касса с весами, сканером и печатью чека. Работает на Windows и Linux, в том числе без интернета.' },
{ icon: '🏬', title: 'Склад и приёмки', text: 'Несколько складов, приёмка со сканером, инвентаризация, списание, оприходование, перемещение.' }, { icon: '🏬', title: 'Склад и приёмки', text: 'Несколько складов, приёмка со сканером, инвентаризация, списание, оприходование, перемещение.' },
{ icon: '🚚', title: 'Закупки и себестоимость', text: 'Приёмки от поставщиков. Себестоимость считается автоматически по приёмкам.' }, { icon: '🚚', title: 'Закупки и себестоимость', text: 'Приёмки от поставщиков. Себестоимость считается автоматически по приёмкам.' },
@ -60,7 +60,7 @@ const modules = [
<div class="rounded-xl border border-slate-200 p-5"> <div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">⬇️</div> <div class="text-3xl">⬇️</div>
<h3 class="font-semibold mt-2">Перенос каталога без ручного ввода</h3> <h3 class="font-semibold mt-2">Перенос каталога без ручного ввода</h3>
<p class="text-sm text-slate-600 mt-1">Загрузите товары, остатки и штрихкоды файлом Excel/CSV. Не нужно заводить тысячи позиций руками.</p> <p class="text-sm text-slate-600 mt-1">Загрузите товары, остатки и штрихкоды файлом CSV (в том числе выгрузкой из 1С) или подтяните из МойСклад. Не нужно заводить тысячи позиций руками.</p>
</div> </div>
<div class="rounded-xl border border-slate-200 p-5"> <div class="rounded-xl border border-slate-200 p-5">
<div class="text-3xl">🎁</div> <div class="text-3xl">🎁</div>
@ -178,7 +178,7 @@ const modules = [
<div class="max-w-3xl mx-auto px-4 sm:px-6 text-center"> <div class="max-w-3xl mx-auto px-4 sm:px-6 text-center">
<h2 class="text-3xl font-bold mb-3">Перенос и интеграции</h2> <h2 class="text-3xl font-bold mb-3">Перенос и интеграции</h2>
<p class="text-slate-600"> <p class="text-slate-600">
Переносим каталог из файлов Excel/CSV — уже сейчас. Переносим каталог из МойСклад и из CSV (в том числе выгрузки из 1С) — уже сейчас.
Онлайн-касса (ОФД), приём оплат картой и маркетплейсы — в разработке. Онлайн-касса (ОФД), приём оплат картой и маркетплейсы — в разработке.
</p> </p>
<a href="/import" class="mt-5 inline-block text-sm font-semibold text-brand hover:text-brand-hover">Как перенести каталог →</a> <a href="/import" class="mt-5 inline-block text-sm font-semibold text-brand hover:text-brand-hover">Как перенести каталог →</a>
@ -199,7 +199,7 @@ const modules = [
<li>2 сотрудника</li> <li>2 сотрудника</li>
<li>Без ограничения по числу товаров</li> <li>Без ограничения по числу товаров</li>
<li>Касса с весами</li> <li>Касса с весами</li>
<li>Перенос каталога из файлов Excel/CSV</li> <li>Перенос каталога из CSV и МойСклад</li>
</ul> </ul>
<a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a> <a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a>
</div> </div>
@ -214,7 +214,7 @@ const modules = [
<li>До 10 сотрудников</li> <li>До 10 сотрудников</li>
<li>Все отчёты по прибыли и остаткам</li> <li>Все отчёты по прибыли и остаткам</li>
</ul> </ul>
<a href="/pricing#configurator" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Настроить тариф</a> <a href="/pricing" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Настроить тариф</a>
</div> </div>
<div class="rounded-xl border border-slate-200 p-6 bg-white"> <div class="rounded-xl border border-slate-200 p-6 bg-white">
<h3 class="font-bold text-lg">Сеть</h3> <h3 class="font-bold text-lg">Сеть</h3>

View file

@ -1,16 +1,15 @@
--- ---
import BaseLayout from '@/layouts/BaseLayout.astro' import BaseLayout from '@/layouts/BaseLayout.astro'
const ready = [ const ready = [
{ title: 'Перенос каталога', items: ['Импорт каталога из файла Excel/CSV', 'Переносятся названия, штрихкоды, цены, остатки, группы'] }, { title: 'Перенос каталога', items: ['Импорт из МойСклад по токену', 'Импорт из файла CSV', 'Выгрузка 1С (сохранённая в CSV)'] },
{ title: 'Оборудование', items: ['Торговые весы CAS, Масса-К и совместимые (USB/COM)', 'USB-сканеры штрихкодов', 'Чековые принтеры 58/80 мм (ESC/POS)', 'Денежные ящики', 'Дисплей покупателя (RS-232)'] }, { title: 'Оборудование', items: ['Торговые весы CAS, Масса-К и совместимые (USB/COM)', 'USB-сканеры штрихкодов', 'Чековые принтеры 58/80 мм', 'Денежные ящики', 'Дисплей покупателя (по RS-232)'] },
] ]
const planned = [ const planned = [
{ title: 'Онлайн-касса и оплаты', items: ['Фискализация через ОФД РК', 'Приём оплат картой (Kaspi Pay и банки)'] }, { title: 'Онлайн-касса и оплаты', items: ['Фискализация через ОФД РК', 'Приём оплат картой (Kaspi Pay и банки)'] },
{ title: 'Маркетплейсы', items: ['Kaspi Magazin', 'Ozon', 'Wildberries'] }, { title: 'Маркетплейсы', items: ['Kaspi Magazin', 'Ozon', 'Wildberries'] },
{ title: 'Импорт из других систем учёта', items: ['Перенос из других систем учёта — появится в личном кабинете магазина'] },
] ]
--- ---
<BaseLayout title="Перенос и интеграции" description="Что уже работает: импорт каталога из файлов Excel/CSV и торговое оборудование. Онлайн-касса (ОФД), приём оплат картой и маркетплейсы — в разработке."> <BaseLayout title="Перенос и интеграции" description="Что уже подключается к ALL MARKET: МойСклад, CSV, 1С, торговое оборудование. Онлайн-касса (ОФД), оплаты картой и маркетплейсы — в разработке.">
<section class="max-w-5xl mx-auto px-4 sm:px-6 py-12"> <section class="max-w-5xl mx-auto px-4 sm:px-6 py-12">
<h1 class="text-4xl font-extrabold text-center">Перенос и интеграции</h1> <h1 class="text-4xl font-extrabold text-center">Перенос и интеграции</h1>
<p class="text-center text-slate-500 mt-3">Честно о том, что уже работает и что готовим.</p> <p class="text-center text-slate-500 mt-3">Честно о том, что уже работает и что готовим.</p>

View file

@ -22,12 +22,13 @@ import BusinessTariffBuilder from '@/components/BusinessTariffBuilder.tsx'
<li>✓ 2 сотрудника</li> <li>✓ 2 сотрудника</li>
<li>✓ Без лимита товаров</li> <li>✓ Без лимита товаров</li>
<li>✓ Касса с поддержкой торговых весов</li> <li>✓ Касса с поддержкой торговых весов</li>
<li>✓ Перенос каталога из файлов Excel/CSV</li> <li>✓ Импорт из других систем</li>
<li>✓ Перенос каталога из CSV и МойСклад</li>
</ul> </ul>
<a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a> <a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a>
</div> </div>
<div id="configurator" class="lg:row-span-2 scroll-mt-20"> <div class="lg:row-span-2">
<BusinessTariffBuilder client:load /> <BusinessTariffBuilder client:load />
</div> </div>

View file

@ -1,5 +1,4 @@
import type { APIRoute } from 'astro' import type { APIRoute } from 'astro'
import { getCollection } from 'astro:content'
const PAGES = [ const PAGES = [
'', 'features', 'pricing', 'pos', 'import', 'integrations', '', 'features', 'pricing', 'pos', 'import', 'integrations',
@ -8,19 +7,9 @@ const PAGES = [
'legal/offer', 'legal/privacy', 'legal/consent', 'legal/requisites', 'legal/offer', 'legal/privacy', 'legal/consent', 'legal/requisites',
] ]
export const GET: APIRoute = async ({ site }) => { export const GET: APIRoute = ({ site }) => {
const base = (site?.toString() ?? 'https://test.food-market.kz').replace(/\/$/, '') const base = (site?.toString() ?? 'https://test.food-market.kz').replace(/\/$/, '')
// Канонические URL — со слэшем на конце (Astro отдаёт .../features/, а запрос const urls = PAGES.map((p) => `<url><loc>${base}/${p}</loc></url>`).join('\n ')
// без слэша 301-редиректится). Статьи KB и блога добавляем из коллекций.
const kb = await getCollection('kb')
const blog = await getCollection('blog')
const paths = [
...PAGES,
...kb.map((e) => `kb/${e.slug}`),
...blog.map((e) => `blog/${e.slug}`),
]
const loc = (p: string) => (p === '' ? `${base}/` : `${base}/${p}/`)
const urls = paths.map((p) => `<url><loc>${loc(p)}</loc></url>`).join('\n ')
const xml = `<?xml version="1.0" encoding="UTF-8"?> const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls} ${urls}

View file

@ -1,18 +1,15 @@
--- ---
import BaseLayout from '@/layouts/BaseLayout.astro' import BaseLayout from '@/layouts/BaseLayout.astro'
--- ---
<BaseLayout title="Статус" description="Страница статуса систем ALL MARKET."> <BaseLayout title="Status" description="Статус систем ALL MARKET: API, админка, касса.">
<section class="max-w-2xl mx-auto px-4 sm:px-6 py-14"> <section class="max-w-2xl mx-auto px-4 sm:px-6 py-14">
<h1 class="text-4xl font-extrabold text-center">Статус систем</h1> <h1 class="text-4xl font-extrabold text-center">Статус систем</h1>
<div class="mt-8 rounded-xl border border-slate-200 bg-white p-6 text-center"> <ul class="mt-10 divide-y divide-slate-200 border border-slate-200 rounded-xl bg-white">
<p class="text-slate-700">Публичная страница статуса с автоматическим мониторингом и историей инцидентов — в разработке. Пока реального статуса в реальном времени здесь нет, поэтому мы не показываем его, чтобы не вводить в заблуждение.</p> <li class="flex items-center justify-between px-5 py-4"><span>API</span><span class="text-emerald-600 text-sm">● Работает</span></li>
<p class="text-slate-600 mt-4">Если вы столкнулись с перебоем в работе — напишите нам, ответим оперативно:</p> <li class="flex items-center justify-between px-5 py-4"><span>Админка</span><span class="text-emerald-600 text-sm">● Работает</span></li>
<p class="mt-3 text-sm"> <li class="flex items-center justify-between px-5 py-4"><span>База данных</span><span class="text-emerald-600 text-sm">● Работает</span></li>
<a href="mailto:support@food-market.kz" class="text-brand underline">support@food-market.kz</a> <li class="flex items-center justify-between px-5 py-4"><span>Касса (Windows)</span><span class="text-slate-400 text-sm">○ Скоро</span></li>
<span class="text-slate-400"> · </span> </ul>
Telegram <span class="font-medium">@FoodMarketKZ_Support</span> <p class="text-xs text-slate-400 mt-6 text-center">Раздел в разработке. Скоро добавим автоматический мониторинг и историю инцидентов.</p>
</p>
</div>
<p class="text-xs text-slate-400 mt-6 text-center">Приложение кассы для Windows и Linux — скоро.</p>
</section> </section>
</BaseLayout> </BaseLayout>

View file

@ -10,36 +10,3 @@
html { scroll-behavior: smooth; } html { scroll-behavior: smooth; }
body { background: #fff; color: #0f172a; } body { background: #fff; color: #0f172a; }
/* Эмодзи-фолбэк: чтобы иконки-эмодзи не рисовались квадратами там, где нет цветного
emoji-шрифта (отдельные окружения/симуляторы). */
:root { --emoji: 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji','Twemoji Mozilla',sans-serif; }
body { font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji', sans-serif; }
/* Типографика markdown-контента (База знаний и блог) — ОДИН источник на весь сайт
(раньше стили были только на странице блога → /kb/* рисовались плоским текстом). */
.prose-md { font-size: 16px; line-height: 1.75; color: #1e293b; }
.prose-md h2 { font-size: 1.5rem; font-weight: 700; margin-top: 2rem; margin-bottom: 0.75rem; }
.prose-md h3 { font-size: 1.125rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.prose-md p { margin: 1rem 0; }
.prose-md ul { list-style: disc; margin: 1rem 0 1rem 1.5rem; }
.prose-md ol { list-style: decimal; margin: 1rem 0 1rem 1.5rem; }
.prose-md li { margin: 0.35rem 0; }
.prose-md a { color: #00B207; text-decoration: underline; }
.prose-md blockquote { border-left: 3px solid #00B207; background: #E8F5E9; padding: 0.75rem 1rem; margin: 1.25rem 0; font-style: italic; color: #1e293b; }
.prose-md code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
.prose-md hr { margin: 2rem 0; border: 0; border-top: 1px solid #e2e8f0; }
.prose-md strong { font-weight: 600; }
.prose-md table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.95rem; }
.prose-md th, .prose-md td { border: 1px solid #e2e8f0; padding: 0.5rem 0.75rem; text-align: left; }
.prose-md th { background: #f8fafc; font-weight: 600; }
/* Увеличенная зона нажатия для инлайн-ссылок (мобильный) без сдвига вёрстки. */
.prose-md a { padding-block: 3px; margin-block: -3px; }
/* Ползунки конструктора «Бизнес»: крупная ручка (удобно пальцем) + вертикальный
скролл страницы не двигает значение (touch-action задан в компоненте). */
.fm-range { height: 2.25rem; background: transparent; cursor: pointer; }
.fm-range::-webkit-slider-runnable-track { height: 6px; border-radius: 9999px; background: #e2e8f0; }
.fm-range::-moz-range-track { height: 6px; border-radius: 9999px; background: #e2e8f0; }
.fm-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -11px; border-radius: 9999px; background: #00B207; border: 3px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
.fm-range::-moz-range-thumb { width: 28px; height: 28px; border-radius: 9999px; background: #00B207; border: 3px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); }

View file

@ -1,169 +0,0 @@
// Проверка второго круга исправлений публичного сайта test.food-market.kz
// (как человек: Playwright, десктоп 1440 + моб. 390). Вывод — docs/site-fixes-2026-10-11/.
import { chromium } from '@playwright/test'
import { mkdirSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
const BASE = process.env.SITE_URL ?? 'https://test.food-market.kz'
const OUT = resolve('/home/nns/food-market', 'docs/site-fixes-2026-10-11')
mkdirSync(OUT, { recursive: true })
const results = []
const shots = []
const ok = (name, cond, detail = '') => { results.push({ name, ok: !!cond, detail }); console.log(`${cond ? '✓' : '✗'} ${name} ${detail}`) }
const norm = (s) => (s || '').replace(/[\s    ]/g, '')
const browser = await chromium.launch({ args: ['--ignore-certificate-errors'] })
// Сбор ошибок консоли/страницы по URL.
async function consoleErrorsFor(ctx, path) {
const errs = []
const p = await ctx.newPage()
p.on('console', (m) => { if (m.type() === 'error') errs.push(m.text()) })
p.on('pageerror', (e) => errs.push(String(e)))
await p.goto(BASE + path, { waitUntil: 'networkidle' }).catch(() => {})
await p.waitForTimeout(1200)
await p.close()
return errs
}
// ---------- DESKTOP 1440x900 @2x ----------
const dctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, ignoreHTTPSErrors: true })
const d = await dctx.newPage()
async function shotD(path, file, caption) {
await d.goto(BASE + path, { waitUntil: 'networkidle' }).catch(() => {})
await d.waitForTimeout(700)
await d.screenshot({ path: resolve(OUT, file) })
shots.push({ file, caption, kind: 'Десктоп 1440' })
}
// #2 — консоль чистая (React #418 ушёл)
for (const [path, label] of [['/', 'главная'], ['/pricing', '/pricing'], ['/signup?plan=business&shops=3&cashes=4&warehouses=1&employees=5', '/signup?plan=business+params'], ['/signup?plan=business', '/signup?plan=business (без параметров)']]) {
const errs = await consoleErrorsFor(dctx, path)
const react418 = errs.filter((e) => /#418|hydrat|Minified React error/i.test(e))
ok(`консоль чистая: ${label}`, errs.length === 0, errs.length ? errs.slice(0, 2).join(' | ') : '')
ok(`нет React #418 (hydration): ${label}`, react418.length === 0, react418.join(' | '))
}
// #3 — plan=business без параметров → «Бизнес» + дефолтная конфигурация 1/1/1/5 = 10 000
await d.goto(`${BASE}/signup?plan=business`, { waitUntil: 'networkidle' }).catch(() => {})
await d.waitForTimeout(1300)
ok('plan=business без параметров: выбран «Бизнес»', await d.locator('label:has(input[name="plan"]:checked):has-text("Бизнес")').count() > 0)
const cfg = (await d.locator('div:has-text("Ваша конфигурация"):has-text("Магазинов")').last().innerText().catch(() => '')) || ''
ok('plan=business по умолчанию: цена 10 000', norm(cfg).includes('10000'), norm(cfg).slice(0, 40))
ok('plan=business по умолчанию: Магазинов 1 / Складов 1 / Сотрудников 5', /Магазинов:\s*1/.test(cfg) && /Складов:\s*1/.test(cfg) && /Сотрудников:\s*5/.test(cfg))
await d.screenshot({ path: resolve(OUT, 'd-signup-business-default.png'), fullPage: true }); shots.push({ file: 'd-signup-business-default.png', caption: '/signup?plan=business без параметров — «Бизнес» с дефолтом 1/1/1/5 = 10 000 ₸', kind: 'Десктоп 1440' })
// plan=network → «Сеть»; plan=xyz → «Старт»; plan=start tile «5 000 ₸/мес»
await d.goto(`${BASE}/signup?plan=network`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(1200)
ok('plan=network: выбран «Сеть»', await d.locator('label:has(input[name="plan"]:checked):has-text("Сеть")').count() > 0)
await d.goto(`${BASE}/signup?plan=xyz`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(1200)
ok('plan=xyz: откат на «Старт»', await d.locator('label:has(input[name="plan"]:checked):has-text("Старт")').count() > 0)
const startTile = norm(await d.locator('label:has(input[name="plan"]):has-text("Старт")').first().innerText().catch(() => ''))
ok('плитка «Старт»: цена «5 000 ₸/мес»', startTile.includes('5000₸/мес'), startTile)
// #4 — типографика KB (prose-md) + billing честный
await d.goto(`${BASE}/kb/billing`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(500)
ok('KB: markdown отрисован (h2 в .prose-md)', await d.locator('.prose-md h2').count() > 0)
ok('KB: списки отрисованы (ul li)', await d.locator('.prose-md ul li').count() > 0)
const billTxt = await d.locator('.prose-md').innerText().catch(() => '')
ok('KB billing: нет «Настройки → Подписка → История»', !/Настройки.{0,3}Подписка.{0,3}История/.test(billTxt))
ok('KB billing: нет «тг», цены в «₸»', !/\bтг\b/.test(billTxt) && billTxt.includes('₸'))
await d.screenshot({ path: resolve(OUT, 'd-kb-billing.png'), fullPage: true }); shots.push({ file: 'd-kb-billing.png', caption: 'KB «Тарифы и оплата» — markdown-типографика, честный текст (без несуществующего биллинг-UI), ₸', kind: 'Десктоп 1440' })
await shotD('/kb/faq', 'd-kb-faq.png', 'KB FAQ — типографика, «бесплатный период», TLS без версии')
// blog: один h1
await d.goto(`${BASE}/blog/quick-start`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(500)
ok('блог quick-start: ровно один <h1>', await d.locator('article h1, h1').count() === 1, `h1=${await d.locator('h1').count()}`)
await d.screenshot({ path: resolve(OUT, 'd-blog-quickstart.png'), fullPage: true }); shots.push({ file: 'd-blog-quickstart.png', caption: 'Блог «Быстрый старт» — один заголовок, без чужих систем учёта', kind: 'Десктоп 1440' })
// бренды: системы учёта 0, Kaspi только как «в разработке»
for (const [path, label] of [['/', 'главная'], ['/integrations', '/integrations'], ['/import', '/import'], ['/kb/faq', '/kb/faq'], ['/blog/launch', '/blog/launch']]) {
await d.goto(BASE + path, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(600)
const body = await d.locator('body').innerText().catch(() => '')
ok(`нет чужих систем учёта (МойСклад/1С): ${label}`, !/мойсклад|мой склад|moysklad|1с\b|1c\b/i.test(body), '')
}
await d.goto(`${BASE}/integrations`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(500)
const integ = await d.locator('body').innerText().catch(() => '')
ok('/integrations: Kaspi присутствует (в блоке «в разработке»)', /kaspi/i.test(integ))
ok('/integrations: блок «В разработке» есть', /в разработке/i.test(integ))
await d.screenshot({ path: resolve(OUT, 'd-integrations.png'), fullPage: true }); shots.push({ file: 'd-integrations.png', caption: '/integrations — платёжки/маркетплейсы только в «В разработке»; «уже работает» без чужих систем учёта', kind: 'Десктоп 1440' })
// #5 pricing: Популярный + 90 дней + конструктор id; changelog без жаргона; status без фикции
await d.goto(`${BASE}/pricing`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(700)
const priceBody = await d.locator('body').innerText().catch(() => '')
ok('/pricing: бейдж «Популярный» на конструкторе', await d.locator('#configurator :text("Популярный")').count() > 0)
ok('/pricing: «90 дней бесплатно» в конструкторе', /90 дней бесплатно/.test(priceBody))
ok('/pricing: якорь #configurator есть', await d.locator('#configurator').count() > 0)
ok('/pricing: ползунки с кнопками −/+', await d.locator('#configurator button[aria-label^="Увеличить"]').count() >= 3)
await d.screenshot({ path: resolve(OUT, 'd-pricing.png'), fullPage: true }); shots.push({ file: 'd-pricing.png', caption: '/pricing — конструктор «Бизнес»: бейдж «Популярный», «90 дней бесплатно», кнопки −/+', kind: 'Десктоп 1440' })
await d.goto(`${BASE}/changelog`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(400)
const chg = await d.locator('body').innerText().catch(() => '')
ok('/changelog: без жаргона (Phase/SuperAdmin/retention/tenant)', !/phase\s*\d|superadmin|retention|tenant/i.test(chg))
await d.screenshot({ path: resolve(OUT, 'd-changelog.png'), fullPage: true }); shots.push({ file: 'd-changelog.png', caption: '/changelog («Что нового») — человеческие формулировки без внутреннего жаргона', kind: 'Десктоп 1440' })
await d.goto(`${BASE}/status`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(400)
const st = await d.locator('body').innerText().catch(() => '')
ok('/status: нет фиктивного «● Работает»', !/●\s*Работает/.test(st))
ok('/status: честная пометка «в разработке»', /в разработке/i.test(st))
await d.screenshot({ path: resolve(OUT, 'd-status.png'), fullPage: true }); shots.push({ file: 'd-status.png', caption: '/status — без фиктивных зелёных индикаторов, честно «в разработке» + контакты', kind: 'Десктоп 1440' })
// sitemap — слэши + kb/blog
const sm = await (await dctx.request.get(`${BASE}/sitemap.xml`, { ignoreHTTPSErrors: true }).catch(() => null))?.text?.().catch(() => '') || ''
ok('sitemap: канонические URL со слэшем', /\/features\/<\/loc>/.test(sm) && !/\/features<\/loc>/.test(sm))
ok('sitemap: статьи KB включены', /\/kb\/billing\//.test(sm) && /\/kb\/faq\//.test(sm))
ok('sitemap: статьи блога включены', /\/blog\/launch\//.test(sm) && /\/blog\/quick-start\//.test(sm))
// ---------- MOBILE 390x844 @3x ----------
const mctx = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 3, ignoreHTTPSErrors: true, isMobile: true, hasTouch: true })
const m = await mctx.newPage()
await m.goto(BASE + '/', { waitUntil: 'networkidle' }).catch(() => {}); await m.waitForTimeout(800)
ok('моб. главная: нет горизонтальной прокрутки', await m.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1))
// CTA «Начать бесплатно» в одну строку
const ctaH = await m.locator('header a[href="/signup"]').first().evaluate((el) => el.getBoundingClientRect().height).catch(() => 0)
ok('моб. шапка: CTA «Начать бесплатно» в одну строку', ctaH > 0 && ctaH < 56, `h=${Math.round(ctaH)}px`)
await m.screenshot({ path: resolve(OUT, 'm-home-closed.png') }); shots.push({ file: 'm-home-closed.png', caption: 'Моб. главная 390px — CTA не переносится, гамбургер в шапке', kind: 'Моб. 390' })
const burger = m.locator('#mobile-menu-btn')
await burger.click().catch(() => {}); await m.waitForTimeout(400)
ok('моб. меню открывается', await m.locator('#mobile-menu:not(.hidden)').count() > 0)
ok('моб. меню: есть «Интеграции»', await m.locator('#mobile-menu a[href="/integrations"]').count() > 0)
ok('моб. меню: есть «Статус»', await m.locator('#mobile-menu a[href="/status"]').count() > 0)
ok('моб. меню: есть «Что нового» (changelog)', await m.locator('#mobile-menu a[href="/changelog"]').count() > 0)
await m.screenshot({ path: resolve(OUT, 'm-menu-open.png') }); shots.push({ file: 'm-menu-open.png', caption: 'Моб. меню — полный состав + Статус/Что нового внизу', kind: 'Моб. 390' })
// #1 Android «Назад» закрывает меню (history.back при открытой панели)
const urlBefore = m.url()
await m.goBack().catch(() => {}); await m.waitForTimeout(500)
ok('моб. «Назад» закрывает меню (не уводит со страницы)', (await m.locator('#mobile-menu.hidden').count() > 0) && m.url().replace(/#.*$/, '') === urlBefore.replace(/#.*$/, ''), m.url())
await m.screenshot({ path: resolve(OUT, 'm-after-back.png') }); shots.push({ file: 'm-after-back.png', caption: 'Моб. после системной «Назад» — панель закрыта, остались на главной', kind: 'Моб. 390' })
// конструктор на моб. — кнопки −/+ и ручка
await m.goto(`${BASE}/pricing`, { waitUntil: 'networkidle' }).catch(() => {}); await m.waitForTimeout(700)
ok('моб. /pricing: кнопки −/+ у ползунков', await m.locator('#configurator button[aria-label^="Увеличить"]').count() >= 3)
await m.locator('#configurator button[aria-label^="Увеличить: Складов"]').first().click().catch(() => {}); await m.waitForTimeout(300)
await m.screenshot({ path: resolve(OUT, 'm-pricing-configurator.png'), fullPage: true }); shots.push({ file: 'm-pricing-configurator.png', caption: 'Моб. конструктор — крупные ручки + кнопки −/+ (значение меняется кнопкой, не случайным свайпом)', kind: 'Моб. 390' })
await m.goto(`${BASE}/signup?plan=business`, { waitUntil: 'networkidle' }).catch(() => {}); await m.waitForTimeout(1200)
await m.screenshot({ path: resolve(OUT, 'm-signup-business-default.png'), fullPage: true }); shots.push({ file: 'm-signup-business-default.png', caption: 'Моб. /signup?plan=business без параметров — «Бизнес» с дефолтом 10 000 ₸', kind: 'Моб. 390' })
await browser.close()
// index.html
const rows = shots.map((s) => `<figure><figcaption>${s.kind}: ${s.caption}</figcaption><img src="${s.file}" loading="lazy"></figure>`).join('\n')
const checks = results.map((r) => `<li class="${r.ok ? 'ok' : 'no'}">${r.ok ? '✓' : '✗'} ${r.name}${r.detail ? ` — <span class="dt">${r.detail}</span>` : ''}</li>`).join('\n')
const html = `<!doctype html><meta charset="utf-8"><title>Исправления сайта 2026-10-11 (круг 2)</title>
<style>body{font:15px system-ui;margin:0;padding:24px;background:#f8fafc;color:#0f172a;max-width:1100px;margin:auto}
h1{font-size:22px} figure{margin:0 0 24px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
figcaption{padding:8px 12px;font-size:13px;color:#475569;background:#f1f5f9} img{display:block;width:100%}
ul{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:14px 32px} li{margin:4px 0}
.ok{color:#16a34a} .no{color:#dc2626;font-weight:700} .dt{color:#64748b;font-size:12px}</style>
<h1>Исправления публичного сайта test.food-market.kz — круг 2 (${new Date().toISOString().slice(0, 10)})</h1>
<p>Проверка после деплоя (${new Date().toISOString()}). Пройдено: ${results.filter((r) => r.ok).length}/${results.length}.</p>
<h2>Автопроверки</h2><ul>${checks}</ul>
<h2>Скриншоты</h2>${rows}`
writeFileSync(resolve(OUT, 'index.html'), html)
writeFileSync(resolve(OUT, 'verify.json'), JSON.stringify({ results, shots }, null, 2))
console.log(`\n[site-verify-2] ${results.filter((r) => r.ok).length}/${results.length} passed → ${OUT}/index.html`)
const failed = results.filter((r) => !r.ok)
if (failed.length) console.log('FAILED:', failed.map((f) => f.name).join('; '))