From 0c5ed4830319237f76e4733166177da4d80f9d2e Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Fri, 8 May 2026 23:56:48 +0300 Subject: [PATCH] chore: initial scaffold (bot, miniapp, backend, docs) --- .gitignore | 64 +++ README.md | 47 ++ backend/Code.gs | 72 +++ backend/README.md | 37 ++ bot/.env.example | 24 + bot/config.py | 52 ++ bot/handlers/__init__.py | 0 bot/handlers/start.py | 62 +++ bot/main.py | 37 ++ bot/requirements.txt | 6 + bot/services/__init__.py | 0 docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md | 950 ++++++++++++++++++++++++++++++++++ miniapp/assets/app.js | 119 +++++ miniapp/assets/styles.css | 107 ++++ miniapp/index.html | 16 + 15 files changed, 1593 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/Code.gs create mode 100644 backend/README.md create mode 100644 bot/.env.example create mode 100644 bot/config.py create mode 100644 bot/handlers/__init__.py create mode 100644 bot/handlers/start.py create mode 100644 bot/main.py create mode 100644 bot/requirements.txt create mode 100644 bot/services/__init__.py create mode 100644 docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md create mode 100644 miniapp/assets/app.js create mode 100644 miniapp/assets/styles.css create mode 100644 miniapp/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc8014a --- /dev/null +++ b/.gitignore @@ -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* diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f51f61 --- /dev/null +++ b/README.md @@ -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) diff --git a/backend/Code.gs b/backend/Code.gs new file mode 100644 index 0000000..6e7a62f --- /dev/null +++ b/backend/Code.gs @@ -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" }; +} diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..8f6b781 --- /dev/null +++ b/backend/README.md @@ -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 +# или для уже существующего: +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) | diff --git a/bot/.env.example b/bot/.env.example new file mode 100644 index 0000000..6ee01fc --- /dev/null +++ b/bot/.env.example @@ -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 diff --git a/bot/config.py b/bot/config.py new file mode 100644 index 0000000..6aa879f --- /dev/null +++ b/bot/config.py @@ -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")), + ) diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/handlers/start.py b/bot/handlers/start.py new file mode 100644 index 0000000..d8556a3 --- /dev/null +++ b/bot/handlers/start.py @@ -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() diff --git a/bot/main.py b/bot/main.py new file mode 100644 index 0000000..cba9e1c --- /dev/null +++ b/bot/main.py @@ -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()) diff --git a/bot/requirements.txt b/bot/requirements.txt new file mode 100644 index 0000000..4f7d125 --- /dev/null +++ b/bot/requirements.txt @@ -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 diff --git a/bot/services/__init__.py b/bot/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md b/docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md new file mode 100644 index 0000000..ed54e12 --- /dev/null +++ b/docs/ТЗ_ЗОВ_Бот_MiniApp_v1.md @@ -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 ` | все | спец-сценарий: переход по ссылке-приглашению от менеджера | +| `/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. +- Документация процедуры: + +### 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, всё остальное бесплатно). diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js new file mode 100644 index 0000000..528ddf0 --- /dev/null +++ b/miniapp/assets/app.js @@ -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 = ` +
+

Кабинет менеджера

+
+ ${me.user.full_name} · ${me.user.salon || ""} + ${statusLabel(status)} +
+
+ + `; +} + +function renderClient(me) { + app.innerHTML = ` +
+

Кабинет клиента

+
+ ${me.user.full_name || "Здравствуйте!"} + ${me.manager ? `
Менеджер: ${me.manager.full_name}, ${me.manager.salon || ""}` : ""} +
+
+ + `; +} + +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 = `
Ошибка загрузки. Попробуйте позже.
`; + console.error(e); + } +} + +init(); diff --git a/miniapp/assets/styles.css b/miniapp/assets/styles.css new file mode 100644 index 0000000..8a990ba --- /dev/null +++ b/miniapp/assets/styles.css @@ -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; } diff --git a/miniapp/index.html b/miniapp/index.html new file mode 100644 index 0000000..16fdbd3 --- /dev/null +++ b/miniapp/index.html @@ -0,0 +1,16 @@ + + + + + + ЗОВ — Кабинет + + + + +
+
Загрузка...
+
+ + +