chore: initial scaffold (bot, miniapp, backend, docs)

This commit is contained in:
wasrusgen 2026-05-08 23:56:48 +03:00
commit 0c5ed48303
15 changed files with 1593 additions and 0 deletions

64
.gitignore vendored Normal file
View File

@ -0,0 +1,64 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
build/
dist/
*.egg-info/
.eggs/
# Virtualenv
.venv/
venv/
env/
ENV/
# Environment variables (содержат секреты — токен бота, API ключи)
.env
.env.local
.env.*.local
*.env
!.env.example
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
desktop.ini
ehthumbs.db
# Logs
*.log
logs/
# Local development data
data/
*.sqlite
*.sqlite3
*.db
# Google Sheets credentials (никогда не коммитим!)
credentials.json
service-account-*.json
*-credentials.json
# Тесты и временные файлы
.pytest_cache/
.coverage
htmlcov/
.tmp/
*.tmp
# Node (для miniapp в будущем, если будет сборка)
node_modules/
package-lock.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# ZOV Tech — AI-подбор кухонной техники
Telegram-бот + MiniApp для подбора техники под кухню фабрики ЗОВ.
Менеджер заполняет с клиентом чек-лист → нейросеть собирает предложение → менеджер получает результат за минуту.
## Структура
```
zov-tech/
├── bot/ — Telegram-бот (Python + aiogram)
├── miniapp/ — MiniApp (HTML + JS, хост на GitHub Pages)
├── backend/ — Google Apps Script (бэкенд + работа с Sheets)
├── docs/ — Документация (ТЗ, deployment, decisions)
└── .claude/ — настройки Claude Code (вне репо)
```
## Стек
| Слой | Технология |
|---|---|
| Бот | Python 3.10+, aiogram 3.x |
| AI | Anthropic Claude (Haiku 4.5) |
| MiniApp | Vanilla JS + HTML, без сборки |
| Backend | Google Apps Script (Web App) |
| БД | Google Sheets (на старте), PostgreSQL (после роста) |
| Хостинг бота | VPS (Selectel / Timeweb) |
| Хостинг MiniApp | GitHub Pages |
## Быстрый старт (когда будет код)
```bash
cd bot
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
copy .env.example .env # заполнить токены
python main.py
```
## Документация
- [Техническое задание](docs/ТЗ_ЗОВот_MiniApp_v1.md) — полное ТЗ продукта.
## Контакты
Куратор / заказчик: Василий ([@wasrusgen](https://t.me/wasrusgen))
Канал: [@wasrusgen1](https://t.me/wasrusgen1)

72
backend/Code.gs Normal file
View File

@ -0,0 +1,72 @@
/**
* ЗОВ — Backend (Google Apps Script Web App)
*
* Деплой: Deploy → New deployment → Type: Web app
* Execute as: Me
* Who has access: Anyone (только так MiniApp сможет POST'ить)
*
* Script Properties (Project Settings → Script Properties):
* - BOT_TOKEN
* - ANTHROPIC_API_KEY
* - ADMIN_TG_ID
* - SHEET_ID
*/
function doPost(e) {
try {
const path = (e.parameter && e.parameter.path) || "";
const body = e.postData && e.postData.contents
? JSON.parse(e.postData.contents)
: {};
let result;
switch (path) {
case "me":
result = handleMe(body);
break;
case "measurement":
result = handleMeasurement(body);
break;
case "podbor":
result = handlePodbor(body);
break;
default:
return jsonResponse({ error: "unknown_path", path }, 404);
}
return jsonResponse(result);
} catch (err) {
return jsonResponse({ error: String(err) }, 500);
}
}
function jsonResponse(obj, _status) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
// =============== handlers ===============
function handleMe(body) {
// TODO:
// 1. Проверить hash в body.initData по BOT_TOKEN.
// 2. Извлечь tg_id из initData.
// 3. Найти пользователя в Sheet "Users" / "Managers" / "Clients".
// 4. Вернуть профиль + статус.
return { error: "not_implemented" };
}
function handleMeasurement(body) {
// TODO: сохранить замер в Sheet "Measurements", уведомить менеджера.
return { error: "not_implemented" };
}
function handlePodbor(body) {
// TODO:
// 1. Сохранить заявку в Sheet "Leads".
// 2. Собрать prompt из body.checklist + measurement.
// 3. Вызвать Claude API.
// 4. Записать ответ.
// 5. Отправить менеджеру через Telegram Bot API.
return { error: "not_implemented" };
}

37
backend/README.md Normal file
View File

@ -0,0 +1,37 @@
# Backend (Google Apps Script)
Этот код — зеркало того, что лежит в Apps Script-проекте, привязанном к Google Sheet «ЗОВ — База».
## Как синхронизировать
Вариант 1 (вручную): копировать содержимое `Code.gs` в редактор Apps Script.
Вариант 2 (через clasp): использовать [clasp](https://github.com/google/clasp).
```bash
npm install -g @google/clasp
clasp login
clasp clone <SCRIPT_ID>
# или для уже существующего:
clasp pull / clasp push
```
## Деплой
1. Открыть Apps Script проект, привязанный к Sheet.
2. Deploy → New deployment.
3. Type: **Web app**.
4. Execute as: **Me**.
5. Who has access: **Anyone** (только так MiniApp сможет POST'ить).
6. Скопировать выданный URL — это `BACKEND_URL` в `miniapp/assets/app.js`.
## Script Properties (секреты)
В Apps Script: ⚙️ Project Settings → Script Properties → Add property.
| Key | Value |
|---|---|
| `BOT_TOKEN` | токен @BotFather |
| `ANTHROPIC_API_KEY` | ключ Anthropic Console |
| `ADMIN_TG_ID` | tg_id куратора |
| `SHEET_ID` | ID Google Sheet (из URL) |

24
bot/.env.example Normal file
View File

@ -0,0 +1,24 @@
# Telegram
BOT_TOKEN=
ADMIN_TG_ID=
# Anthropic Claude (для AI-подбора)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-haiku-4-5-20251001
# Google Sheets (служебный аккаунт)
SHEET_ID=
GOOGLE_CREDENTIALS_PATH=./credentials.json
# MiniApp
MINIAPP_URL=https://example.github.io/zov-tech/
# Webhook (для прода). Для дева оставь пустым — будет polling
WEBHOOK_URL=
WEBHOOK_HOST=0.0.0.0
WEBHOOK_PORT=8080
WEBHOOK_PATH=/tg/webhook
# Параметры бизнес-логики (можно держать и в Sheets/Settings)
ACTIVE_PERIOD_DAYS=90
GRACE_PERIOD_DAYS=14

52
bot/config.py Normal file
View File

@ -0,0 +1,52 @@
from dataclasses import dataclass
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / ".env")
@dataclass(frozen=True)
class Config:
bot_token: str
admin_tg_id: int
anthropic_api_key: str
anthropic_model: str
sheet_id: str
google_credentials_path: str
miniapp_url: str
webhook_url: str
webhook_host: str
webhook_port: int
webhook_path: str
active_period_days: int
grace_period_days: int
@property
def use_webhook(self) -> bool:
return bool(self.webhook_url)
def _required(key: str) -> str:
val = os.getenv(key)
if not val:
raise RuntimeError(f"Не задана обязательная переменная окружения: {key}")
return val
def load_config() -> Config:
return Config(
bot_token=_required("BOT_TOKEN"),
admin_tg_id=int(_required("ADMIN_TG_ID")),
anthropic_api_key=_required("ANTHROPIC_API_KEY"),
anthropic_model=os.getenv("ANTHROPIC_MODEL", "claude-haiku-4-5-20251001"),
sheet_id=_required("SHEET_ID"),
google_credentials_path=os.getenv("GOOGLE_CREDENTIALS_PATH", "./credentials.json"),
miniapp_url=_required("MINIAPP_URL"),
webhook_url=os.getenv("WEBHOOK_URL", ""),
webhook_host=os.getenv("WEBHOOK_HOST", "0.0.0.0"),
webhook_port=int(os.getenv("WEBHOOK_PORT", "8080")),
webhook_path=os.getenv("WEBHOOK_PATH", "/tg/webhook"),
active_period_days=int(os.getenv("ACTIVE_PERIOD_DAYS", "90")),
grace_period_days=int(os.getenv("GRACE_PERIOD_DAYS", "14")),
)

0
bot/handlers/__init__.py Normal file
View File

62
bot/handlers/start.py Normal file
View File

@ -0,0 +1,62 @@
from aiogram import Router, F
from aiogram.filters import CommandStart
from aiogram.types import (
Message,
InlineKeyboardMarkup,
InlineKeyboardButton,
WebAppInfo,
CallbackQuery,
)
from config import Config
router = Router(name="start")
def role_choice_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="👤 Менеджер", callback_data="role:manager"),
InlineKeyboardButton(text="🏠 Клиент", callback_data="role:client"),
]
]
)
def open_app_kb(miniapp_url: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="🚀 Открыть кабинет",
web_app=WebAppInfo(url=miniapp_url),
)
]
]
)
@router.message(CommandStart())
async def cmd_start(message: Message, config: Config) -> None:
# TODO: проверить, есть ли пользователь в БД (Google Sheet → users).
# Если есть → сразу показывать "Открыть кабинет".
# Если нет → спрашивать роль.
await message.answer(
"👋 Здравствуйте! Я помогу с подбором кухонной техники.\n\nКто вы?",
reply_markup=role_choice_kb(),
)
@router.callback_query(F.data.startswith("role:"))
async def on_role_chosen(callback: CallbackQuery, config: Config) -> None:
role = callback.data.split(":", 1)[1]
# TODO: сохранить роль в БД (Google Sheet → users)
text = {
"manager": "Отлично, открываю кабинет менеджера 👇",
"client": "Спасибо! Открываю ваш кабинет 👇",
}.get(role, "Открываю кабинет 👇")
await callback.message.edit_text(text, reply_markup=open_app_kb(config.miniapp_url))
await callback.answer()

37
bot/main.py Normal file
View File

@ -0,0 +1,37 @@
import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from config import load_config
from handlers import start
async def main() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
config = load_config()
bot = Bot(
token=config.bot_token,
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
dp = Dispatcher()
dp["config"] = config
dp.include_router(start.router)
if config.use_webhook:
raise NotImplementedError("Webhook mode будет добавлен после MVP")
logging.info("Запуск в режиме polling")
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())

6
bot/requirements.txt Normal file
View File

@ -0,0 +1,6 @@
aiogram>=3.4.0,<4.0.0
anthropic>=0.40.0
python-dotenv>=1.0.0
gspread>=6.0.0
google-auth>=2.27.0
httpx>=0.27.0

0
bot/services/__init__.py Normal file
View File

View File

@ -0,0 +1,950 @@
# Техническое задание
# Telegram-бот + MiniApp «AI-подбор техники для кухни ЗОВ»
**Версия:** 1.0
**Дата:** 2026-05-08
**Заказчик:** Василий (vasrusgen@gmail.com), куратор партнёрской сети ЗОВ
**Канал:** [@wasrusgen1](https://t.me/wasrusgen1)
**Целевая аудитория продукта:** 112 менеджеров салонов ЗОВ + конечные клиенты, ведомые через канал
---
## 1. Контекст и цели
### 1.1 Что это за продукт
Telegram-бот выступает шлюзом авторизации, после чего открывается единый MiniApp с двумя ролями:
- **Кабинет менеджера** — рабочий инструмент: подбор техники для клиента, замеры, заявки, сделки, статус доступа.
- **Кабинет клиента** — воронка покупки кухни: замер, подбор, калькулятор, идеи, связь с менеджером.
Внутри MiniApp клиент или менеджер заполняют структурированный чек-лист. Данные уходят в backend, который вызывает LLM (OpenAI / Claude). LLM возвращает готовое предложение по технике с учётом брендов, ниш, бюджета и сценария использования. Результат доставляется обратно в Telegram.
### 1.2 Какую проблему решает
- Сокращает срок согласования кухни с клиентом (главная боль ЗОВ).
- Привязывает менеджера к куратору: «работаешь со мной — инструмент бесплатно, ушёл — платно».
- Превращает 112 менеджеров в управляемую партнёрскую сеть с метриками.
- Даёт клиентам бесплатную ценность до контакта с менеджером (калькулятор, идеи, самозамер).
### 1.3 Бизнес-модель в продукте
| Сегмент | Доступ | Условие |
|---|---|---|
| Менеджеры ЗОВ (свои салоны) | бесплатно навсегда | по умолчанию |
| Менеджеры партнёрских салонов, активные | бесплатно | сделка через куратора за последние 90 дней |
| Менеджеры партнёрских салонов, неактивные | платно | подписка / pay-per-use |
| Конечные клиенты | базовый доступ бесплатно | подбор + замер открыты, премиум-функции потом |
Точное определение «активного менеджера» — поле `last_order_date` в реестре + 90 дней.
### 1.4 Что в скоупе MVP
- Бот с одним диалогом (выбор роли + кнопка «Открыть кабинет»).
- MiniApp с двумя меню (менеджер / клиент), но **рабочих пунктов всего три**:
- 🔧 «Подбор техники» (для менеджера, на базе существующего HTML-чек-листа `02_Чек-лист_клиенту.html`).
- 📐 «Самозамер» (для клиента, новая 5-шаговая форма).
- 💰 «Мой статус» (для менеджера, простая страница).
- Backend на Google Apps Script + Google Sheet как БД.
- AI-подбор через OpenAI API (`gpt-4o-mini`) с готовым промптом.
- Доставка результата менеджеру через бота.
### 1.5 Что НЕ в скоупе MVP (на будущее)
- Платёжная интеграция (ЮKassa / Telegram Stars).
- Раздел «Сделки», «База знаний», «Обучение», «FAQ», «Записаться в салон».
- Личный кабинет клиента с историей.
- Push-уведомления об активности.
- Партнёрская комиссия с производителей техники.
- Мобильное нативное приложение (всегда останется MiniApp).
---
## 2. Архитектура
### 2.1 Компоненты
```
Telegram канал @wasrusgen1
│ кнопка/ссылка "Открыть подбор"
Telegram бот @zov_tech_bot
│ /start → выбор роли → кнопка WebApp
MiniApp (SPA, HTML+JS)
на хостинге GitHub Pages / Netlify (HTTPS обязателен)
│ POST /api/*
Backend Google Apps Script (Web App URL)
│ R/W
Google Sheet "ЗОВ — База"
│ при подборе:
OpenAI API (gpt-4o-mini)
│ результат
Backend → Telegram Bot API → менеджер/клиент
```
### 2.2 Технологический стек
| Слой | Технология | Обоснование |
|---|---|---|
| Бот | Python + aiogram **либо** n8n + Telegram Trigger | aiogram — гибче, n8n — без кода |
| MiniApp | HTML + Vanilla JS + минимальный CSS | существующий чек-лист уже в этом стеке |
| Хостинг MiniApp | GitHub Pages | бесплатно, HTTPS, Git-history |
| Backend | Google Apps Script (Web App) | бесплатно, нативная связка с Sheet |
| БД | Google Sheet | прозрачность для заказчика, ручное редактирование |
| AI | OpenAI API, модель `gpt-4o-mini` | дёшево (~$0.001 на запрос), хватает для подбора |
| Платежи (вне MVP) | ЮKassa или Telegram Stars | российские реалии |
### 2.3 Окружения
- **Dev:** локальный HTML, бот в режиме polling, тестовая Google Sheet.
- **Prod:** GitHub Pages, бот в режиме webhook, прод-Sheet с реестром.
---
## 3. Бот @zov_tech_bot
### 3.1 Общие принципы
- Бот **не содержит продуктовой логики**. Он только аутентифицирует и открывает MiniApp.
- Все длинные диалоги, формы, чек-листы — внутри MiniApp.
- Бот шлёт результаты подбора и системные уведомления.
### 3.2 Команды
| Команда | Доступ | Действие |
|---|---|---|
| `/start` | все | приветствие, выбор роли (если новый пользователь) или кнопка «Открыть кабинет» (если уже зарегистрирован) |
| `/start <invite_code>` | все | спец-сценарий: переход по ссылке-приглашению от менеджера |
| `/menu` | все | повторная кнопка «Открыть кабинет» |
| `/help` | все | краткая справка + контакт куратора |
| `/admin` | только админ (tg_id куратора) | админ-меню (см. 3.5) |
### 3.3 Дерево диалогов
```
[НОВЫЙ ПОЛЬЗОВАТЕЛЬ]
/start
"👋 Здравствуйте! Я помогу подобрать технику для кухни.
Кто вы?"
[👤 Менеджер] [🏠 Клиент]
│ │
▼ ▼
сохранить сохранить
role=manager role=client
в БД в БД
│ │
▼ ▼
"Отлично, открываю "Спасибо! Открываю
ваш кабинет 👇" кабинет 👇"
[🚀 Открыть кабинет] [🚀 Открыть кабинет]
(WebApp button с URL MiniApp)
[ВЕРНУВШИЙСЯ ПОЛЬЗОВАТЕЛЬ]
/start
"С возвращением, {имя}!"
[🚀 Открыть кабинет]
[ССЫЛКА-ПРИГЛАШЕНИЕ ОТ МЕНЕДЖЕРА]
/start client_inv_AB12CD
проверить invite_code в БД
│ найден → manager_id = X
сохранить role=client + manager_id=X
"Здравствуйте! Вас сопровождает {ФИО менеджера}, ЗОВ {салон}.
Открываю кабинет 👇"
[🚀 Открыть кабинет]
```
### 3.4 Кнопка «Открыть кабинет»
Это **WebApp-кнопка** Telegram (не URL-button), которая открывает MiniApp с подписанным `initData`.
Параметр `web_app.url`:
```
https://wasrusgen.github.io/zov-tech/?v=1
```
`initData` содержит `tg_id`, имя пользователя, hash для проверки. Backend проверяет hash и определяет роль по БД.
### 3.5 Админ-функции (для куратора)
Команда `/admin` доступна только при `tg_id == ADMIN_TG_ID` (env-переменная).
Меню:
- `📊 Статистика` — сколько активных менеджеров, заявок за неделю, конверсия
- `👤 Управление менеджерами`
- `+ Добавить менеджера` (диалог: ФИО, tg_username, salon)
- `✏️ Изменить статус` (выбор → active / lapsed + дата)
- `🔍 Найти менеджера` (по ФИО / tg_username)
- `📨 Рассылка` — отправить сообщение всем менеджерам / всем клиентам / выбранным сегментам
### 3.6 Системные уведомления (исходящие от бота)
| Триггер | Получатель | Текст |
|---|---|---|
| Клиент завершил подбор | менеджер этого клиента | «Новая заявка от {ФИО клиента}: {short_summary}. [Открыть в кабинете]» |
| Менеджер запросил подбор | менеджер | «Готово! Подбор для {ФИО клиента}: [PDF / текст]» |
| Клиент сделал самозамер | менеджер этого клиента | «{ФИО клиента} прислал замер кухни. [Посмотреть]» |
| Статус менеджера изменён куратором | менеджер | «Ваш доступ продлён до {дата}» / «Доступ переведён в платный режим» |
Все уведомления сопровождаются inline-кнопкой, открывающей соответствующий экран MiniApp.
### 3.7 Технические требования к боту
- Режим **webhook** в проде, polling в деве.
- Хранилище состояний — память (FSM aiogram) или Redis для масштаба. На MVP — память.
- Логирование всех `update` в файл / Sheet «Логи бота».
- Обработка ошибок: при exception — сообщение «Произошла ошибка, мы уже разбираемся» + alert куратору.
### 3.8 Регистрация бота в @BotFather
1. `/newbot``zov_tech_bot` (или альтернативное имя, если занято).
2. `/setdomain` → домен MiniApp (`wasrusgen.github.io`).
3. `/newapp` → создать MiniApp:
- Title: «Подбор техники ЗОВ»
- Short name: `podbor`
- Web App URL: `https://wasrusgen.github.io/zov-tech/`
4. `/setmenubutton` → опционально, кнопка-меню в боте, ведущая в MiniApp.
---
## 4. MiniApp
### 4.1 Карта страниц (hash-роутинг внутри одного SPA)
```
#/ — авто-редирект по роли
#/m — главное меню менеджера
#/m/podbor — чек-лист подбора (существующий HTML)
#/m/measurements — список замеров
#/m/measurements/new — форма замера (менеджер за клиента)
#/m/measurements/:id — карточка одного замера
#/m/leads — список заявок (просмотр, MVP — read-only)
#/m/status — мой статус и доступ
#/m/help — связь с куратором
#/c — главное меню клиента
#/c/measure — выбор: вызвать замерщика / замерить самому
#/c/measure/self — 5-шаговая форма самозамера
#/c/measure/visit — заявка на выезд замерщика
#/c/podbor — упрощённый чек-лист (вне MVP, заглушка)
#/c/contact — связь с менеджером
```
### 4.2 Авторизация и определение роли
```js
// Псевдокод старта MiniApp
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
const initData = tg.initData; // подписанная строка
const startParam = tg.initDataUnsafe.start_param || null;
const me = await fetch('/api/me', {
method: 'POST',
body: JSON.stringify({ initData, startParam })
}).then(r => r.json());
// me = { role: "manager"|"client", user: {...}, manager: {...}, status: "active"|"lapsed" }
if (me.role === 'manager') router.go('#/m');
else router.go('#/c');
```
Backend `/api/me`:
1. Проверяет HMAC `initData` по `BOT_TOKEN` (стандартная процедура Telegram).
2. Ищет `tg_id` в листе «Пользователи».
3. Если нет — создаёт запись (роль уже сохранена ботом, должна быть в БД).
4. Возвращает профиль + статус.
### 4.3 Кабинет менеджера
**Главный экран `#/m`:**
```
┌─────────────────────────────────────┐
│ Привет, {ФИО} │
│ Салон: {salon} │
│ Статус: 🟢 active до 12.08.2026 │
│ │
│ ─────────────────────────────── │
│ │
│ 🔧 Подбор техники для клиента → │
│ 📐 Замеры → │
│ 📋 Заявки клиентов → │
│ 💼 Сделки (скоро) · │
│ 📚 База знаний (скоро) · │
│ 💰 Мой статус → │
│ 🆘 Связь с куратором → │
└─────────────────────────────────────┘
```
Пункты «Сделки», «База знаний» в MVP — disabled-кнопки с текстом «Скоро».
**Страница `#/m/podbor`:**
Это существующий файл `02_Чек-лист_клиенту.html` с минимальными доработками:
1. Удалить захардкоженный блок менеджера (Любовь Алпеева) — подставлять из `me.user`.
2. Добавить поле «Размеры ниш» (см. п. 4.5 — пробелы из анализа).
3. Добавить поле «Бюджет по категориям».
4. Добавить блок «Сценарий использования» (семья, готовка).
5. Кнопку «Отправить» завести на `POST /api/podbor` вместо `mailto:`.
6. После отправки — `tg.showAlert('Заявка отправлена, результат придёт в чат')` + `tg.close()`.
**Страница `#/m/measurements`:**
Список замеров клиентов этого менеджера. Колонки: дата, ФИО клиента, статус (🟡 запрошен / 🟢 готов / 🔵 загружен вручную), кнопка «Открыть».
**Страница `#/m/measurements/new`:**
Та же форма, что и у клиента в `#/c/measure/self`, но с дополнительным полем «Клиент» (выбор из списка или ввод).
**Страница `#/m/status`:**
```
┌─────────────────────────────────────┐
│ Ваш статус │
│ │
│ 🟢 Active │
│ Доступ продлён до 12.08.2026 │
│ Осталось 96 дней │
│ │
│ Последняя сделка через куратора: │
│ «Кухня Петров, 14.05.2026» │
│ │
│ Заявок за всё время: 23 │
│ Сделок через подбор: 7 (30%) │
│ │
│ Чтобы продлить доступ — │
│ проведите следующую сделку │
│ через @wasrusgen. │
└─────────────────────────────────────┘
```
Если `status == lapsed`:
```
┌─────────────────────────────────────┐
│ ⚠️ Доступ ограничен │
│ │
│ Последняя сделка через куратора │
│ была 12.02.2026. │
│ Бесплатный доступ закончился. │
│ │
│ Варианты: │
│ • Возобновить сопровождение через │
@wasrusgen — доступ снова free. │
│ • Подписка 3 000 ₽/мес. │
│ • Pay-per-use 500 ₽/подбор. │
│ │
│ [Связаться с куратором] │
│ [Оформить подписку] (вне MVP) │
└─────────────────────────────────────┘
```
### 4.4 Кабинет клиента
**Главный экран `#/c`:**
```
┌─────────────────────────────────────┐
│ Привет, {имя} │
│ {если есть} │
│ Менеджер: {ФИО}, ЗОВ {салон} │
│ │
│ ─────────────────────────────── │
│ │
│ 📐 Замер кухни → │
│ 🔧 Подобрать технику (скоро) · │
│ 📐 Калькулятор бюджета (скоро) · │
│ 💡 Идеи и кейсы (скоро) · │
│ 📞 Связаться с менеджером → │
│ 📍 Записаться в салон (скоро) · │
└─────────────────────────────────────┘
```
В MVP активны: «Замер кухни», «Связаться с менеджером».
**Страница `#/c/measure`:**
Развилка:
```
Выберите способ замера:
[📞 Вызвать замерщика]
Бесплатно при заказе кухни
2 000 ₽ — отдельная услуга
[📏 Замерить самому]
5 шагов с инструкцией
Займёт ~15 минут
```
**Страница `#/c/measure/self`:**
5 экранов, каждый — отдельный шаг. Прогресс-бар сверху.
**Шаг 1: План кухни**
- Выбор формы (radio-картинки): прямая / Г-образная / П-образная / с островом / свой вариант
- Поле «Площадь, м²» (number)
- Поле «Высота потолка, мм» (number, default 2700)
**Шаг 2: Стены и габариты**
- Длина стены 1, мм
- Длина стены 2, мм (если Г-/П-образная)
- Длина стены 3, мм (если П-образная)
- Загрузка фото общего вида (опционально)
**Шаг 3: Окна и двери**
- Чекбоксы: «Есть окно» / «Есть дверь» / «Есть выход на балкон»
- Для каждого активного — поля: положение (выбор стены), ширина, высота, расстояние от пола
**Шаг 4: Коммуникации (важно для AI-подбора)**
- Тип подключения варочной: газ / электрика 220 / электрика 380
- Наличие вентиляционной шахты на кухне: да / нет / не знаю
- Расположение мойки: стена 1 / 2 / 3 / остров
- Свободное поле «Особенности коммуникаций»
**Шаг 5: Ниши под технику (если планируется встройка)**
Чекбокс «Планирую встроенную технику» → раскрывает блок:
- Холодильник: Ш × В × Г, мм
- Варочная: Ш × В × Г, мм
- Духовой шкаф: Ш × В × Г, мм
- Посудомойка: Ш × В × Г, мм
- Стиральная (если на кухне): Ш × В × Г, мм
- Микроволновка: Ш × В × Г, мм
- Кофемашина: Ш × В × Г, мм
Поля опциональные. Если не планируется встройка — блок свёрнут, шаг можно пропустить.
**Финал:**
- Кнопка «Сохранить замер».
- POST `/api/measurement` → сохранение + уведомление менеджеру (если manager_id привязан).
- `tg.showAlert('Замер сохранён')` + `tg.close()`.
### 4.5 Доработки существующего чек-листа подбора
Согласно анализу `02_Чек-лист_клиенту.html` нужно добавить:
1. **Блок «Параметры кухни»** в онбординг (или подгружать из последнего замера).
2. **Бюджет по категориям** — простая таблица в финальном экране.
3. **Блок «Семья и готовка»** — 45 быстрых вопросов с радиокнопками:
- Состав семьи: 1 взрослый / пара / семья с детьми / 2+ поколения
- Частота готовки: ежедневно / 34 раза в неделю / реже
- Любимые техники: выпечка / на пару / гриль / wok / низкотемпературное (multi-select)
- Приём гостей: часто / иногда / редко
4. **Чекбокс «уже есть, не меняю»** в каждой категории — клиент исключает позиции.
5. **Webhook вместо `mailto:`** — POST на backend.
6. **Обязательные поля**: имя, контакт, общий бюджет, адрес.
### 4.6 Дизайн-токены (брендинг ЗОВ)
Использовать существующие в HTML переменные:
```css
:root {
--bronze: #76BD22; /* фирменный зелёный */
--accent: #003E7E; /* фирменный синий */
--cream: #FAFAFA;
--sand: #F0F9E8;
}
```
Шрифт — системный sans-serif (как в текущем HTML). Скруглённые углы 8px, тени минимальные.
---
## 5. База данных (Google Sheets)
Файл: **«ЗОВ — База»** (один документ, несколько листов).
### 5.1 Лист «Users»
Все пользователи бота (менеджеры + клиенты).
| Колонка | Тип | Описание |
|---|---|---|
| tg_id | number | Telegram user ID, **первичный ключ** |
| tg_username | text | @username, может отсутствовать |
| first_name | text | Имя из Telegram |
| last_name | text | Фамилия из Telegram |
| role | text | `manager` / `client` / `admin` |
| created_at | datetime | Дата первого `/start` |
| last_seen_at | datetime | Последний контакт с ботом |
| invite_code_used | text | Код приглашения, если был |
### 5.2 Лист «Managers»
Расширенный профиль менеджеров.
| Колонка | Тип | Описание |
|---|---|---|
| tg_id | number | FK → Users.tg_id |
| full_name | text | ФИО полное |
| email | text | Рабочая почта |
| phone | text | Телефон |
| salon | text | Название салона |
| city | text | Город |
| is_zov_employee | bool | true = свой, false = партнёр |
| status | formula | `=IF(active_until>=TODAY();"active";"lapsed")` |
| last_order_date | date | Дата последней сделки через куратора |
| active_until | formula | `=last_order_date+90` |
| total_leads | formula | `=COUNTIF(Leads.manager_tg_id; tg_id)` |
| total_deals | number | Заполняется куратором вручную |
| conversion_rate | formula | `=total_deals / total_leads` |
| invite_code | text | Уникальный код для генерации client-ссылок (например `MGR_AB12`) |
### 5.3 Лист «Clients»
| Колонка | Тип | Описание |
|---|---|---|
| tg_id | number | FK → Users.tg_id |
| full_name | text | |
| phone | text | |
| email | text | |
| address | text | |
| city | text | |
| budget_total | number | Общий бюджет на кухню + технику |
| manager_tg_id | number | FK → Managers.tg_id, **связка клиент—менеджер** |
| source | text | `channel` / `manager_invite` / `direct` |
| last_measurement_id | text | FK → Measurements.id |
### 5.4 Лист «Measurements»
| Колонка | Тип | Описание |
|---|---|---|
| id | text | UUID |
| created_at | datetime | |
| client_tg_id | number | FK → Clients.tg_id |
| manager_tg_id | number | FK → Managers.tg_id |
| filled_by | text | `client_self` / `manager_for_client` |
| layout | text | прямая / Г / П / остров / другое |
| area_m2 | number | |
| ceiling_mm | number | |
| walls_json | json-text | `{"w1":3200,"w2":2400,"w3":null}` |
| openings_json | json-text | окна, двери |
| infra_json | json-text | газ/электрика, шахта, мойка |
| niches_json | json-text | размеры ниш под встройку |
| photos_urls | text | через запятую |
| notes | text | |
| status | text | `draft` / `submitted` / `verified` |
### 5.5 Лист «Leads» (заявки на подбор)
| Колонка | Тип | Описание |
|---|---|---|
| id | text | UUID |
| created_at | datetime | |
| manager_tg_id | number | кто отправил |
| client_tg_id | number | для кого |
| client_name | text | |
| measurement_id | text | FK → Measurements.id, может быть пусто |
| checklist_json | json-text | весь ответ чек-листа |
| ai_response | long-text | результат от LLM |
| ai_model | text | `gpt-4o-mini` |
| ai_tokens_used | number | для контроля расходов |
| sent_to_tg | bool | результат доставлен в Telegram |
| deal_status | text | `new` / `in_progress` / `won` / `lost` (заполняет менеджер вручную или куратор) |
| deal_amount | number | сумма закрытой кухни |
### 5.6 Лист «Logs»
Лог всех action-событий (для дебага и аналитики).
| Колонка | Тип |
|---|---|
| timestamp | datetime |
| event | text (`bot_start`, `role_selected`, `miniapp_opened`, `measurement_submitted`, `lead_created`, `ai_response_received`, `ai_error`, ...) |
| tg_id | number |
| payload | json-text |
### 5.7 Лист «Settings»
Параметры, которые могут меняться без правки кода.
| key | value | description |
|---|---|---|
| ACTIVE_PERIOD_DAYS | 90 | срок active-статуса после сделки |
| GRACE_PERIOD_DAYS | 14 | grace-период перед переводом в lapsed |
| AI_MODEL | gpt-4o-mini | какая модель используется |
| AI_TEMPERATURE | 0.3 | температура для подбора |
| ADMIN_TG_ID | <ваш tg_id> | кто получает алерты |
| PAID_PRICE_PER_LEAD | 500 | цена pay-per-use |
| PAID_SUBSCRIPTION | 3000 | цена подписки |
### 5.8 Лист «Dashboard» (для куратора)
Сводка с формулами:
```
Активных менеджеров: =COUNTIF(Managers.status; "active")
Lapsed-менеджеров: =COUNTIF(Managers.status; "lapsed")
Заявок за 30 дней: =COUNTIFS(Leads.created_at; ">="&TODAY()-30)
Конверсия в сделку: =COUNTIF(Leads.deal_status; "won") / COUNTA(Leads.id)
Средний чек закрытых сделок: =AVERAGEIF(Leads.deal_status; "won"; Leads.deal_amount)
Топ-5 менеджеров по сделкам: pivot
Расход на AI за 30 дней: =SUMPRODUCT(Leads.ai_tokens_used) * 0.0000015 * <курс>
```
---
## 6. Backend (Google Apps Script)
### 6.1 Структура проекта
```
ZOV_Backend.gs
├─ doPost(e) — единая точка входа, роутер
├─ /api/me — определить пользователя по initData
├─ /api/measurement — сохранить замер
├─ /api/podbor — создать заявку, вызвать AI, отправить в Telegram
├─ /api/lead/:id — получить заявку (для менеджера)
├─ /api/manager/status — статус менеджера
├─ helpers/
│ ├─ verifyInitData(initData, botToken)
│ ├─ readSheet(sheetName, query)
│ ├─ writeSheet(sheetName, row)
│ ├─ callOpenAI(prompt, params)
│ ├─ sendTelegram(chatId, text, options)
│ └─ generateUuid()
```
### 6.2 Деплой
1. Привязать скрипт к Google Sheet «ЗОВ — База».
2. Deploy → New deployment → Type «Web app».
3. Execute as: «Me».
4. Who has access: «Anyone» (для приёма запросов с MiniApp).
5. Получить URL `https://script.google.com/macros/s/.../exec` — это backend endpoint, прописать в MiniApp как `BACKEND_URL`.
### 6.3 Переменные окружения (Script Properties)
| Ключ | Описание |
|---|---|
| `BOT_TOKEN` | Токен бота от BotFather |
| `OPENAI_API_KEY` | ключ OpenAI |
| `ADMIN_TG_ID` | ваш Telegram ID |
| `SHEET_ID` | ID Google Sheet |
### 6.4 Безопасность
- Все запросы из MiniApp **обязаны** содержать `initData` от Telegram.
- Backend проверяет HMAC `initData` по `BOT_TOKEN` (стандартная процедура).
- Без валидного `initData` любой запрос → 401.
- Документация процедуры: <https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app>
### 6.5 Бизнес-логика статусов менеджера
```pseudo
function getManagerStatus(tg_id):
m = Managers.find(tg_id)
if not m: return "unknown"
if m.is_zov_employee: return "active" // свои всегда active
active_until = m.last_order_date + ACTIVE_PERIOD_DAYS
grace_until = active_until + GRACE_PERIOD_DAYS
today = TODAY()
if today <= active_until: return "active"
if today <= grace_until: return "grace" // мягкое предупреждение
return "lapsed"
```
---
## 7. AI-подбор
### 7.1 Входные данные (контракт LLM-вызова)
JSON, который backend собирает перед отправкой в OpenAI:
```json
{
"client": {
"name": "Пётр Сидоров",
"city": "Москва",
"family": "пара_с_ребёнком",
"cooking_frequency": "ежедневно",
"favorite_techniques": ["выпечка", "пар"],
"guests": "иногда"
},
"kitchen": {
"layout": "Г-образная",
"area_m2": 12,
"ceiling_mm": 2700,
"walls": { "w1": 3200, "w2": 2400 },
"infra": {
"stove_power": "электрика_220",
"vent_shaft": true
},
"niches": {
"fridge": { "w": 600, "h": 1850, "d": 600 },
"hob": { "w": 600, "h": 60, "d": 520 },
"oven": { "w": 600, "h": 600, "d": 560 },
"dw": { "w": 450, "h": 820, "d": 555 }
}
},
"budget": {
"total": 350000,
"by_category": {
"fridge": 80000,
"hob": 50000,
"oven": 60000,
"hood": 25000,
"dw": 50000,
"microwave": 15000,
"coffee": 40000,
"washer": 30000
}
},
"preferences": {
"fridge": {
"tier": "middle",
"brands_preferred": ["Bosch", "Liebherr"],
"brands_alternative": ["Samsung"],
"type": "двухкамерный",
"color": "inox",
"features": ["NoFrost", "инвертор", "≤40 дБ"]
},
"hob": { "...": "..." }
/* и т.д. для всех 8 категорий, как в чек-листе */
},
"exclusions": ["microwave"] // уже есть, не подбирать
}
```
### 7.2 Промпт (system + user)
**System prompt:**
```
Ты — эксперт-консультант по подбору кухонной техники для фабрики мебели «ЗОВ».
Ты работаешь с менеджерами салонов и помогаешь им быстро согласовать с клиентом
комплект техники под их кухню.
Принципы подбора:
1. Физические ограничения важнее эстетики. Если ниша 600×1850×600 — не предлагай
холодильник 700×2000×650, даже если он лучше.
2. Уважай бюджет. Если в категории задан лимит — не превышай его более чем на 10%.
3. Уважай предпочтения по брендам. Сначала пробуй preferred (★), потом alternative (✓),
потом нейтральные альтернативы. Если ни один preferred бренд не подходит по цене —
скажи прямо.
4. Связывай выбор со сценарием использования. Семья с детьми = простота интерфейса,
защита от детей, легко мыть. Любитель выпечки = пар + 4D HotAir + термощуп.
5. Учитывай инфраструктуру. Газ исключает индукцию (если пользователь не готов
менять подключение). Нет шахты = только рециркуляция в вытяжке.
6. По каждой позиции: модель, цена, 2-3 ключевых преимущества под этого клиента,
1 предупреждение/нюанс если он есть.
Формат ответа — JSON по схеме:
{
"summary": "1-2 предложения",
"items": [
{
"category": "fridge",
"brand": "Bosch",
"model": "KGN39LB35R",
"price_rub": 79990,
"size_mm": { "w": 600, "h": 2030, "d": 660 },
"fits_niche": true,
"highlights": ["NoFrost", "инвертор", "тихий 39 дБ"],
"caveats": "Глубина 660мм — на 60мм глубже стандартной ниши, проверьте",
"match_score": 0.92,
"rationale": "Соответствует предпочтениям по бренду и тиру..."
}
],
"total_price_rub": 350000,
"budget_status": "в рамках" | "превышение" | "значительно ниже",
"warnings": ["..."],
"next_steps": ["..."]
}
Не выдумывай несуществующие модели. Если не уверен в модели — указывай линейку
("Bosch Serie 4, 60см, NoFrost") а не конкретный артикул.
Если данных недостаточно для подбора в категории — оставляй её с пометкой
"need_more_info" и указывай, чего не хватает.
Отвечай только валидным JSON без оборачивания в markdown-блоки.
```
**User prompt:**
```
Подбери технику для следующего клиента:
{JSON_INPUT}
Дай развёрнутое предложение в формате, описанном в инструкции.
```
### 7.3 Параметры вызова OpenAI
```python
{
"model": "gpt-4o-mini",
"temperature": 0.3,
"max_tokens": 4000,
"response_format": { "type": "json_object" }
}
```
### 7.4 Постобработка ответа
Backend получает JSON от OpenAI и:
1. Валидирует схему. При расхождении — алёрт куратору, ответ всё равно пытается доставить.
2. Сохраняет в `Leads.ai_response` (raw JSON).
3. Рендерит в человекочитаемый вид (текст + кнопки).
4. Отправляет менеджеру через бота:
```
✅ Подбор готов
Клиент: Пётр Сидоров
Бюджет: 350 000 ₽
🧊 Холодильник
Bosch Serie 4 KGN39LB35R — 79 990 ₽
✓ NoFrost, инвертор, тихий 39 дБ
⚠️ Глубина 660мм — на 60мм больше ниши, проверьте
🔥 Варочная панель
Electrolux EHF6342XOK — 49 990 ₽
✓ Hi-Light, защита от детей, простой интерфейс
...
ИТОГО: 348 970 ₽ (в рамках бюджета)
[📄 Скачать предложение для клиента]
[💬 Открыть в кабинете]
```
---
## 8. Безопасность и приватность
### 8.1 Аутентификация
- Все вызовы backend от MiniApp подписаны `initData` Telegram.
- Backend верифицирует HMAC `initData` ≤ 24 часов давности.
- Без подписи → 401.
### 8.2 Контроль доступа
- Менеджер видит **только свои** заявки и замеры (фильтр `WHERE manager_tg_id = me.tg_id`).
- Клиент видит **только свои** замеры.
- Куратор (admin) видит всё.
### 8.3 Хранение PII
- Имена, телефоны, адреса клиентов лежат в Google Sheet куратора.
- Доступ к Sheet — только у куратора.
- В логи не пишутся телефоны/email открытым текстом — только маскированно (`+7***1234`).
### 8.4 Согласие на обработку данных
- При первом `/start` клиент получает короткое сообщение:
> Заполняя форму подбора, вы соглашаетесь с обработкой данных в рамках консультации по подбору кухни. Подробнее — [политика](https://wasrusgen.github.io/zov-tech/privacy.html).
- Простая HTML-страница `privacy.html` рядом с MiniApp.
---
## 9. Метрики успеха MVP
Через 60 дней после запуска проверяем:
| Метрика | Целевое значение |
|---|---|
| Зарегистрированных менеджеров | ≥ 60 из 112 |
| Менеджеров, сделавших ≥1 подбор | ≥ 30 |
| Сделанных замеров клиентами | ≥ 50 |
| Заявок на подбор | ≥ 80 |
| AI-подборов с положительной обратной связью | ≥ 70% |
| Время от заявки до результата | ≤ 60 секунд |
| Конверсия подбор → закрытая сделка | ≥ 20% |
| Стоимость одного подбора (AI) | ≤ 0.5 ₽ |
---
## 10. Дорожная карта после MVP
**Спринт 4 (через 1 месяц после MVP):**
- Раздел «Сделки» с возможностью менеджеру отмечать выигранные сделки.
- Авто-продление статуса по факту отметки сделки (с подтверждением куратора).
- Раздел «База знаний» с гайдами и сравнениями.
**Спринт 5 (через 2 месяца):**
- Платный режим для lapsed-менеджеров через ЮKassa или Telegram Stars.
- Подписка / pay-per-use, биллинг.
**Спринт 6 (3+ месяца):**
- Записаться в салон (выбор салона + дата + слот).
- Личный кабинет клиента с историей замеров и заявок.
- Раздел «Идеи и кейсы» с галереей.
- Партнёрки с производителями техники.
---
## 11. Чек-лист запуска
```
[ ] Бот создан в @BotFather, токен сохранён в Apps Script Properties
[ ] Домен MiniApp привязан к боту (/setdomain)
[ ] MiniApp зарегистрирован в @BotFather (/newapp)
[ ] HTML захостен на GitHub Pages, HTTPS работает
[ ] Google Sheet «ЗОВ — База» создан, листы 5.1—5.7 готовы
[ ] Apps Script деплой как Web App, URL получен
[ ] OpenAI ключ получен, лимиты выставлены ($20/мес)
[ ] Реестр 112 менеджеров загружен в Sheet «Managers»
[ ] Свои менеджеры ЗОВ помечены is_zov_employee=true
[ ] Существующий чек-лист `02_Чек-лист_клиенту.html` доработан (4.5)
[ ] Форма самозамера реализована (4.4 шаги 1-5)
[ ] AI-промпт протестирован на 5 синтетических заявках
[ ] Уведомления бота работают (3.6)
[ ] Privacy-страница опубликована
[ ] /admin меню работает только для ADMIN_TG_ID
[ ] Бот переведён в режим webhook
[ ] Закрепили пост в канале с кнопкой WebApp
[ ] Сделали внутренний прогон с 3 «своими» менеджерами
```
---
## 12. Контакты и ответственные
| Роль | Кто | Контакт |
|---|---|---|
| Заказчик / куратор | Василий | vasrusgen@gmail.com, @wasrusgen |
| Канал | @wasrusgen1 | |
| Telegram-бот | @zov_tech_bot (после регистрации) | |
| MiniApp URL | https://wasrusgen.github.io/zov-tech/ (плановый) | |
---
## 13. Приложение: что отдаём разработчику
При выдаче ТЗ разработчику передаём:
1. Этот документ (`ТЗ_ЗОВот_MiniApp_v1.md`).
2. Текущий HTML-чек-лист `02_Чек-лист_клиенту.html`.
3. Доступ к Telegram-каналу @wasrusgen1 (для размещения кнопки).
4. Креды от Google-аккаунта для создания Sheet и Apps Script.
5. (После регистрации бота) — токен и admin_tg_id в зашифрованном виде.
**Ожидаемое время выполнения MVP:** 57 рабочих дней.
**Ожидаемая стоимость инфраструктуры в месяц:** $515 (только OpenAI, всё остальное бесплатно).

119
miniapp/assets/app.js Normal file
View File

@ -0,0 +1,119 @@
// ЗОВ MiniApp — главный скрипт
// На входе: подписанный initData от Telegram.
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
const tg = window.Telegram?.WebApp;
const BACKEND_URL = ""; // TODO: заполнить URL Apps Script Web App
const app = document.getElementById("app");
async function fetchMe() {
if (!BACKEND_URL) {
// dev-режим без backend — для локального просмотра вёрстки
return {
role: "manager",
user: { full_name: "Тест Менеджер", salon: "ЗОВ Москва" },
status: "active",
status_until: "2026-08-12",
};
}
const res = await fetch(`${BACKEND_URL}/api/me`, {
method: "POST",
body: JSON.stringify({
initData: tg?.initData || "",
startParam: tg?.initDataUnsafe?.start_param || null,
}),
});
return res.json();
}
function renderManager(me) {
const status = me.status || "active";
app.innerHTML = `
<div class="header">
<h1>Кабинет менеджера</h1>
<div class="subtitle">
${me.user.full_name} · ${me.user.salon || ""}
<span class="status-badge ${status}">${statusLabel(status)}</span>
</div>
</div>
<nav class="menu">
<a class="menu-item" href="#/m/podbor">
<span class="icon">🔧</span>
<span class="label">Подбор техники для клиента</span>
<span class="arrow"></span>
</a>
<a class="menu-item" href="#/m/measurements">
<span class="icon">📐</span>
<span class="label">Замеры</span>
<span class="arrow"></span>
</a>
<div class="menu-item disabled">
<span class="icon">📋</span>
<span class="label">Заявки клиентов <small>(скоро)</small></span>
</div>
<div class="menu-item disabled">
<span class="icon">💼</span>
<span class="label">Сделки <small>(скоро)</small></span>
</div>
<a class="menu-item" href="#/m/status">
<span class="icon">💰</span>
<span class="label">Мой статус и доступ</span>
<span class="arrow"></span>
</a>
</nav>
`;
}
function renderClient(me) {
app.innerHTML = `
<div class="header">
<h1>Кабинет клиента</h1>
<div class="subtitle">
${me.user.full_name || "Здравствуйте!"}
${me.manager ? `<br>Менеджер: ${me.manager.full_name}, ${me.manager.salon || ""}` : ""}
</div>
</div>
<nav class="menu">
<a class="menu-item" href="#/c/measure">
<span class="icon">📐</span>
<span class="label">Замер кухни</span>
<span class="arrow"></span>
</a>
<div class="menu-item disabled">
<span class="icon">🔧</span>
<span class="label">Подобрать технику <small>(скоро)</small></span>
</div>
<div class="menu-item disabled">
<span class="icon">💡</span>
<span class="label">Идеи и кейсы <small>(скоро)</small></span>
</div>
<a class="menu-item" href="#/c/contact">
<span class="icon">📞</span>
<span class="label">Связаться с менеджером</span>
<span class="arrow"></span>
</a>
</nav>
`;
}
function statusLabel(s) {
return { active: "🟢 active", lapsed: "🔴 lapsed", grace: "🟡 grace" }[s] || s;
}
async function init() {
if (tg) {
tg.ready();
tg.expand();
}
try {
const me = await fetchMe();
if (me.role === "manager") renderManager(me);
else renderClient(me);
} catch (e) {
app.innerHTML = `<div class="loader">Ошибка загрузки. Попробуйте позже.</div>`;
console.error(e);
}
}
init();

107
miniapp/assets/styles.css Normal file
View File

@ -0,0 +1,107 @@
:root {
--bronze: #76BD22;
--accent: #003E7E;
--cream: #FAFAFA;
--sand: #F0F9E8;
--ink: #1B1B1B;
--muted: #6B6B6B;
--line: #E5E5E5;
--r: 12px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--ink);
background: var(--cream);
}
#app {
max-width: 720px;
margin: 0 auto;
padding: 16px;
min-height: 100vh;
}
.loader {
text-align: center;
padding: 64px 16px;
color: var(--muted);
}
.header {
margin-bottom: 16px;
}
.header h1 {
font-size: 20px;
margin: 0 0 4px;
color: var(--accent);
}
.header .subtitle {
font-size: 14px;
color: var(--muted);
}
.menu {
display: grid;
gap: 8px;
}
.menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: white;
border: 1px solid var(--line);
border-radius: var(--r);
cursor: pointer;
user-select: none;
text-decoration: none;
color: inherit;
transition: transform 0.05s, box-shadow 0.1s;
}
.menu-item:active {
transform: scale(0.99);
box-shadow: var(--shadow);
}
.menu-item.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.menu-item .icon {
font-size: 22px;
flex-shrink: 0;
}
.menu-item .label {
flex: 1;
font-weight: 500;
}
.menu-item .arrow {
color: var(--muted);
font-size: 18px;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active { background: var(--sand); color: var(--bronze); }
.status-badge.lapsed { background: #FFF1F0; color: #C82C2C; }
.status-badge.grace { background: #FFF8E1; color: #B07E00; }

16
miniapp/index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>ЗОВ — Кабинет</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css">
</head>
<body>
<main id="app">
<div class="loader">Загрузка...</div>
</main>
<script src="assets/app.js"></script>
</body>
</html>