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
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:
parent
5cb2c5934c
commit
b8e42e216c
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 |
|
|
@ -1,3 +1,3 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://test.food-market.kz/sitemap-index.xml
|
||||
Sitemap: https://test.food-market.kz/sitemap.xml
|
||||
|
|
|
|||
|
|
@ -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 pages = [
|
||||
{ file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами, импорт каталога, ОФД РК. Бесплатно 90 дней.' },
|
||||
{ file: 'home.png', title: 'Программа учёта и касса для розничных магазинов в Казахстане', sub: 'Касса с торговыми весами и импортом каталога. Бесплатно 90 дней.' },
|
||||
{ file: 'pos.png', title: 'Касса для Windows с поддержкой торговых весов', sub: 'Установка за 5 минут. Офлайн-режим. Стандартное оборудование.' },
|
||||
{ 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: '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-cafe.png', title: 'ALL MARKET для кафе и общепита', sub: 'Модификаторы блюд, столики, печать на кухню.' },
|
||||
{ file: 'for-alcohol.png', title: 'ALL MARKET для алкомаркета', sub: 'Акцизные марки, контроль времени продаж, маркировка.' },
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { PRICING, businessPrice, fmtKzt } from '@/lib/pricing'
|
||||
|
||||
const BASE = 10000
|
||||
const PER_SHOP = 2000
|
||||
const PER_CASH = 500
|
||||
const PER_WAREHOUSE = 500
|
||||
const BASE = PRICING.base
|
||||
const PER_SHOP = PRICING.perShop
|
||||
const PER_CASH = PRICING.perCash
|
||||
const PER_WAREHOUSE = PRICING.perWarehouse
|
||||
|
||||
interface SliderProps {
|
||||
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 [employees, setEmployees] = useState(5)
|
||||
|
||||
const total = useMemo(() => {
|
||||
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 total = useMemo(() => businessPrice({ shops, cashes, warehouses }), [shops, cashes, warehouses])
|
||||
|
||||
const fmt = new Intl.NumberFormat('ru')
|
||||
const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}`
|
||||
|
|
|
|||
|
|
@ -12,33 +12,32 @@ import Logo from './Logo.astro'
|
|||
<div>
|
||||
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Продукт</h3>
|
||||
<ul class="space-y-1.5">
|
||||
<li><a href="/features" class="text-slate-700 hover:text-brand">Возможности</a></li>
|
||||
<li><a href="/pos" class="text-slate-700 hover:text-brand">Касса</a></li>
|
||||
<li><a href="/import" class="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="/integrations" class="text-slate-700 hover:text-brand">Перенос и интеграции</a></li>
|
||||
<li><a href="/pricing" class="text-slate-700 hover:text-brand">Тарифы</a></li>
|
||||
<li><a href="/changelog" 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="block py-2 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="block py-2 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="block py-2 text-slate-700 hover:text-brand">Тарифы</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Ресурсы</h3>
|
||||
<ul class="space-y-1.5">
|
||||
<li><a href="/kb" class="text-slate-700 hover:text-brand">База знаний</a></li>
|
||||
<li><a href="/blog" class="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="/changelog" class="text-slate-700 hover:text-brand">Changelog</a></li>
|
||||
<li><a href="/kb" class="block py-2 text-slate-700 hover:text-brand">База знаний</a></li>
|
||||
<li><a href="/blog" class="block py-2 text-slate-700 hover:text-brand">Блог</a></li>
|
||||
<li><a href="/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>
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold uppercase text-slate-500 mb-2">Компания</h3>
|
||||
<ul class="space-y-1.5">
|
||||
<li><a href="/about" class="text-slate-700 hover:text-brand">О нас</a></li>
|
||||
<li><a href="/contacts" class="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/privacy" class="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/requisites" 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="block py-2 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="block py-2 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="block py-2 text-slate-700 hover:text-brand">Реквизиты</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,81 @@
|
|||
---
|
||||
import Logo from './Logo.astro'
|
||||
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">
|
||||
<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 lg:flex items-center gap-1 text-sm">
|
||||
<a href="/features" class="px-3 py-2 hover:text-brand transition">Возможности</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.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">
|
||||
<a href={`${APP_URL}/login`} class="px-3 py-1.5 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={`${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="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>
|
||||
|
||||
<!-- Мобильная панель: все пункты десктоп-меню + входы. Зоны нажатия ≥ 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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,34 @@
|
|||
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'
|
||||
|
||||
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 API_URL = APP_URL
|
||||
|
|
@ -15,11 +43,16 @@ interface Props {
|
|||
type FieldErrors = Partial<Record<'email' | 'password' | 'orgName' | 'phone' | 'agree', string>>
|
||||
|
||||
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 [password, setPassword] = useState('')
|
||||
const [orgName, setOrgName] = 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 [busy, setBusy] = useState(false)
|
||||
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`, {
|
||||
method: 'POST',
|
||||
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) {
|
||||
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="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ id: 'start', name: 'Старт', price: '5 000 ₸' },
|
||||
{ id: 'business', name: 'Бизнес', price: 'от 10 000 ₸' },
|
||||
{ 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'}`}>
|
||||
|
|
@ -165,6 +206,21 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
|
|||
</label>
|
||||
))}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@ cover_image: /photos/blog-scales.jpg
|
|||
- **Дешевле в эксплуатации** — не платите за сторонние модули интеграции
|
||||
- **Проще обучение кассиров** — они работают как привыкли, никакой «сложной системы»
|
||||
|
||||
[Скачать кассу →](/pos)
|
||||
[Подробнее о кассе →](/pos)
|
||||
|
|
|
|||
|
|
@ -35,10 +35,9 @@ cover_image: /photos/blog-launch.jpg
|
|||
Сейчас мы открываем регистрацию и принимаем первых клиентов. Будем активно собирать обратную связь и улучшать продукт каждую неделю.
|
||||
|
||||
В ближайших планах:
|
||||
- Интеграция с Kaspi Magazin (Q3 2026)
|
||||
- Интеграция с Kaspi Magazin
|
||||
- Marketplace интеграции (Wildberries, Ozon — Q4 2026)
|
||||
- Мобильное приложение для руководителя
|
||||
- Готовая архитектура для будущей маркировки в РК
|
||||
|
||||
[Зарегистрироваться →](/signup)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ cover_image: /photos/blog-quickstart.jpg
|
|||
- Email — это будет ваш логин
|
||||
- Пароль (минимум 8 символов)
|
||||
- Название магазина
|
||||
- Выберите тариф (можно оставить «Триал» — 90 дней бесплатно)
|
||||
- Выберите тариф (Старт, Бизнес или Сеть) — первые 90 дней бесплатно
|
||||
|
||||
Через 30 секунд после нажатия «Зарегистрироваться» вы окажетесь в админке ALL MARKET с готовым к работе пустым магазином.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ cover_image: /photos/blog-quickstart.jpg
|
|||
|
||||
## Шаг 3. Установите кассу (5 минут)
|
||||
|
||||
В админке: «Касса» → «Скачать установщик».
|
||||
Приложение кассы для Windows — скоро (появится в разделе «Касса»).
|
||||
|
||||
1. Запустите .msi файл — нажмите «Далее» три раза
|
||||
2. Введите ключ организации (показан в админке)
|
||||
|
|
@ -57,7 +57,7 @@ cover_image: /photos/blog-quickstart.jpg
|
|||
В кассе:
|
||||
1. «Открыть смену» — введите начальный остаток наличных
|
||||
2. Сосканируйте штрихкод любого товара — он появится в чеке
|
||||
3. «Оплата» → выберите тип (Наличные / Kaspi / Карта)
|
||||
3. «Оплата» → выберите тип (наличные или карта)
|
||||
4. Введите сумму полученных денег
|
||||
5. Подтвердите — чек распечатается
|
||||
|
||||
|
|
@ -68,7 +68,6 @@ cover_image: /photos/blog-quickstart.jpg
|
|||
После первой продажи:
|
||||
- Добавьте остальных кассиров — «Сотрудники» → «Добавить»
|
||||
- Настройте программу лояльности — «Лояльность» → «Создать программу»
|
||||
- Подключите Kaspi Pay для приёма безнала — «Интеграции» → «Kaspi»
|
||||
- Запустите акции и скидки — «Маркетинг» → «Акции»
|
||||
|
||||
Полная документация в нашей [Базе знаний](/kb).
|
||||
|
|
|
|||
|
|
@ -4,16 +4,6 @@ category: Биллинг
|
|||
order: 4
|
||||
---
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Тарифы, оплата, изменение тарифа
|
||||
slug: billing
|
||||
section: account
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Тарифы, оплата, изменение тарифа
|
||||
|
||||
Полная информация о подписках ALL MARKET и порядке оплаты.
|
||||
|
||||
## Тарифы
|
||||
|
|
@ -21,8 +11,9 @@ order: 1
|
|||
См. полную таблицу на [странице тарифов](/pricing).
|
||||
|
||||
Кратко:
|
||||
- **Триал** — 90 дней, бесплатно, без карты, все возможности
|
||||
- **Старт** — 5 000 тг/мес, 1 магазин, 1 касса, 1 склад
|
||||
Все тарифы — первые 90 дней бесплатно, без банковской карты.
|
||||
|
||||
- **Старт** — 5 000 тг/мес, 1 магазин, 1 касса, 1 склад, до 2 сотрудников
|
||||
- **Бизнес** — от 10 000 тг/мес, конструктор (до 3 магазинов / 5 касс / 5 складов)
|
||||
- **Сеть** — по запросу, без ограничений
|
||||
|
||||
|
|
@ -30,11 +21,7 @@ order: 1
|
|||
|
||||
### Для физических лиц и ИП
|
||||
|
||||
- Kaspi Pay (карта или счёт)
|
||||
- Halyk Bank, Jusan, Forte и другие казахстанские банки
|
||||
- Visa / Mastercard
|
||||
|
||||
Оплата проходит мгновенно. Доступ продлевается на оплаченный период.
|
||||
Способ оплаты подписки уточняется — свяжитесь с нами: support@food-market.kz. Приём оплат картой онлайн — в разработке.
|
||||
|
||||
### Для юридических лиц
|
||||
|
||||
|
|
@ -43,15 +30,6 @@ order: 1
|
|||
- Срок оплаты — 5 рабочих дней с даты выставления счёта
|
||||
- Доступ продлевается после поступления средств на расчётный счёт
|
||||
|
||||
## Скидки за длительные периоды
|
||||
|
||||
При оплате авансом:
|
||||
- 3 месяца: −5%
|
||||
- 6 месяцев: −10%
|
||||
- 12 месяцев: −20%
|
||||
|
||||
Скидки применяются автоматически при выборе периода в форме оплаты.
|
||||
|
||||
## Изменение тарифа
|
||||
|
||||
В любой момент через «Настройки» → «Подписка».
|
||||
|
|
@ -78,17 +56,14 @@ order: 1
|
|||
- Импорт и экспорт работают
|
||||
|
||||
Через 30 дней без оплаты:
|
||||
- Учётная запись помечается на удаление
|
||||
- Email с финальным предупреждением
|
||||
- Через 60 дней без оплаты — данные удаляются
|
||||
- Контент сохраняется 30 календарных дней (см. оферту, п. 5.3/11.3), после чего может быть удалён
|
||||
|
||||
## Возврат средств
|
||||
|
||||
Возврат за неиспользованный период возможен:
|
||||
- В течение первых 14 дней с момента оплаты — без обоснования
|
||||
- После 14 дней — пропорционально неиспользованному периоду
|
||||
Возврат денежных средств за неиспользованный период осуществляется в случаях, предусмотренных законодательством РК, по письменному заявлению (см. оферту, п. 6.5).
|
||||
|
||||
Заявка на возврат: support@food-market.kz с указанием причины и реквизитов для возврата.
|
||||
Заявка: support@food-market.kz с указанием причины и реквизитов для возврата.
|
||||
|
||||
## История платежей и счёта
|
||||
|
||||
|
|
@ -106,5 +81,3 @@ order: 1
|
|||
- Выделенный менеджер
|
||||
|
||||
Заявка: sales@food-market.kz
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,6 @@ category: Общее
|
|||
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 за склад.
|
||||
|
|
@ -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 минут.
|
||||
|
|
@ -98,5 +88,4 @@ order: 99
|
|||
Базовая поддержка бесплатна для всех тарифов. Приоритетная поддержка с SLA — в тарифах Бизнес и Сеть.
|
||||
|
||||
### Есть ли обучение и внедрение?
|
||||
Платные консультации 5 000 тг/час. Большинство клиентов справляются самостоятельно с помощью нашей базы знаний и видео-уроков.
|
||||
```
|
||||
Большинство клиентов справляются самостоятельно с помощью базы знаний. Если нужна помощь с настройкой или импортом — напишите в поддержку, поможем.
|
||||
|
|
|
|||
|
|
@ -4,16 +4,6 @@ category: Касса
|
|||
order: 3
|
||||
---
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Настройка кассы для Windows
|
||||
slug: pos-setup
|
||||
section: cash-register
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Настройка кассы для Windows
|
||||
|
||||
Полная инструкция установки и настройки кассы ALL MARKET для Windows. Время — 15-20 минут.
|
||||
|
||||
## Системные требования
|
||||
|
|
@ -28,7 +18,7 @@ order: 1
|
|||
|
||||
1. Войдите в админку ALL MARKET
|
||||
2. Откройте раздел «Касса» в боковом меню
|
||||
3. Нажмите «Скачать кассу для Windows»
|
||||
3. Приложение кассы для Windows — скоро (кнопка появится в разделе «Касса»)
|
||||
4. Сохраните файл `food-market-pos-setup.msi` (~80 МБ)
|
||||
|
||||
## Установка
|
||||
|
|
@ -82,7 +72,7 @@ order: 1
|
|||
1. Нажмите «Открыть смену»
|
||||
2. Введите начальный остаток наличных в кассе
|
||||
3. Найдите товар поиском или сканером
|
||||
4. Нажмите «Оплата» → выберите тип оплаты (Наличные / Kaspi / Карта)
|
||||
4. Нажмите «Оплата» → выберите тип оплаты (наличные или карта)
|
||||
5. Введите сумму полученных денег → подтвердите
|
||||
6. Чек распечатается, сделка пройдёт в учёт
|
||||
|
||||
|
|
@ -107,5 +97,3 @@ order: 1
|
|||
## Поддержка
|
||||
|
||||
support@food-market.kz — отвечаем в течение часа в рабочее время.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,6 @@ category: Начало работы
|
|||
order: 1
|
||||
---
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Быстрый старт за 15 минут
|
||||
slug: quickstart
|
||||
section: getting-started
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Быстрый старт за 15 минут
|
||||
|
||||
После регистрации в ALL MARKET вам нужно сделать 4 простых шага, чтобы начать продавать.
|
||||
|
||||
## Шаг 1. Заполните данные магазина (1 мин)
|
||||
|
|
@ -36,7 +26,7 @@ order: 1
|
|||
|
||||
## Шаг 3. Установите кассу (5 мин)
|
||||
|
||||
«Касса» → «Скачать установщик». Запустите .msi файл, нажмите «Далее» три раза. После установки введите ключ организации (показан в разделе «Касса»).
|
||||
Приложение кассы для Windows — скоро (следите за разделом «Касса» в админке). После установки нужно будет ввести ключ организации из раздела «Касса».
|
||||
|
||||
Подключите оборудование:
|
||||
- Электронные торговые весы — через RS-232 или USB-COM
|
||||
|
|
@ -51,8 +41,5 @@ order: 1
|
|||
|
||||
## Что дальше?
|
||||
|
||||
- Добавить остальных кассиров и сотрудников: [Управление пользователями](/kb/users)
|
||||
- Настроить скидки и программу лояльности: [Лояльность и скидки](/kb/loyalty)
|
||||
- Добавить остальных кассиров и сотрудников — в админке «Настройки» → «Сотрудники»
|
||||
- Перенести каталог из старой системы: [Импорт каталога](/import)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -57,11 +57,9 @@ const ymId = import.meta.env.PUBLIC_YM_ID ?? 'REPLACE_ME'
|
|||
offers: { '@type': 'Offer', price: '5000', priceCurrency: 'KZT' },
|
||||
})} />
|
||||
|
||||
{/* Sprint 20: analytics placeholders. См. docs/analytics.md.
|
||||
Скрипты подключаются только при заданных env-vars; иначе
|
||||
в HTML остаётся data-id="REPLACE_ME" — служит маркером
|
||||
«не настроено» при ручной проверке view-source. */}
|
||||
{gaId !== 'REPLACE_ME' ? (
|
||||
{/* Аналитика подключается ТОЛЬКО при реальных env-ID (PUBLIC_GA_ID / PUBLIC_YM_ID).
|
||||
Без них НИЧЕГО не выводим — никаких заглушек data-id="REPLACE_ME" в HTML. */}
|
||||
{gaId !== 'REPLACE_ME' && (
|
||||
<>
|
||||
<script async src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}></script>
|
||||
<script set:html={`
|
||||
|
|
@ -71,11 +69,9 @@ const ymId = import.meta.env.PUBLIC_YM_ID ?? 'REPLACE_ME'
|
|||
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={`
|
||||
(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();
|
||||
|
|
@ -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");
|
||||
ym(${ymId}, "init", {clickmap:true,trackLinks:true,accurateTrackBounce:true});
|
||||
`} />
|
||||
) : (
|
||||
<script data-analytics="yandex-metrika" data-id="REPLACE_ME" data-doc="docs/analytics.md" />
|
||||
)}
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col">
|
||||
|
|
|
|||
35
src/food-market.public/src/lib/pricing.ts
Normal file
35
src/food-market.public/src/lib/pricing.ts
Normal 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)} ₸`
|
||||
|
|
@ -35,7 +35,7 @@ const faq = [
|
|||
---
|
||||
<BaseLayout title="Импорт каталога" description="Перенесите товары, контрагентов, остатки и штрихкоды из вашей системы за минуты. CSV, 1С, МойСклад." ogImage="/og/import.png">
|
||||
<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="relative max-w-7xl mx-auto px-4 sm:px-6 py-20 lg:py-28">
|
||||
<div class="max-w-2xl">
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const modules = [
|
|||
sizes="(max-width: 1024px) 100vw, 1100px"
|
||||
width="1440" height="900"
|
||||
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>
|
||||
</section>
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ const modules = [
|
|||
sizes="(max-width: 1024px) 100vw, 600px"
|
||||
width="1920" height="1080"
|
||||
alt="Окно продажи кассы ALL MARKET: чек с товарами, избранные позиции, итоговая сумма и горячие клавиши"
|
||||
class="w-full block rounded-md" loading="lazy" />
|
||||
class="w-full block rounded-md" width="1440" height="810" loading="lazy" />
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const steps = [
|
|||
sizes="(max-width: 1024px) 100vw, 600px"
|
||||
width="1920" height="1080"
|
||||
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>
|
||||
</figure>
|
||||
<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"
|
||||
width="1920" height="1080"
|
||||
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>
|
||||
</figure>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue