Compare commits
No commits in common. "5c5f92de52895347c55326051f93741fca69fe01" and "304b72dc0468ea67b87c1aa965b5696e49d4e5db" have entirely different histories.
5c5f92de52
...
304b72dc04
|
|
@ -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**.
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 527 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 728 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 682 KiB |
|
Before Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 296 KiB |
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
|
@ -17,9 +17,9 @@ const logoUri = `data:image/png;base64,${logoPng.toString('base64')}`
|
|||
|
||||
const pages = [
|
||||
{ file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами и импортом каталога. Бесплатно 90 дней.' },
|
||||
{ file: 'pos.png', title: 'Касса для Windows и Linux с поддержкой торговых весов', sub: 'Офлайн-режим. Торговые весы и стандартное оборудование.' },
|
||||
{ file: 'pricing.png', title: 'Прозрачные тарифы ALL MARKET', sub: 'От 5 000 ₸/мес. Все возможности на любом тарифе. 90 дней бесплатно.' },
|
||||
{ file: 'import.png', title: 'Перенесите каталог за один клик', sub: 'Импорт каталога из файлов Excel и CSV. До 100 000 товаров. Бесплатно.' },
|
||||
{ file: 'pos.png', title: 'Касса для Windows с поддержкой торговых весов', sub: 'Установка за 5 минут. Офлайн-режим. Стандартное оборудование.' },
|
||||
{ file: 'pricing.png', title: 'Прозрачные тарифы ALL MARKET', sub: 'От 5 000 ₸/мес. Все возможности на любом тарифе. 90 дней триал.' },
|
||||
{ file: 'import.png', title: 'Перенесите каталог за один клик', sub: 'Excel, CSV, 1С, МойСклад. До 100 000 товаров. Бесплатно.' },
|
||||
{ file: 'about.png', title: 'О ALL MARKET', sub: 'Программа учёта розничной торговли, созданная в Казахстане.' },
|
||||
{ file: 'for-grocery.png', title: 'ALL MARKET для продуктового магазина', sub: 'Весовой товар, скоропорт, штрихкоды, учёт остатков.' },
|
||||
{ file: 'for-pharmacy.png', title: 'ALL MARKET для аптеки', sub: 'Серии, сроки годности, рецептурный отпуск, контроль ЛП.' },
|
||||
|
|
|
|||
|
|
@ -10,24 +10,16 @@ interface SliderProps {
|
|||
label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string
|
||||
}
|
||||
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 (
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
<span className="text-2xl font-bold tabular-nums">{value}{suffix && <span className="text-sm text-slate-500">{suffix}</span>}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range" min={min} max={max} value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="fm-range w-full mt-2 accent-[#00B207]"
|
||||
style={{ touchAction: 'pan-y' }}
|
||||
className="w-full mt-2 accent-[#00B207]"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-slate-400 mt-1"><span>{min}</span><span>{max}</span></div>
|
||||
</div>
|
||||
|
|
@ -46,11 +38,10 @@ export default function BusinessTariffBuilder() {
|
|||
const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}`
|
||||
|
||||
return (
|
||||
<div className="relative 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="rounded-xl border-2 border-brand bg-white p-6">
|
||||
<div className="flex items-baseline justify-between mb-6">
|
||||
<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>
|
||||
</div>
|
||||
<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]">
|
||||
Выбрать конфигурацию · {fmt.format(total)} ₸/мес
|
||||
</a>
|
||||
<p className="text-xs text-center text-slate-500 mt-2">90 дней бесплатно, без карты</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import Logo from './Logo.astro'
|
|||
<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="/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>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,33 +6,27 @@ const nav = [
|
|||
{ href: '/pos', label: 'Касса' },
|
||||
{ href: '/for-grocery', label: 'Для продуктовых' },
|
||||
{ href: '/import', label: 'Импорт' },
|
||||
{ href: '/integrations', label: 'Интеграции' },
|
||||
{ href: '/pricing', label: 'Тарифы' },
|
||||
{ href: '/kb', 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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<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="/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>
|
||||
<!-- Гамбургер: планшет/мобильный и средний десктоп (полное меню — от xl) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<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">Начать бесплатно</a>
|
||||
<!-- Гамбургер: только на планшете/мобильном (десктоп-меню скрыто < lg) -->
|
||||
<button
|
||||
id="mobile-menu-btn"
|
||||
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-controls="mobile-menu"
|
||||
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="/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 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>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
@ -64,32 +55,16 @@ const secondary = [
|
|||
const iconOpen = document.getElementById('mobile-menu-icon-open')
|
||||
const iconClose = document.getElementById('mobile-menu-icon-close')
|
||||
if (btn && menu) {
|
||||
// Пока панель открыта — в истории висит наша запись, чтобы системная кнопка
|
||||
// «Назад» (Android) закрывала меню, а не уводила со страницы.
|
||||
let pushed = false
|
||||
const setOpen = (open: boolean, fromPop = false) => {
|
||||
const setOpen = (open: boolean) => {
|
||||
menu.classList.toggle('hidden', !open)
|
||||
btn.setAttribute('aria-expanded', String(open))
|
||||
iconOpen?.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) => {
|
||||
e.stopPropagation()
|
||||
setOpen(menu.classList.contains('hidden'))
|
||||
})
|
||||
// системная кнопка «Назад» / popstate — закрыть панель
|
||||
window.addEventListener('popstate', () => {
|
||||
if (!menu.classList.contains('hidden')) setOpen(false, true)
|
||||
})
|
||||
// закрытие по тапу вне меню
|
||||
document.addEventListener('click', (e) => {
|
||||
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() }
|
||||
})
|
||||
// при переходе к десктопной ширине — сбросить состояние
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { validateEmail, validatePassword, validatePhone } from '@/lib/validation'
|
||||
import { PhoneInput } from '@/components/PhoneInput'
|
||||
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>>
|
||||
|
||||
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) {
|
||||
// Первый клиентский рендер ДОЛЖЕН совпасть с SSR-разметкой, иначе React кидает
|
||||
// #418 (hydration mismatch). Поэтому стартуем из props/дефолтов, а ?plan= и
|
||||
// конфигуратор читаем из URL уже в useEffect — после гидрации.
|
||||
const initialPlan: PlanId = (VALID_PLANS as readonly string[]).includes(defaultPlan) ? (defaultPlan as PlanId) : 'start'
|
||||
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 : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Разбор ?plan= и конфигурации из URL — один раз при инициализации (клиент).
|
||||
const [sel] = useState(() => readUrlSelection(defaultPlan))
|
||||
const [config] = useState<BusinessConfig>(sel.config)
|
||||
const hasConfig = sel.hasConfig
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [orgName, setOrgName] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
const [plan, setPlan] = useState<string>(sel.plan)
|
||||
const businessTotal = businessPrice(config)
|
||||
const [agree, setAgree] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
|
@ -111,7 +83,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
|
|||
// принимать ли — отдельно, см. отчёт site-fixes).
|
||||
body: JSON.stringify({
|
||||
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 }
|
||||
: {}),
|
||||
}),
|
||||
|
|
@ -223,18 +195,18 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
|
|||
<div className="text-sm font-medium mb-2">Тариф</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ id: 'start', name: 'Старт', price: `${fmtKzt(PRICING.startPrice)}/мес` },
|
||||
{ id: 'business', name: 'Бизнес', price: `${fmtKzt(businessTotal)}/мес` },
|
||||
{ id: 'start', name: 'Старт', price: fmtKzt(PRICING.startPrice) },
|
||||
{ id: 'business', name: 'Бизнес', price: hasConfig ? `${fmtKzt(businessTotal)}/мес` : 'от 10 000 ₸' },
|
||||
{ id: 'network', name: 'Сеть', price: 'по запросу' },
|
||||
].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'}`}>
|
||||
<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="text-xs text-slate-500 whitespace-nowrap">{p.price}</div>
|
||||
<div className="text-xs text-slate-500">{p.price}</div>
|
||||
</label>
|
||||
))}
|
||||
</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="flex items-baseline justify-between">
|
||||
<span className="font-medium">Ваша конфигурация «Бизнес»</span>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ description: "Сегодня мы запускаем ALL MARKET — програ
|
|||
cover_image: /photos/blog-launch.jpg
|
||||
---
|
||||
|
||||
# ALL MARKET запустился
|
||||
|
||||
Сегодня мы открываем публичный доступ к ALL MARKET — программе учёта розничной торговли, созданной командой казахстанских разработчиков и практиков ритейла.
|
||||
|
||||
## Что мы сделали
|
||||
|
|
@ -16,25 +18,25 @@ cover_image: /photos/blog-launch.jpg
|
|||
- **работать с первого клика** — без долгой настройки и платных консультаций
|
||||
- **стоить честно** — без скрытых доплат за модули, которые должны быть в базе
|
||||
- **поддерживать локальное железо** — торговые весы нативно, без переходников и сторонних драйверов
|
||||
- **позволять попробовать без риска** — честный бесплатный период, а не «14 дней с привязкой карты»
|
||||
- **позволять попробовать без риска** — настоящий триал, а не «14 дней с привязкой карты»
|
||||
|
||||
Мы воплотили эти принципы в ALL MARKET.
|
||||
|
||||
## Что вы получаете
|
||||
|
||||
- **5 000 ₸/мес** за тариф «Старт» — 1 магазин, 1 касса, 1 склад, без лимита товаров
|
||||
- **90 дней бесплатно** — без карты и без обязательств
|
||||
- **Касса для Windows и Linux** с нативной поддержкой торговых весов (приложение для скачивания — скоро)
|
||||
- **Импорт каталога** из файлов Excel/CSV за один клик
|
||||
- **Без доплат за модули** — учёт денег, отчёты и работа с клиентами входят в тариф
|
||||
- **5 000 тг/мес** за тариф «Старт» — 1 магазин, 1 касса, 1 склад, без лимита товаров
|
||||
- **90 дней триала** без карты, без обязательств, со всеми возможностями
|
||||
- **Касса для Windows** с нативной поддержкой торговых весов
|
||||
- **Импорт каталога** из Excel, CSV или API за один клик
|
||||
- **Все возможности на любом тарифе** — никаких доплат за CRM, лояльность, финансы или сценарии
|
||||
|
||||
## Что дальше
|
||||
|
||||
Сейчас мы открываем регистрацию и принимаем первых клиентов. Будем активно собирать обратную связь и улучшать продукт каждую неделю.
|
||||
|
||||
В ближайших планах:
|
||||
- Приём оплат картой и онлайн-касса (ОФД) — в разработке
|
||||
- Маркетплейсы — в разработке
|
||||
- Интеграция с Kaspi Magazin
|
||||
- Marketplace интеграции (Wildberries, Ozon — Q4 2026)
|
||||
- Мобильное приложение для руководителя
|
||||
|
||||
[Зарегистрироваться →](/signup)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ date: 2026-04-30
|
|||
author: Команда ALL MARKET
|
||||
category: guide
|
||||
description: "Пошаговая инструкция первого запуска: от регистрации до первой продажи."
|
||||
cover_image: /screenshots/catalog.webp
|
||||
cover_image: /photos/blog-quickstart.jpg
|
||||
---
|
||||
|
||||
# Запустите магазин в ALL MARKET за 15 минут
|
||||
|
||||
Часто новые SaaS-сервисы требуют дней на настройку и обучение. Мы спроектировали ALL MARKET так, чтобы вы могли начать продавать в день регистрации.
|
||||
|
||||
Реальное время от пустого экрана до первого пробитого чека — **15 минут**.
|
||||
|
|
@ -30,7 +32,9 @@ cover_image: /screenshots/catalog.webp
|
|||
- Сопоставьте колонки (название, штрихкод, цена) — ALL MARKET подскажет правильное соответствие
|
||||
- Запустите импорт — 5 000 SKU перенесутся за минуту
|
||||
|
||||
**Б. Импорт из других систем учёта** — в разработке: появится в личном кабинете магазина. Пока используйте импорт из файла Excel/CSV.
|
||||
**Б. Импорт через API** — если работаете в системе с открытым API:
|
||||
- «Импорт» → выберите систему → введите API-токен
|
||||
- Запустите автоматическую синхронизацию
|
||||
|
||||
**В. Вручную** — если у вас 10-50 товаров:
|
||||
- «Товары» → «Добавить»
|
||||
|
|
@ -38,10 +42,10 @@ cover_image: /screenshots/catalog.webp
|
|||
|
||||
## Шаг 3. Установите кассу (5 минут)
|
||||
|
||||
Приложение кассы для Windows и Linux — скоро (появится в разделе «Касса»).
|
||||
Приложение кассы для Windows — скоро (появится в разделе «Касса»).
|
||||
|
||||
1. Когда касса будет доступна: укажите адрес сервера и войдите логином
|
||||
2. Задайте PIN кассира — касса привяжется к точке продаж
|
||||
1. Запустите .msi файл — нажмите «Далее» три раза
|
||||
2. Введите ключ организации (показан в админке)
|
||||
3. Подключите торговые весы через USB-COM
|
||||
4. Подключите сканер штрихкодов через USB
|
||||
5. Подключите чековый принтер
|
||||
|
|
@ -63,8 +67,8 @@ cover_image: /screenshots/catalog.webp
|
|||
|
||||
После первой продажи:
|
||||
- Добавьте остальных кассиров — «Сотрудники» → «Добавить»
|
||||
- Запустите акции и скидки — раздел «Акции и скидки»
|
||||
- Программа лояльности — в расширенном режиме (раздел «Лояльность»)
|
||||
- Настройте программу лояльности — «Лояльность» → «Создать программу»
|
||||
- Запустите акции и скидки — «Маркетинг» → «Акции»
|
||||
|
||||
Полная документация в нашей [Базе знаний](/kb).
|
||||
|
||||
|
|
|
|||
|
|
@ -4,41 +4,60 @@ category: Биллинг
|
|||
order: 4
|
||||
---
|
||||
|
||||
Информация о тарифах ALL MARKET и порядке оплаты.
|
||||
Полная информация о подписках ALL MARKET и порядке оплаты.
|
||||
|
||||
## Тарифы
|
||||
|
||||
См. полную таблицу на [странице тарифов](/pricing).
|
||||
|
||||
Кратко — первые 90 дней бесплатно, без банковской карты:
|
||||
Кратко:
|
||||
Все тарифы — первые 90 дней бесплатно, без банковской карты.
|
||||
|
||||
- **Старт** — 5 000 ₸/мес, 1 магазин, 1 касса, 1 склад, до 2 сотрудников
|
||||
- **Бизнес** — от 10 000 ₸/мес, конструктор (до 3 магазинов / 5 касс / 5 складов)
|
||||
- **Старт** — 5 000 тг/мес, 1 магазин, 1 касса, 1 склад, до 2 сотрудников
|
||||
- **Бизнес** — от 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 с указанием причины и реквизитов для возврата.
|
||||
|
||||
## История платежей и документы
|
||||
## История платежей и счёта
|
||||
|
||||
Историю платежей и копии счетов можно запросить в поддержке — billing@food-market.kz. Личный кабинет с историей платежей и скачиванием документов — в разработке.
|
||||
«Настройки» → «Подписка» → «История»:
|
||||
- Список всех платежей за период
|
||||
- Скачивание счетов-фактур (для юрлиц)
|
||||
- Скачивание чеков об оплате (для физлиц)
|
||||
|
||||
## Корпоративные клиенты
|
||||
|
||||
Для тарифа «Сеть» и сетей с несколькими магазинами:
|
||||
|
||||
Для тарифа «Сеть» и сетей с >3 магазинами:
|
||||
- Индивидуальный договор
|
||||
- Безналичный расчёт
|
||||
- Персональный менеджер
|
||||
- Индивидуальные условия поддержки
|
||||
- Безналичный расчёт с НДС
|
||||
- SLA по доступности и скорости поддержки
|
||||
- Выделенный менеджер
|
||||
|
||||
Заявка: sales@food-market.kz
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@ category: Общее
|
|||
order: 5
|
||||
---
|
||||
|
||||
## Регистрация и бесплатный период
|
||||
## Регистрация и триал
|
||||
|
||||
### Сколько длится бесплатный период?
|
||||
### Сколько длится триал?
|
||||
90 дней с момента регистрации. Без необходимости вводить карту.
|
||||
|
||||
### Что произойдёт после 90 дней?
|
||||
Доступ к админке сохранится в режиме просмотра. Касса заблокируется. Если в течение 30 дней не оплатите — данные удаляются.
|
||||
|
||||
### Можно ли активировать бесплатный период повторно?
|
||||
Нет, бесплатный период предоставляется один раз на 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 за склад.
|
||||
|
|
@ -52,13 +52,13 @@ order: 5
|
|||
## Данные и безопасность
|
||||
|
||||
### Где хранятся мои данные?
|
||||
На серверах в Казахстане. Делаем регулярные резервные копии.
|
||||
На серверах в Казахстане. Резервные копии каждые 24 часа.
|
||||
|
||||
### Можно ли экспортировать данные?
|
||||
Да, в любой момент. Экспорт каталога, контрагентов, операций в Excel/CSV.
|
||||
|
||||
### Безопасно ли вводить данные клиентов?
|
||||
Да. Данные шифруются при передаче (TLS) и при хранении. Персональные данные обрабатываем в соответствии с законодательством РК.
|
||||
Да. Данные шифруются при передаче (TLS 1.3) и при хранении. Соответствие ЗРК «О персональных данных».
|
||||
|
||||
### Что будет с моими данными если я закрою аккаунт?
|
||||
30 дней хранятся на случай восстановления, потом удаляются безвозвратно.
|
||||
|
|
@ -66,7 +66,7 @@ order: 5
|
|||
## Импорт и миграция
|
||||
|
||||
### Можно перенести данные из другой системы учёта?
|
||||
Да. Поддерживаем импорт каталога из файла Excel/CSV (название, штрихкод, цена, остаток, группа). Импорт из других систем учёта — в разработке (появится в личном кабинете). Подробнее — на странице [/import](/import).
|
||||
Да. Поддерживаем импорт из файла Excel/CSV, выгрузку 1С (сохранённую в CSV) и перенос из МойСклад по токену. Подробнее — на странице [/import](/import).
|
||||
|
||||
### Какой объём каталога переносится?
|
||||
До 100 000 товаров за один импорт. Каталог 10 000 позиций обычно переносится за 5–10 минут.
|
||||
|
|
@ -85,7 +85,7 @@ order: 5
|
|||
Ежедневно с 09:00 до 18:00 по Астане. В выходные — задержка ответа до 24 часов.
|
||||
|
||||
### Платная ли поддержка?
|
||||
Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка — в тарифах Бизнес и Сеть.
|
||||
Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка с SLA — в тарифах Бизнес и Сеть.
|
||||
|
||||
### Есть ли обучение и внедрение?
|
||||
Большинство клиентов справляются самостоятельно с помощью базы знаний. Если нужна помощь с настройкой или импортом — напишите в поддержку, поможем.
|
||||
|
|
|
|||
|
|
@ -4,29 +4,38 @@ category: Касса
|
|||
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 года
|
||||
- **RAM:** 4 GB минимум, 8 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`)
|
||||
2. Войдите логином сотрудника — касса привяжется к точке продаж
|
||||
3. Дальше — быстрый вход по **PIN** кассира на этом устройстве
|
||||
4. Касса загрузит каталог товаров для работы офлайн
|
||||
1. В админке ALL MARKET: «Касса» → «Ключ организации» — скопируйте 32-символьный код
|
||||
2. В кассе — вставьте ключ, нажмите «Активировать»
|
||||
3. Касса синхронизирует каталог товаров с облаком (~30 секунд для 5000 SKU)
|
||||
|
||||
## Подключение оборудования
|
||||
|
||||
|
|
@ -56,7 +65,7 @@ order: 3
|
|||
4. Выберите принтер из выпадающего списка
|
||||
5. Нажмите «Тестовая печать» — должен распечататься тестовый чек
|
||||
|
||||
Поддерживаются чековые принтеры 58/80 мм (ESC/POS).
|
||||
Поддерживаемые принтеры: любые ОКП РК-совместимые модели.
|
||||
|
||||
## Открытие смены и первая продажа
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import BaseLayout from '@/layouts/BaseLayout.astro'
|
|||
<li>Касса с торговыми весами — для Windows и Linux, работает без интернета.</li>
|
||||
<li>Склад: приёмки, инвентаризация, списание, оприходование, перемещение.</li>
|
||||
<li>Отчёты по продажам, остаткам и прибыли.</li>
|
||||
<li>Перенос каталога из файлов Excel/CSV.</li>
|
||||
<li>Перенос каталога из МойСклад и из файла CSV (в том числе выгрузки 1С).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Что в разработке</h2>
|
||||
|
|
|
|||
|
|
@ -24,3 +24,19 @@ const fmt = post.data.date.toLocaleDateString('ru-KZ', { day: '2-digit', month:
|
|||
<div class="prose-md mt-8"><Content /></div>
|
||||
</article>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
const releases = [
|
||||
{ date: '2026-04', title: 'Публичный сайт и регистрация', items: ['Сайт ALL MARKET','Самостоятельная регистрация магазина','Тарифы: Старт, конструктор «Бизнес», Сеть'] },
|
||||
{ date: '2026-04', title: 'Управление и безопасность', items: ['Управление организациями','Журнал действий','Режим просмотра «открыть как…»','Правки с указанием причины и историей изменений','Настраиваемый срок хранения данных'] },
|
||||
{ date: '2026-04', title: 'Цены, сотрудники и роли', items: ['Гибкие типы цен (розница, опт)','Сотрудники и права доступа','Готовые роли: администратор и кассир'] },
|
||||
{ date: '2026-04', title: 'Закупки и приёмки', items: ['Приёмка товара со сканером','Автоматический расчёт себестоимости','Быстрое добавление позиций'] },
|
||||
{ date: '2026-03', title: 'Каталог и учёт по магазинам', items: ['Данные каждого магазина изолированы друг от друга','Товары, группы, штрихкоды, остатки','Импорт каталога из файлов Excel/CSV'] },
|
||||
{ date: '2026-04', title: 'Phase 6 — Публичный сайт', items: ['Маркетинговый сайт food-market.kz','Регистрация без админа платформы','Тарифы Старт / Бизнес-конструктор / Сеть'] },
|
||||
{ date: '2026-04', title: 'Phase 4 — SuperAdmin консоль', items: ['Управление организациями','Журнал действий','Read-only «открыть как…»','Edit-mode с reason + audit-trail','Настраиваемый retention period'] },
|
||||
{ date: '2026-04', title: 'Phase 3 — Цены и роли', items: ['Расширенная модель цен','Сотрудники и роли','Системные роли (Администратор, Кассир)'] },
|
||||
{ date: '2026-04', title: 'Phase 2 — Закупки и приёмки', items: ['Документ приёмки со сканером','Скользящее среднее себестоимости','Inline quick-add позиций'] },
|
||||
{ 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">
|
||||
<h1 class="text-4xl font-extrabold">Что нового</h1>
|
||||
<p class="text-slate-500 mt-2">История обновлений ALL MARKET.</p>
|
||||
<h1 class="text-4xl font-extrabold">Changelog</h1>
|
||||
<p class="text-slate-500 mt-2">История релизов ALL MARKET.</p>
|
||||
<div class="mt-10 space-y-8">
|
||||
{releases.map((r) => (
|
||||
<article class="rounded-xl border border-slate-200 bg-white p-5">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
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: 'stock', icon: '🏬', title: 'Склад и остатки', items: ['Несколько складов','Приёмка со сканером','Себестоимость считается автоматически','Инвентаризация','Списание (брак/просрочка)','Оприходование','Перемещения между складами','История движений'] },
|
||||
{ id: 'purchase', icon: '🚚', title: 'Закупки и поставщики', items: ['База поставщиков','Документы приёмки','Себестоимость по приёмкам','Возвраты поставщику'] },
|
||||
|
|
|
|||
|
|
@ -2,38 +2,38 @@
|
|||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
|
||||
const formats = [
|
||||
{ icon: '📊', title: 'Файл Excel/CSV', text: 'Выгрузите каталог из старой системы в CSV (Excel умеет «Сохранить как CSV»). ALL MARKET разложит данные по колонкам. Нужны колонки: название, штрихкод, цена — остальное по желанию.' },
|
||||
{ icon: '✍️', title: 'Вручную', text: 'Если товаров немного — заводите карточки прямо в админке: «Товары» → «Добавить». Удобно для 10–50 позиций.' },
|
||||
{ icon: '🧩', title: 'Из других систем учёта', text: 'Импорт напрямую из других систем учёта — в разработке. Появится в личном кабинете магазина.' },
|
||||
{ icon: '📊', title: 'Файл CSV', text: 'Выгрузите каталог из старой системы в CSV (Excel умеет «Сохранить как CSV»). ALL MARKET разложит данные по колонкам. Нужны колонки: название, штрихкод, цена — остальное по желанию.' },
|
||||
{ icon: '🔌', title: 'Из МойСклад', text: 'Введите токен МойСклад — ALL MARKET подтянет товары, контрагентов, остатки и цены.' },
|
||||
{ icon: '📦', title: 'Из 1С', text: 'Сохраните типовую выгрузку 1С (Бухгалтерия, УТ, Розница) в CSV и загрузите файлом. Каталог, цены, штрихкоды, группы.' },
|
||||
]
|
||||
|
||||
const matrix: [string, string][] = [
|
||||
['Товары (название, штрихкоды, цены)', '✅'],
|
||||
['Группы товаров', '✅'],
|
||||
['Единицы измерения', '✅'],
|
||||
['Контрагенты (поставщики, клиенты)', '⚠️ Отдельным файлом'],
|
||||
['Текущие остатки по складам', '⚠️ Отдельным файлом'],
|
||||
['Цены по типам (розница, опт)', '⚠️ Отдельным файлом'],
|
||||
const matrix: [string, string, string][] = [
|
||||
['Товары (название, штрихкоды, цены)', '✅', '✅'],
|
||||
['Группы товаров', '✅', '✅'],
|
||||
['Единицы измерения', '✅', '✅'],
|
||||
['Контрагенты (поставщики, клиенты)', '⚠️ Отдельным файлом', '✅'],
|
||||
['Текущие остатки по складам', '⚠️ Отдельным файлом', '✅'],
|
||||
['Цены по типам (розница, опт)', '⚠️ Отдельным файлом', '✅'],
|
||||
]
|
||||
|
||||
const steps = [
|
||||
{ title: 'Зарегистрируйтесь в ALL MARKET', text: 'Заполните короткую форму: email, название магазина, пароль. 90 дней бесплатно, без карты.' },
|
||||
{ title: 'Подготовьте файл', text: 'Выгрузите каталог из старой системы в CSV (или Excel «Сохранить как CSV»). Нужны колонки: название, штрихкод, цена.' },
|
||||
{ title: 'Запустите импорт', text: 'В админке: «Импорт» → «Из файла» → загрузите файл → сопоставьте колонки → нажмите «Запустить».' },
|
||||
{ title: 'Подготовьте данные', text: 'CSV: выгрузите каталог из старой системы в CSV. Из МойСклад: получите токен в настройках МойСклад.' },
|
||||
{ title: 'Запустите импорт', text: 'В админке: «Импорт» → выберите формат → загрузите файл или введите токен → нажмите «Запустить».' },
|
||||
{ title: 'Проверьте результат', text: 'Откройте «Товары» — каталог должен быть полным. Проверьте 5–10 случайных товаров: цены, штрихкоды, остатки.' },
|
||||
{ title: 'Начните вести учёт', text: 'Каталог, остатки и отчёты — сразу в браузере. Касса для Windows и Linux появится для скачивания скоро.' },
|
||||
]
|
||||
|
||||
const faq = [
|
||||
{ q: 'Сохранятся ли данные в исходной системе?', a: 'Да. Импорт работает с копией вашего файла — он ничего не меняет в исходной системе. Ваш текущий учёт остаётся нетронутым.' },
|
||||
{ q: 'Можно ли работать в двух системах параллельно?', a: 'Технически — да, в бесплатный период. Но синхронизации в реальном времени нет — это создаст путаницу с остатками. Лучше выбрать одну систему как основную.' },
|
||||
{ q: 'Сохранятся ли данные в исходной системе?', a: 'Да. Импорт из МойСклад только читает данные — ничего не удаляет и не меняет. Импорт из файла работает с копией. Ваш текущий учёт остаётся нетронутым.' },
|
||||
{ q: 'Можно ли работать в обеих системах параллельно?', a: 'Технически — да, в бесплатный период. Но синхронизации в реальном времени нет — это создаст путаницу с остатками. Лучше выбрать одну систему как основную.' },
|
||||
{ q: 'Что если у меня кастомные поля или специфичные настройки?', a: 'Напишите нам перед импортом — посмотрим. В большинстве случаев всё переносится. Если что-то нестандартное — настроим вручную.' },
|
||||
{ q: 'Сколько времени занимает импорт?', a: 'Каталог 1 000 товаров — около 1 минуты. 10 000 товаров — 5–10 минут. 50 000+ — до часа. Прогресс отображается в реальном времени.' },
|
||||
{ q: 'Сколько стоит импорт?', a: 'Бесплатно. Никаких сборов за импорт, настройку или консультацию.' },
|
||||
{ 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">
|
||||
<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>
|
||||
|
|
@ -73,14 +73,16 @@ const faq = [
|
|||
<thead class="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{matrix.map(([entity, csv]) => (
|
||||
{matrix.map(([entity, csv, api]) => (
|
||||
<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 text-center">{csv}</td>
|
||||
<td class="px-5 py-3.5 text-center">{api}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import BaseLayout from '@/layouts/BaseLayout.astro'
|
|||
import FAQ from '@/components/FAQ.tsx'
|
||||
|
||||
const modules = [
|
||||
{ icon: '📦', title: 'Товары и каталог', text: 'Группы, штрихкоды, цены, остатки. Перенос каталога из файла (Excel/CSV).' },
|
||||
{ icon: '📦', title: 'Товары и каталог', text: 'Группы, штрихкоды, цены, остатки. Перенос каталога файлом или из МойСклад.' },
|
||||
{ icon: '💳', title: 'Продажи и касса', text: 'Касса с весами, сканером и печатью чека. Работает на Windows и Linux, в том числе без интернета.' },
|
||||
{ icon: '🏬', title: 'Склад и приёмки', text: 'Несколько складов, приёмка со сканером, инвентаризация, списание, оприходование, перемещение.' },
|
||||
{ icon: '🚚', title: 'Закупки и себестоимость', text: 'Приёмки от поставщиков. Себестоимость считается автоматически по приёмкам.' },
|
||||
|
|
@ -60,7 +60,7 @@ const modules = [
|
|||
<div class="rounded-xl border border-slate-200 p-5">
|
||||
<div class="text-3xl">⬇️</div>
|
||||
<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 class="rounded-xl border border-slate-200 p-5">
|
||||
<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">
|
||||
<h2 class="text-3xl font-bold mb-3">Перенос и интеграции</h2>
|
||||
<p class="text-slate-600">
|
||||
Переносим каталог из файлов Excel/CSV — уже сейчас.
|
||||
Переносим каталог из МойСклад и из CSV (в том числе выгрузки из 1С) — уже сейчас.
|
||||
Онлайн-касса (ОФД), приём оплат картой и маркетплейсы — в разработке.
|
||||
</p>
|
||||
<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>Без ограничения по числу товаров</li>
|
||||
<li>Касса с весами</li>
|
||||
<li>Перенос каталога из файлов Excel/CSV</li>
|
||||
<li>Перенос каталога из CSV и МойСклад</li>
|
||||
</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>
|
||||
</div>
|
||||
|
|
@ -214,7 +214,7 @@ const modules = [
|
|||
<li>До 10 сотрудников</li>
|
||||
<li>Все отчёты по прибыли и остаткам</li>
|
||||
</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 class="rounded-xl border border-slate-200 p-6 bg-white">
|
||||
<h3 class="font-bold text-lg">Сеть</h3>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
const ready = [
|
||||
{ title: 'Перенос каталога', items: ['Импорт каталога из файла Excel/CSV', 'Переносятся названия, штрихкоды, цены, остатки, группы'] },
|
||||
{ title: 'Оборудование', items: ['Торговые весы CAS, Масса-К и совместимые (USB/COM)', 'USB-сканеры штрихкодов', 'Чековые принтеры 58/80 мм (ESC/POS)', 'Денежные ящики', 'Дисплей покупателя (RS-232)'] },
|
||||
{ title: 'Перенос каталога', items: ['Импорт из МойСклад по токену', 'Импорт из файла CSV', 'Выгрузка 1С (сохранённая в CSV)'] },
|
||||
{ title: 'Оборудование', items: ['Торговые весы CAS, Масса-К и совместимые (USB/COM)', 'USB-сканеры штрихкодов', 'Чековые принтеры 58/80 мм', 'Денежные ящики', 'Дисплей покупателя (по RS-232)'] },
|
||||
]
|
||||
const planned = [
|
||||
{ title: 'Онлайн-касса и оплаты', items: ['Фискализация через ОФД РК', 'Приём оплат картой (Kaspi Pay и банки)'] },
|
||||
{ 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">
|
||||
<h1 class="text-4xl font-extrabold text-center">Перенос и интеграции</h1>
|
||||
<p class="text-center text-slate-500 mt-3">Честно о том, что уже работает и что готовим.</p>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ import BusinessTariffBuilder from '@/components/BusinessTariffBuilder.tsx'
|
|||
<li>✓ 2 сотрудника</li>
|
||||
<li>✓ Без лимита товаров</li>
|
||||
<li>✓ Касса с поддержкой торговых весов</li>
|
||||
<li>✓ Перенос каталога из файлов Excel/CSV</li>
|
||||
<li>✓ Импорт из других систем</li>
|
||||
<li>✓ Перенос каталога из CSV и МойСклад</li>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div id="configurator" class="lg:row-span-2 scroll-mt-20">
|
||||
<div class="lg:row-span-2">
|
||||
<BusinessTariffBuilder client:load />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { APIRoute } from 'astro'
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
const PAGES = [
|
||||
'', 'features', 'pricing', 'pos', 'import', 'integrations',
|
||||
|
|
@ -8,19 +7,9 @@ const PAGES = [
|
|||
'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(/\/$/, '')
|
||||
// Канонические URL — со слэшем на конце (Astro отдаёт .../features/, а запрос
|
||||
// без слэша 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 urls = PAGES.map((p) => `<url><loc>${base}/${p}</loc></url>`).join('\n ')
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
---
|
||||
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">
|
||||
<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">
|
||||
<p class="text-slate-700">Публичная страница статуса с автоматическим мониторингом и историей инцидентов — в разработке. Пока реального статуса в реальном времени здесь нет, поэтому мы не показываем его, чтобы не вводить в заблуждение.</p>
|
||||
<p class="text-slate-600 mt-4">Если вы столкнулись с перебоем в работе — напишите нам, ответим оперативно:</p>
|
||||
<p class="mt-3 text-sm">
|
||||
<a href="mailto:support@food-market.kz" class="text-brand underline">support@food-market.kz</a>
|
||||
<span class="text-slate-400"> · </span>
|
||||
Telegram <span class="font-medium">@FoodMarketKZ_Support</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-6 text-center">Приложение кассы для Windows и Linux — скоро.</p>
|
||||
<ul class="mt-10 divide-y divide-slate-200 border border-slate-200 rounded-xl bg-white">
|
||||
<li class="flex items-center justify-between px-5 py-4"><span>API</span><span class="text-emerald-600 text-sm">● Работает</span></li>
|
||||
<li class="flex items-center justify-between px-5 py-4"><span>Админка</span><span class="text-emerald-600 text-sm">● Работает</span></li>
|
||||
<li class="flex items-center justify-between px-5 py-4"><span>База данных</span><span class="text-emerald-600 text-sm">● Работает</span></li>
|
||||
<li class="flex items-center justify-between px-5 py-4"><span>Касса (Windows)</span><span class="text-slate-400 text-sm">○ Скоро</span></li>
|
||||
</ul>
|
||||
<p class="text-xs text-slate-400 mt-6 text-center">Раздел в разработке. Скоро добавим автоматический мониторинг и историю инцидентов.</p>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
|
|
|||
|
|
@ -10,36 +10,3 @@
|
|||
|
||||
html { scroll-behavior: smooth; }
|
||||
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); }
|
||||
|
|
|
|||
|
|
@ -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('; '))
|
||||