fix(site): критичные + честность текстов на test.food-market.kz (этап 1)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions

1. /signup читает ?plan= и конфигурацию (общий модуль lib/pricing.ts): предвыбор тарифа,
   для «Бизнес» — конфигурация и цена (15 500 ₸ для 3/4/1); неизвестный план → «Старт».
2. Мобильная шапка: гамбургер + панель со всеми пунктами (aria-expanded, Esc/клик-вне, ≥44px).
3. og-картинки без ОФД/REST API/ABC — перегенерированы.
4. KB/блог: убраны несуществующие обещания (Kaspi Pay/Halyk/Visa, REST API, «Скачать установщик»,
   Kaspi Magazin Q3, маркировка, платные консультации, скидки 3/6/12, «Триал» как тариф).
   Противоречия сведены к оферте (удаление данных 30 дн, возврат по заявлению, Старт=2 сотр.).
5. /import: чужой HR-скриншот заменён на скриншот каталога продукта.
6. KB рендерился как сырой front-matter в code-блоке — развёрнут, дубль # убран.
7. Аналитика без реальных ID не подключается (нет data-id=REPLACE_ME).
8. robots.txt → рабочий /sitemap.xml.
9. Футер: дубль «Дорожная карта»/«Changelog» убран; 10. зоны нажатия футера + width/height у lazy-картинок.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-10 23:53:58 +05:00
parent 5cb2c5934c
commit b8e42e216c
21 changed files with 215 additions and 143 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View file

@ -1,3 +1,3 @@
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: https://test.food-market.kz/sitemap-index.xml Sitemap: https://test.food-market.kz/sitemap.xml

View file

@ -16,12 +16,12 @@ const logoPng = await fs.readFile(path.join(__dirname, 'allmarket-logo-color.png
const logoUri = `data:image/png;base64,${logoPng.toString('base64')}` const logoUri = `data:image/png;base64,${logoPng.toString('base64')}`
const pages = [ const pages = [
{ file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами, импорт каталога, ОФД РК. Бесплатно 90 дней.' }, { file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами и импортом каталога. Бесплатно 90 дней.' },
{ file: 'pos.png', title: 'Касса для Windows с поддержкой торговых весов', sub: 'Установка за 5 минут. Офлайн-режим. Стандартное оборудование.' }, { file: 'pos.png', title: 'Касса для Windows с поддержкой торговых весов', sub: 'Установка за 5 минут. Офлайн-режим. Стандартное оборудование.' },
{ file: 'pricing.png', title: 'Прозрачные тарифы ALL MARKET', sub: 'От 5 000 ₸/мес. Все возможности на любом тарифе. 90 дней триал.' }, { file: 'pricing.png', title: 'Прозрачные тарифы ALL MARKET', sub: 'От 5 000 ₸/мес. Все возможности на любом тарифе. 90 дней триал.' },
{ file: 'import.png', title: 'Перенесите каталог за один клик', sub: 'Excel, CSV, REST API. До 100 000 товаров. Бесплатно.' }, { file: 'import.png', title: 'Перенесите каталог за один клик', sub: 'Excel, CSV, 1С, МойСклад. До 100 000 товаров. Бесплатно.' },
{ file: 'about.png', title: 'О ALL MARKET', sub: 'Программа учёта розничной торговли, созданная в Казахстане.' }, { file: 'about.png', title: 'О ALL MARKET', sub: 'Программа учёта розничной торговли, созданная в Казахстане.' },
{ file: 'for-grocery.png', title: 'ALL MARKET для продуктового магазина', sub: 'Весовой товар, скоропорт, штрихкоды, ABC-анализ.' }, { file: 'for-grocery.png', title: 'ALL MARKET для продуктового магазина', sub: 'Весовой товар, скоропорт, штрихкоды, учёт остатков.' },
{ file: 'for-pharmacy.png', title: 'ALL MARKET для аптеки', sub: 'Серии, сроки годности, рецептурный отпуск, контроль ЛП.' }, { file: 'for-pharmacy.png', title: 'ALL MARKET для аптеки', sub: 'Серии, сроки годности, рецептурный отпуск, контроль ЛП.' },
{ file: 'for-cafe.png', title: 'ALL MARKET для кафе и общепита', sub: 'Модификаторы блюд, столики, печать на кухню.' }, { file: 'for-cafe.png', title: 'ALL MARKET для кафе и общепита', sub: 'Модификаторы блюд, столики, печать на кухню.' },
{ file: 'for-alcohol.png', title: 'ALL MARKET для алкомаркета', sub: 'Акцизные марки, контроль времени продаж, маркировка.' }, { file: 'for-alcohol.png', title: 'ALL MARKET для алкомаркета', sub: 'Акцизные марки, контроль времени продаж, маркировка.' },

View file

@ -1,9 +1,10 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { PRICING, businessPrice, fmtKzt } from '@/lib/pricing'
const BASE = 10000 const BASE = PRICING.base
const PER_SHOP = 2000 const PER_SHOP = PRICING.perShop
const PER_CASH = 500 const PER_CASH = PRICING.perCash
const PER_WAREHOUSE = 500 const PER_WAREHOUSE = PRICING.perWarehouse
interface SliderProps { interface SliderProps {
label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string
@ -31,12 +32,7 @@ export default function BusinessTariffBuilder() {
const [warehouses, setWarehouses] = useState(1) const [warehouses, setWarehouses] = useState(1)
const [employees, setEmployees] = useState(5) const [employees, setEmployees] = useState(5)
const total = useMemo(() => { const total = useMemo(() => businessPrice({ shops, cashes, warehouses }), [shops, cashes, warehouses])
return BASE
+ Math.max(0, shops - 1) * PER_SHOP
+ Math.max(0, cashes - 1) * PER_CASH
+ Math.max(0, warehouses - 1) * PER_WAREHOUSE
}, [shops, cashes, warehouses])
const fmt = new Intl.NumberFormat('ru') const fmt = new Intl.NumberFormat('ru')
const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}` const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}`

View file

@ -12,33 +12,32 @@ import Logo from './Logo.astro'
<div> <div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Продукт</h3> <h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Продукт</h3>
<ul class="space-y-1.5"> <ul class="space-y-1.5">
<li><a href="/features" class="text-slate-700 hover:text-brand">Возможности</a></li> <li><a href="/features" class="block py-2 text-slate-700 hover:text-brand">Возможности</a></li>
<li><a href="/pos" class="text-slate-700 hover:text-brand">Касса</a></li> <li><a href="/pos" class="block py-2 text-slate-700 hover:text-brand">Касса</a></li>
<li><a href="/import" class="text-slate-700 hover:text-brand">Импорт каталога</a></li> <li><a href="/import" class="block py-2 text-slate-700 hover:text-brand">Импорт каталога</a></li>
<li><a href="/for-grocery" class="text-slate-700 hover:text-brand">Для продуктовых</a></li> <li><a href="/for-grocery" class="block py-2 text-slate-700 hover:text-brand">Для продуктовых</a></li>
<li><a href="/integrations" class="text-slate-700 hover:text-brand">Перенос и интеграции</a></li> <li><a href="/integrations" class="block py-2 text-slate-700 hover:text-brand">Перенос и интеграции</a></li>
<li><a href="/pricing" class="text-slate-700 hover:text-brand">Тарифы</a></li> <li><a href="/pricing" class="block py-2 text-slate-700 hover:text-brand">Тарифы</a></li>
<li><a href="/changelog" class="text-slate-700 hover:text-brand">Дорожная карта</a></li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Ресурсы</h3> <h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Ресурсы</h3>
<ul class="space-y-1.5"> <ul class="space-y-1.5">
<li><a href="/kb" class="text-slate-700 hover:text-brand">База знаний</a></li> <li><a href="/kb" class="block py-2 text-slate-700 hover:text-brand">База знаний</a></li>
<li><a href="/blog" class="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="text-slate-700 hover:text-brand">Status</a></li> <li><a href="/status" class="block py-2 text-slate-700 hover:text-brand">Status</a></li>
<li><a href="/changelog" class="text-slate-700 hover:text-brand">Changelog</a></li> <li><a href="/changelog" class="block py-2 text-slate-700 hover:text-brand">Changelog</a></li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Компания</h3> <h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Компания</h3>
<ul class="space-y-1.5"> <ul class="space-y-1.5">
<li><a href="/about" class="text-slate-700 hover:text-brand">О нас</a></li> <li><a href="/about" class="block py-2 text-slate-700 hover:text-brand">О нас</a></li>
<li><a href="/contacts" class="text-slate-700 hover:text-brand">Контакты</a></li> <li><a href="/contacts" class="block py-2 text-slate-700 hover:text-brand">Контакты</a></li>
<li><a href="/legal/offer" class="text-slate-700 hover:text-brand">Оферта</a></li> <li><a href="/legal/offer" class="block py-2 text-slate-700 hover:text-brand">Оферта</a></li>
<li><a href="/legal/privacy" class="text-slate-700 hover:text-brand">Политика ПДн</a></li> <li><a href="/legal/privacy" class="block py-2 text-slate-700 hover:text-brand">Политика ПДн</a></li>
<li><a href="/legal/consent" class="text-slate-700 hover:text-brand">Согласие ПДн</a></li> <li><a href="/legal/consent" class="block py-2 text-slate-700 hover:text-brand">Согласие ПДн</a></li>
<li><a href="/legal/requisites" class="text-slate-700 hover:text-brand">Реквизиты</a></li> <li><a href="/legal/requisites" class="block py-2 text-slate-700 hover:text-brand">Реквизиты</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -1,24 +1,81 @@
--- ---
import Logo from './Logo.astro' import Logo from './Logo.astro'
const APP_URL = import.meta.env.PUBLIC_APP_URL || 'https://admin.food-market.kz' const APP_URL = import.meta.env.PUBLIC_APP_URL || 'https://admin.food-market.kz'
const nav = [
{ href: '/features', label: 'Возможности' },
{ href: '/pos', label: 'Касса' },
{ href: '/for-grocery', label: 'Для продуктовых' },
{ href: '/import', label: 'Импорт' },
{ href: '/pricing', label: 'Тарифы' },
{ href: '/kb', label: 'База знаний' },
{ href: '/blog', label: 'Блог' },
]
--- ---
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-slate-200"> <header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-slate-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
<a href="/" class="flex items-center gap-2"><Logo /></a> <a href="/" class="flex items-center gap-2"><Logo /></a>
<nav class="hidden lg:flex items-center gap-1 text-sm"> <nav class="hidden lg:flex items-center gap-1 text-sm">
<a href="/features" class="px-3 py-2 hover:text-brand transition">Возможности</a> {nav.map((i) => <a href={i.href} class="px-3 py-2 hover:text-brand transition">{i.label}</a>)}
<a href="/pos" class="px-3 py-2 hover:text-brand transition">Касса</a>
<a href="/for-grocery" class="px-3 py-2 hover:text-brand transition">Для продуктовых</a>
<a href="/import" class="px-3 py-2 hover:text-brand transition">Импорт</a>
<a href="/pricing" class="px-3 py-2 hover:text-brand transition">Тарифы</a>
<a href="/kb" class="px-3 py-2 hover:text-brand transition">База знаний</a>
<a href="/blog" class="px-3 py-2 hover:text-brand transition">Блог</a>
</nav> </nav>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href={`${APP_URL}/login`} class="px-3 py-1.5 text-sm text-slate-700 hover:text-brand">Войти</a> <a href={`${APP_URL}/login`} class="px-3 min-h-[44px] flex items-center text-sm text-slate-700 hover:text-brand">Войти</a>
<a href="/signup" class="px-3 py-1.5 text-sm font-medium bg-brand text-white rounded-md hover:bg-brand-hover">Начать бесплатно</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="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"
>
<svg id="mobile-menu-icon-open" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="17" x2="20" y2="17"/></svg>
<svg id="mobile-menu-icon-close" class="hidden" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
</button>
</div> </div>
</div> </div>
<!-- Мобильная панель: все пункты десктоп-меню + входы. Зоны нажатия ≥ 44px. -->
<nav id="mobile-menu" class="hidden lg:hidden border-t border-slate-200 bg-white" aria-label="Мобильное меню">
<div class="max-w-7xl mx-auto px-2 sm:px-4 py-2">
{nav.map((i) => <a href={i.href} class="flex items-center min-h-[44px] px-3 rounded-md text-[15px] text-slate-800 hover:bg-slate-100 hover:text-brand">{i.label}</a>)}
<div class="mt-2 pt-2 border-t border-slate-100 grid grid-cols-2 gap-2">
<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>
</nav>
</header> </header>
<script>
const btn = document.getElementById('mobile-menu-btn')
const menu = document.getElementById('mobile-menu')
const iconOpen = document.getElementById('mobile-menu-icon-open')
const iconClose = document.getElementById('mobile-menu-icon-close')
if (btn && menu) {
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)
}
btn.addEventListener('click', (e) => {
e.stopPropagation()
setOpen(menu.classList.contains('hidden'))
})
// закрытие по тапу вне меню
document.addEventListener('click', (e) => {
if (menu.classList.contains('hidden')) return
const t = e.target as Node
if (!menu.contains(t) && !btn.contains(t)) setOpen(false)
})
// закрытие по Esc
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !menu.classList.contains('hidden')) { setOpen(false); (btn as HTMLButtonElement).focus() }
})
// при переходе к десктопной ширине — сбросить состояние
window.matchMedia('(min-width: 1024px)').addEventListener('change', (m) => { if (m.matches) setOpen(false) })
}
</script>

View file

@ -1,6 +1,34 @@
import { useState } from 'react' import { useState } from 'react'
import { validateEmail, validatePassword, validatePhone } from '@/lib/validation' import { validateEmail, validatePassword, validatePhone } from '@/lib/validation'
import { PhoneInput } from '@/components/PhoneInput' import { PhoneInput } from '@/components/PhoneInput'
import { PRICING, businessPrice, clamp, fmtKzt, type BusinessConfig } from '@/lib/pricing'
const VALID_PLANS = ['start', 'business', 'network'] as const
type PlanId = (typeof VALID_PLANS)[number]
/** Читаем ?plan= и параметры конфигуратора из URL на клиенте (сайт статический —
* серверного query нет). Неизвестный план → «Старт». Параметры конфигурации
* клампуются к границам конструктора. */
function readUrlSelection(fallbackPlan: string): { plan: PlanId; config: BusinessConfig; hasConfig: boolean } {
const planFallback: PlanId = (VALID_PLANS as readonly string[]).includes(fallbackPlan) ? (fallbackPlan as PlanId) : 'start'
let plan = planFallback
const cfg: BusinessConfig = { shops: PRICING.shops.min, cashes: PRICING.cashes.min, warehouses: PRICING.warehouses.min, employees: PRICING.employees.min }
let hasConfig = false
if (typeof window !== 'undefined') {
const q = new URLSearchParams(window.location.search)
const p = q.get('plan')
plan = p && (VALID_PLANS as readonly string[]).includes(p) ? (p as PlanId) : 'start'
const num = (k: string, rng: { min: number; max: number }) => {
const v = q.get(k); if (v == null || v === '') return undefined
hasConfig = true; return clamp(Number(v), rng.min, rng.max)
}
cfg.shops = num('shops', PRICING.shops) ?? cfg.shops
cfg.cashes = num('cashes', PRICING.cashes) ?? cfg.cashes
cfg.warehouses = num('warehouses', PRICING.warehouses) ?? cfg.warehouses
cfg.employees = num('employees', PRICING.employees) ?? cfg.employees
}
return { plan, config: cfg, hasConfig }
}
const APP_URL = (import.meta.env.PUBLIC_APP_URL as string | undefined) ?? 'https://admin.food-market.kz' const APP_URL = (import.meta.env.PUBLIC_APP_URL as string | undefined) ?? 'https://admin.food-market.kz'
const API_URL = APP_URL const API_URL = APP_URL
@ -15,11 +43,16 @@ interface Props {
type FieldErrors = Partial<Record<'email' | 'password' | 'orgName' | 'phone' | 'agree', string>> type FieldErrors = Partial<Record<'email' | 'password' | 'orgName' | 'phone' | 'agree', string>>
export default function SignupForm({ defaultPlan = 'start' }: Props) { export default function SignupForm({ defaultPlan = 'start' }: Props) {
// Разбор ?plan= и конфигурации из URL — один раз при инициализации (клиент).
const [sel] = useState(() => readUrlSelection(defaultPlan))
const [config] = useState<BusinessConfig>(sel.config)
const hasConfig = sel.hasConfig
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [orgName, setOrgName] = useState('') const [orgName, setOrgName] = useState('')
const [phone, setPhone] = useState('') const [phone, setPhone] = useState('')
const [plan, setPlan] = useState(defaultPlan) const [plan, setPlan] = useState<string>(sel.plan)
const businessTotal = businessPrice(config)
const [agree, setAgree] = useState(false) const [agree, setAgree] = useState(false)
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -45,7 +78,15 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
const res = await fetch(`${API_URL}/api/auth/signup`, { const res = await fetch(`${API_URL}/api/auth/signup`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, organizationName: orgName, phone: phone || null, plan }), // plan сервер принимает (SignupInput.Plan). Параметры конфигурации «Бизнес»
// отправляем вперёд-совместимо; текущий контракт их игнорирует (решение сервера —
// принимать ли — отдельно, см. отчёт site-fixes).
body: JSON.stringify({
email, password, organizationName: orgName, phone: phone || null, plan,
...(plan === 'business' && hasConfig
? { shops: config.shops, cashes: config.cashes, warehouses: config.warehouses, employees: config.employees }
: {}),
}),
}) })
if (!res.ok) { if (!res.ok) {
const j = await res.json().catch(() => ({})) const j = await res.json().catch(() => ({}))
@ -154,8 +195,8 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
<div className="text-sm font-medium mb-2">Тариф</div> <div className="text-sm font-medium mb-2">Тариф</div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{[ {[
{ id: 'start', name: 'Старт', price: '5 000 ₸' }, { id: 'start', name: 'Старт', price: fmtKzt(PRICING.startPrice) },
{ id: 'business', name: 'Бизнес', price: 'от 10 000 ₸' }, { id: 'business', name: 'Бизнес', price: hasConfig ? `${fmtKzt(businessTotal)}/мес` : 'от 10 000 ₸' },
{ id: 'network', name: 'Сеть', price: 'по запросу' }, { id: 'network', name: 'Сеть', price: 'по запросу' },
].map((p) => ( ].map((p) => (
<label key={p.id} className={`p-2.5 rounded-md border cursor-pointer text-center text-sm ${plan === p.id ? 'border-brand bg-brand/5' : 'border-slate-200'}`}> <label key={p.id} className={`p-2.5 rounded-md border cursor-pointer text-center text-sm ${plan === p.id ? 'border-brand bg-brand/5' : 'border-slate-200'}`}>
@ -165,6 +206,21 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
</label> </label>
))} ))}
</div> </div>
{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>
<span className="font-bold">{fmtKzt(businessTotal)}/мес</span>
</div>
<div className="mt-1.5 text-xs text-slate-600 grid grid-cols-2 gap-x-4 gap-y-0.5">
<span>Магазинов: <b>{config.shops}</b></span>
<span>Касс: <b>{config.cashes}</b></span>
<span>Складов: <b>{config.warehouses}</b></span>
<span>Сотрудников: <b>{config.employees}</b></span>
</div>
<p className="mt-1.5 text-[11px] text-slate-400">Изменить — в <a href="/pricing" className="underline">конструкторе тарифов</a>.</p>
</div>
)}
</div> </div>
<div> <div>

View file

@ -40,4 +40,4 @@ cover_image: /photos/blog-scales.jpg
- **Дешевле в эксплуатации** — не платите за сторонние модули интеграции - **Дешевле в эксплуатации** — не платите за сторонние модули интеграции
- **Проще обучение кассиров** — они работают как привыкли, никакой «сложной системы» - **Проще обучение кассиров** — они работают как привыкли, никакой «сложной системы»
[Скачать кассу →](/pos) [Подробнее о кассе →](/pos)

View file

@ -35,10 +35,9 @@ cover_image: /photos/blog-launch.jpg
Сейчас мы открываем регистрацию и принимаем первых клиентов. Будем активно собирать обратную связь и улучшать продукт каждую неделю. Сейчас мы открываем регистрацию и принимаем первых клиентов. Будем активно собирать обратную связь и улучшать продукт каждую неделю.
В ближайших планах: В ближайших планах:
- Интеграция с Kaspi Magazin (Q3 2026) - Интеграция с Kaspi Magazin
- Marketplace интеграции (Wildberries, Ozon — Q4 2026) - Marketplace интеграции (Wildberries, Ozon — Q4 2026)
- Мобильное приложение для руководителя - Мобильное приложение для руководителя
- Готовая архитектура для будущей маркировки в РК
[Зарегистрироваться →](/signup) [Зарегистрироваться →](/signup)

View file

@ -19,7 +19,7 @@ cover_image: /photos/blog-quickstart.jpg
- Email — это будет ваш логин - Email — это будет ваш логин
- Пароль (минимум 8 символов) - Пароль (минимум 8 символов)
- Название магазина - Название магазина
- Выберите тариф (можно оставить «Триал» — 90 дней бесплатно) - Выберите тариф (Старт, Бизнес или Сеть) — первые 90 дней бесплатно
Через 30 секунд после нажатия «Зарегистрироваться» вы окажетесь в админке ALL MARKET с готовым к работе пустым магазином. Через 30 секунд после нажатия «Зарегистрироваться» вы окажетесь в админке ALL MARKET с готовым к работе пустым магазином.
@ -42,7 +42,7 @@ cover_image: /photos/blog-quickstart.jpg
## Шаг 3. Установите кассу (5 минут) ## Шаг 3. Установите кассу (5 минут)
В админке: «Касса» → «Скачать установщик». Приложение кассы для Windows — скоро (появится в разделе «Касса»).
1. Запустите .msi файл — нажмите «Далее» три раза 1. Запустите .msi файл — нажмите «Далее» три раза
2. Введите ключ организации (показан в админке) 2. Введите ключ организации (показан в админке)
@ -57,7 +57,7 @@ cover_image: /photos/blog-quickstart.jpg
В кассе: В кассе:
1. «Открыть смену» — введите начальный остаток наличных 1. «Открыть смену» — введите начальный остаток наличных
2. Сосканируйте штрихкод любого товара — он появится в чеке 2. Сосканируйте штрихкод любого товара — он появится в чеке
3. «Оплата» → выберите тип (Наличные / Kaspi / Карта) 3. «Оплата» → выберите тип (наличные или карта)
4. Введите сумму полученных денег 4. Введите сумму полученных денег
5. Подтвердите — чек распечатается 5. Подтвердите — чек распечатается
@ -68,7 +68,6 @@ cover_image: /photos/blog-quickstart.jpg
После первой продажи: После первой продажи:
- Добавьте остальных кассиров — «Сотрудники» → «Добавить» - Добавьте остальных кассиров — «Сотрудники» → «Добавить»
- Настройте программу лояльности — «Лояльность» → «Создать программу» - Настройте программу лояльности — «Лояльность» → «Создать программу»
- Подключите Kaspi Pay для приёма безнала — «Интеграции» → «Kaspi»
- Запустите акции и скидки — «Маркетинг» → «Акции» - Запустите акции и скидки — «Маркетинг» → «Акции»
Полная документация в нашей [Базе знаний](/kb). Полная документация в нашей [Базе знаний](/kb).

View file

@ -4,16 +4,6 @@ category: Биллинг
order: 4 order: 4
--- ---
```markdown
---
title: Тарифы, оплата, изменение тарифа
slug: billing
section: account
order: 1
---
# Тарифы, оплата, изменение тарифа
Полная информация о подписках ALL MARKET и порядке оплаты. Полная информация о подписках ALL MARKET и порядке оплаты.
## Тарифы ## Тарифы
@ -21,8 +11,9 @@ order: 1
См. полную таблицу на [странице тарифов](/pricing). См. полную таблицу на [странице тарифов](/pricing).
Кратко: Кратко:
- **Триал** — 90 дней, бесплатно, без карты, все возможности Все тарифы — первые 90 дней бесплатно, без банковской карты.
- **Старт** — 5 000 тг/мес, 1 магазин, 1 касса, 1 склад
- **Старт** — 5 000 тг/мес, 1 магазин, 1 касса, 1 склад, до 2 сотрудников
- **Бизнес** — от 10 000 тг/мес, конструктор (до 3 магазинов / 5 касс / 5 складов) - **Бизнес** — от 10 000 тг/мес, конструктор (до 3 магазинов / 5 касс / 5 складов)
- **Сеть** — по запросу, без ограничений - **Сеть** — по запросу, без ограничений
@ -30,11 +21,7 @@ order: 1
### Для физических лиц и ИП ### Для физических лиц и ИП
- Kaspi Pay (карта или счёт) Способ оплаты подписки уточняется — свяжитесь с нами: support@food-market.kz. Приём оплат картой онлайн — в разработке.
- Halyk Bank, Jusan, Forte и другие казахстанские банки
- Visa / Mastercard
Оплата проходит мгновенно. Доступ продлевается на оплаченный период.
### Для юридических лиц ### Для юридических лиц
@ -43,15 +30,6 @@ order: 1
- Срок оплаты — 5 рабочих дней с даты выставления счёта - Срок оплаты — 5 рабочих дней с даты выставления счёта
- Доступ продлевается после поступления средств на расчётный счёт - Доступ продлевается после поступления средств на расчётный счёт
## Скидки за длительные периоды
При оплате авансом:
- 3 месяца: −5%
- 6 месяцев: −10%
- 12 месяцев: −20%
Скидки применяются автоматически при выборе периода в форме оплаты.
## Изменение тарифа ## Изменение тарифа
В любой момент через «Настройки» → «Подписка». В любой момент через «Настройки» → «Подписка».
@ -78,17 +56,14 @@ order: 1
- Импорт и экспорт работают - Импорт и экспорт работают
Через 30 дней без оплаты: Через 30 дней без оплаты:
- Учётная запись помечается на удаление
- Email с финальным предупреждением - Email с финальным предупреждением
- Через 60 дней без оплаты — данные удаляются - Контент сохраняется 30 календарных дней (см. оферту, п. 5.3/11.3), после чего может быть удалён
## Возврат средств ## Возврат средств
Возврат за неиспользованный период возможен: Возврат денежных средств за неиспользованный период осуществляется в случаях, предусмотренных законодательством РК, по письменному заявлению (см. оферту, п. 6.5).
- В течение первых 14 дней с момента оплаты — без обоснования
- После 14 дней — пропорционально неиспользованному периоду
Заявка на возврат: support@food-market.kz с указанием причины и реквизитов для возврата. Заявка: support@food-market.kz с указанием причины и реквизитов для возврата.
## История платежей и счёта ## История платежей и счёта
@ -106,5 +81,3 @@ order: 1
- Выделенный менеджер - Выделенный менеджер
Заявка: sales@food-market.kz Заявка: sales@food-market.kz
```

View file

@ -4,16 +4,6 @@ category: Общее
order: 5 order: 5
--- ---
```markdown
---
title: Частые вопросы
slug: faq
section: general
order: 99
---
# Частые вопросы
## Регистрация и триал ## Регистрация и триал
### Сколько длится триал? ### Сколько длится триал?
@ -31,7 +21,7 @@ order: 99
## Тарифы и оплата ## Тарифы и оплата
### Сколько стоит самый дешёвый тариф? ### Сколько стоит самый дешёвый тариф?
Старт — 5 000 тг/мес. Включает 1 магазин, 1 кассу, 1 склад, без лимита товаров и пользователей. Старт — 5 000 тг/мес. Включает 1 магазин, 1 кассу, 1 склад, до 2 сотрудников, без лимита товаров.
### Что входит в тариф Бизнес? ### Что входит в тариф Бизнес?
Конструктор: до 3 магазинов, до 5 касс, до 5 складов. Цена считается по формуле base 10 000 + 2 000 за магазин + 500 за кассу + 500 за склад. Конструктор: до 3 магазинов, до 5 касс, до 5 складов. Цена считается по формуле base 10 000 + 2 000 за магазин + 500 за кассу + 500 за склад.
@ -76,7 +66,7 @@ order: 99
## Импорт и миграция ## Импорт и миграция
### Можно перенести данные из другой системы учёта? ### Можно перенести данные из другой системы учёта?
Да. Поддерживаем импорт через Excel/CSV-файл, через стандартный REST API и через выгрузку 1С. Подробнее — на странице [/import](/import). Да. Поддерживаем импорт из файла Excel/CSV, выгрузку 1С (сохранённую в CSV) и перенос из МойСклад по токену. Подробнее — на странице [/import](/import).
### Какой объём каталога переносится? ### Какой объём каталога переносится?
До 100 000 товаров за один импорт. Каталог 10 000 позиций обычно переносится за 5–10 минут. До 100 000 товаров за один импорт. Каталог 10 000 позиций обычно переносится за 5–10 минут.
@ -98,5 +88,4 @@ order: 99
Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка с SLA — в тарифах Бизнес и Сеть. Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка с SLA — в тарифах Бизнес и Сеть.
### Есть ли обучение и внедрение? ### Есть ли обучение и внедрение?
Платные консультации 5 000 тг/час. Большинство клиентов справляются самостоятельно с помощью нашей базы знаний и видео-уроков. Большинство клиентов справляются самостоятельно с помощью базы знаний. Если нужна помощь с настройкой или импортом — напишите в поддержку, поможем.
```

View file

@ -4,16 +4,6 @@ category: Касса
order: 3 order: 3
--- ---
```markdown
---
title: Настройка кассы для Windows
slug: pos-setup
section: cash-register
order: 1
---
# Настройка кассы для Windows
Полная инструкция установки и настройки кассы ALL MARKET для Windows. Время — 15-20 минут. Полная инструкция установки и настройки кассы ALL MARKET для Windows. Время — 15-20 минут.
## Системные требования ## Системные требования
@ -28,7 +18,7 @@ order: 1
1. Войдите в админку ALL MARKET 1. Войдите в админку ALL MARKET
2. Откройте раздел «Касса» в боковом меню 2. Откройте раздел «Касса» в боковом меню
3. Нажмите «Скачать кассу для Windows» 3. Приложение кассы для Windows — скоро (кнопка появится в разделе «Касса»)
4. Сохраните файл `food-market-pos-setup.msi` (~80 МБ) 4. Сохраните файл `food-market-pos-setup.msi` (~80 МБ)
## Установка ## Установка
@ -82,7 +72,7 @@ order: 1
1. Нажмите «Открыть смену» 1. Нажмите «Открыть смену»
2. Введите начальный остаток наличных в кассе 2. Введите начальный остаток наличных в кассе
3. Найдите товар поиском или сканером 3. Найдите товар поиском или сканером
4. Нажмите «Оплата» → выберите тип оплаты (Наличные / Kaspi / Карта) 4. Нажмите «Оплата» → выберите тип оплаты (наличные или карта)
5. Введите сумму полученных денег → подтвердите 5. Введите сумму полученных денег → подтвердите
6. Чек распечатается, сделка пройдёт в учёт 6. Чек распечатается, сделка пройдёт в учёт
@ -107,5 +97,3 @@ order: 1
## Поддержка ## Поддержка
support@food-market.kz — отвечаем в течение часа в рабочее время. support@food-market.kz — отвечаем в течение часа в рабочее время.
```

View file

@ -4,16 +4,6 @@ category: Начало работы
order: 1 order: 1
--- ---
```markdown
---
title: Быстрый старт за 15 минут
slug: quickstart
section: getting-started
order: 1
---
# Быстрый старт за 15 минут
После регистрации в ALL MARKET вам нужно сделать 4 простых шага, чтобы начать продавать. После регистрации в ALL MARKET вам нужно сделать 4 простых шага, чтобы начать продавать.
## Шаг 1. Заполните данные магазина (1 мин) ## Шаг 1. Заполните данные магазина (1 мин)
@ -36,7 +26,7 @@ order: 1
## Шаг 3. Установите кассу (5 мин) ## Шаг 3. Установите кассу (5 мин)
«Касса» → «Скачать установщик». Запустите .msi файл, нажмите «Далее» три раза. После установки введите ключ организации (показан в разделе «Касса»). Приложение кассы для Windows — скоро (следите за разделом «Касса» в админке). После установки нужно будет ввести ключ организации из раздела «Касса».
Подключите оборудование: Подключите оборудование:
- Электронные торговые весы — через RS-232 или USB-COM - Электронные торговые весы — через RS-232 или USB-COM
@ -51,8 +41,5 @@ order: 1
## Что дальше? ## Что дальше?
- Добавить остальных кассиров и сотрудников: [Управление пользователями](/kb/users) - Добавить остальных кассиров и сотрудников — в админке «Настройки» → «Сотрудники»
- Настроить скидки и программу лояльности: [Лояльность и скидки](/kb/loyalty)
- Перенести каталог из старой системы: [Импорт каталога](/import) - Перенести каталог из старой системы: [Импорт каталога](/import)
```

View file

@ -57,11 +57,9 @@ const ymId = import.meta.env.PUBLIC_YM_ID ?? 'REPLACE_ME'
offers: { '@type': 'Offer', price: '5000', priceCurrency: 'KZT' }, offers: { '@type': 'Offer', price: '5000', priceCurrency: 'KZT' },
})} /> })} />
{/* Sprint 20: analytics placeholders. См. docs/analytics.md. {/* Аналитика подключается ТОЛЬКО при реальных env-ID (PUBLIC_GA_ID / PUBLIC_YM_ID).
Скрипты подключаются только при заданных env-vars; иначе Без них НИЧЕГО не выводим — никаких заглушек data-id="REPLACE_ME" в HTML. */}
в HTML остаётся data-id="REPLACE_ME" — служит маркером {gaId !== 'REPLACE_ME' && (
«не настроено» при ручной проверке view-source. */}
{gaId !== 'REPLACE_ME' ? (
<> <>
<script async src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}></script> <script async src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}></script>
<script set:html={` <script set:html={`
@ -71,11 +69,9 @@ const ymId = import.meta.env.PUBLIC_YM_ID ?? 'REPLACE_ME'
gtag('config', '${gaId}', { anonymize_ip: true }); gtag('config', '${gaId}', { anonymize_ip: true });
`} /> `} />
</> </>
) : (
<script data-analytics="google" data-id="REPLACE_ME" data-doc="docs/analytics.md" />
)} )}
{ymId !== 'REPLACE_ME' ? ( {ymId !== 'REPLACE_ME' && (
<script set:html={` <script set:html={`
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date(); m[i].l=1*new Date();
@ -84,8 +80,6 @@ const ymId = import.meta.env.PUBLIC_YM_ID ?? 'REPLACE_ME'
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(${ymId}, "init", {clickmap:true,trackLinks:true,accurateTrackBounce:true}); ym(${ymId}, "init", {clickmap:true,trackLinks:true,accurateTrackBounce:true});
`} /> `} />
) : (
<script data-analytics="yandex-metrika" data-id="REPLACE_ME" data-doc="docs/analytics.md" />
)} )}
</head> </head>
<body class="min-h-screen flex flex-col"> <body class="min-h-screen flex flex-col">

View file

@ -0,0 +1,35 @@
// Единый расчёт цены тарифа «Бизнес» — ОДИН источник правды для конструктора
// (BusinessTariffBuilder), формы регистрации (SignupForm) и страницы /pricing.
// База включает первый магазин/кассу/склад; далее — надбавка за каждый сверх первого.
export const PRICING = {
base: 10000,
perShop: 2000,
perCash: 500,
perWarehouse: 500,
// границы конфигуратора «Бизнес»
shops: { min: 1, max: 3 },
cashes: { min: 1, max: 5 },
warehouses: { min: 1, max: 5 },
employees: { min: 5, max: 10 },
startPrice: 5000,
} as const
export interface BusinessConfig { shops: number; cashes: number; warehouses: number; employees?: number }
export function businessPrice(c: BusinessConfig): number {
const s = clamp(c.shops, PRICING.shops.min, PRICING.shops.max)
const k = clamp(c.cashes, PRICING.cashes.min, PRICING.cashes.max)
const w = clamp(c.warehouses, PRICING.warehouses.min, PRICING.warehouses.max)
return PRICING.base
+ Math.max(0, s - 1) * PRICING.perShop
+ Math.max(0, k - 1) * PRICING.perCash
+ Math.max(0, w - 1) * PRICING.perWarehouse
}
export function clamp(n: number, min: number, max: number): number {
if (!Number.isFinite(n)) return min
return Math.min(max, Math.max(min, Math.round(n)))
}
const KZT = new Intl.NumberFormat('ru')
export const fmtKzt = (n: number): string => `${KZT.format(n)} ₸`

View file

@ -35,7 +35,7 @@ const faq = [
--- ---
<BaseLayout title="Импорт каталога" description="Перенесите товары, контрагентов, остатки и штрихкоды из вашей системы за минуты. CSV, 1С, МойСклад." ogImage="/og/import.png"> <BaseLayout title="Импорт каталога" description="Перенесите товары, контрагентов, остатки и штрихкоды из вашей системы за минуты. CSV, 1С, МойСклад." ogImage="/og/import.png">
<section class="relative overflow-hidden"> <section class="relative overflow-hidden">
<img src="/photos/import-hero.jpg" alt="" class="absolute inset-0 w-full h-full object-cover" /> <img src="/screenshots/catalog.webp" srcset="/screenshots/catalog.webp 1440w, /screenshots/catalog@2x.webp 2880w" sizes="100vw" width="1440" height="900" alt="Каталог товаров в ALL MARKET" class="absolute inset-0 w-full h-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-r from-slate-900/85 via-slate-900/60 to-slate-900/30"></div> <div class="absolute inset-0 bg-gradient-to-r from-slate-900/85 via-slate-900/60 to-slate-900/30"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 py-20 lg:py-28"> <div class="relative max-w-7xl mx-auto px-4 sm:px-6 py-20 lg:py-28">
<div class="max-w-2xl"> <div class="max-w-2xl">

View file

@ -86,7 +86,7 @@ const modules = [
sizes="(max-width: 1024px) 100vw, 1100px" sizes="(max-width: 1024px) 100vw, 1100px"
width="1440" height="900" width="1440" height="900"
alt="Каталог товаров ALL MARKET: список товаров с группами, штрихкодами, себестоимостью и розничной ценой" alt="Каталог товаров ALL MARKET: список товаров с группами, штрихкодами, себестоимостью и розничной ценой"
class="w-full block rounded-b-md" loading="lazy" /> class="w-full block rounded-b-md" width="1440" height="900" loading="lazy" />
</div> </div>
</section> </section>
@ -111,7 +111,7 @@ const modules = [
sizes="(max-width: 1024px) 100vw, 600px" sizes="(max-width: 1024px) 100vw, 600px"
width="1920" height="1080" width="1920" height="1080"
alt="Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши" alt="Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши"
class="w-full block rounded-md" loading="lazy" /> class="w-full block rounded-md" width="1440" height="810" loading="lazy" />
</figure> </figure>
</div> </div>
</section> </section>

View file

@ -54,7 +54,7 @@ const steps = [
sizes="(max-width: 1024px) 100vw, 600px" sizes="(max-width: 1024px) 100vw, 600px"
width="1920" height="1080" width="1920" height="1080"
alt="Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши" alt="Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши"
class="w-full block rounded-md" loading="lazy" /> class="w-full block rounded-md" width="1440" height="810" loading="lazy" />
<figcaption class="text-center text-sm text-slate-500 mt-3">Экран продажи</figcaption> <figcaption class="text-center text-sm text-slate-500 mt-3">Экран продажи</figcaption>
</figure> </figure>
<figure class="rounded-xl border border-slate-200 bg-slate-50 p-3 shadow-lg"> <figure class="rounded-xl border border-slate-200 bg-slate-50 p-3 shadow-lg">
@ -64,7 +64,7 @@ const steps = [
sizes="(max-width: 1024px) 100vw, 600px" sizes="(max-width: 1024px) 100vw, 600px"
width="1920" height="1080" width="1920" height="1080"
alt="Окно оплаты кассы ALL MARKET: выбор способа оплаты — наличные, карта, смешанная — и расчёт сдачи" alt="Окно оплаты кассы ALL MARKET: выбор способа оплаты — наличные, карта, смешанная — и расчёт сдачи"
class="w-full block rounded-md" loading="lazy" /> class="w-full block rounded-md" width="1440" height="810" loading="lazy" />
<figcaption class="text-center text-sm text-slate-500 mt-3">Оплата и расчёт сдачи</figcaption> <figcaption class="text-center text-sm text-slate-500 mt-3">Оплата и расчёт сдачи</figcaption>
</figure> </figure>
</div> </div>