mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 14:24:49 +00:00
chore: initial scaffold (bot, miniapp, backend, docs)
This commit is contained in:
commit
0c5ed48303
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal 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
47
README.md
Normal 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
72
backend/Code.gs
Normal 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
37
backend/README.md
Normal 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
24
bot/.env.example
Normal 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
52
bot/config.py
Normal 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
0
bot/handlers/__init__.py
Normal file
62
bot/handlers/start.py
Normal file
62
bot/handlers/start.py
Normal 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
37
bot/main.py
Normal 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
6
bot/requirements.txt
Normal 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
0
bot/services/__init__.py
Normal file
950
docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md
Normal file
950
docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md
Normal 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. **Блок «Семья и готовка»** — 4–5 быстрых вопросов с радиокнопками:
|
||||||
|
- Состав семьи: 1 взрослый / пара / семья с детьми / 2+ поколения
|
||||||
|
- Частота готовки: ежедневно / 3–4 раза в неделю / реже
|
||||||
|
- Любимые техники: выпечка / на пару / гриль / 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:** 5–7 рабочих дней.
|
||||||
|
**Ожидаемая стоимость инфраструктуры в месяц:** $5–15 (только OpenAI, всё остальное бесплатно).
|
||||||
119
miniapp/assets/app.js
Normal file
119
miniapp/assets/app.js
Normal 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
107
miniapp/assets/styles.css
Normal 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
16
miniapp/index.html
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user