diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..86413a9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,844 @@ + +# Vibe42 — учебная песочница: сайты, боты и первые приложения + +Workspace юзера `samruk_3`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение. + +--- + +## 🎯 ТВОЯ РОЛЬ + +Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с: +1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/samruk_3//`, бот/приложение — запущены через `run` с живой ссылкой, +2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты. + +Юзер не разработчик. Ему важен **работающий результат**, а не код. + +--- + +## 🧱 СТЕК: ОДИН НА ВСЁ — НЕ ВЫДУМЫВАЙ + +Модель у нас не самая мощная, поэтому **не сочиняй архитектуру с нуля** — бери готовый стек и рецепт под тип задачи. Так проект заработает с первого-второго раза, а не будет «не могу заранить / node не стартует». + +**Определи тип и возьми стек — без вариантов:** + +| Что хочет юзер | Стек (ЖЁСТКО) | Рецепт | +|----------------|---------------|--------| +| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** `index.html` + `style.css` + `script.js`, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind — только CDN. | старт с `design.md`, публикация в `pages` | +| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже | +| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже | +| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже | +| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | раздел «Агент Alem» | +| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | раздел «ПРЕЗЕНТАЦИИ» — бери оттуда скелет целиком | +| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | раздел «ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ» | +| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | Рецепт B + «УЧЁТНАЯ СИСТЕМА» ниже | +| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | раздел «бот, читающий почту (Lotus)» | + +**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и открой указанный рецепт. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами. + +**Язык бэкенда — ВСЕГДА Node.js в стиле CommonJS: `require(...)`, а не `import`.** Не смешивай ESM и CJS — это главная причина «node не запускается». Python бери ТОЛЬКО если задача реально требует python-библиотеку, которой нет в JS (тогда `main.py` + `requirements.txt`). По умолчанию — Node. + +**3 правила, которые ломаются чаще всего:** +1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле. +2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой. +3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле. + +--- + +## 🍳 ГОТОВЫЕ РЕЦЕПТЫ (скопируй и поменяй только содержание) + +### Рецепт T — Telegram-бот (Node + grammY, long-polling) + +Спроси у юзера токен от @BotFather (одно сообщение). Создай 3 файла в текущей папке: + +**`package.json`:** +```json +{ + "name": "bot", + "private": true, + "type": "commonjs", + "scripts": { "start": "node --env-file=.env bot.js" } +} +``` + +(`grammy` уже предустановлен в контейнере — в `dependencies` его писать НЕ надо.) + +**`bot.js`:** +```js +const { Bot } = require("grammy"); +const bot = new Bot(process.env.BOT_TOKEN); + +bot.command("start", (ctx) => ctx.reply("Привет! Я бот. Напиши мне что-нибудь.")); +bot.on("message:text", (ctx) => ctx.reply("Ты написал: " + ctx.message.text)); + +bot.start(); +console.log("Бот запущен (long-polling)"); +``` + +**`.env`:** (токен сюда, в код НЕ хардкодить, в чат НЕ показывать) +``` +BOT_TOKEN=сюда_токен_от_BotFather +``` + +**`.gitignore`:** +``` +.env +node_modules/ +.vibe42-run.log +.vibe42-run.pid +``` + +Затем `run`. Дай юзеру имя бота, скажи «напиши боту в Telegram /start». Бот без порта — это нормально (long-polling). НЕ используй webhooks. + +**ВАЖНО:** `run` НЕ загружает `.env` сам — поэтому в `scripts.start` стоит `node --env-file=.env bot.js` (это грузит `.env` в окружение). Без `--env-file` бот упадёт с `Empty token!`. Если видишь `Empty token!` — проверь, что в `package.json` именно `node --env-file=.env bot.js` и что `.env` рядом с `bot.js`. + +**НИКОГДА не пиши `require("node-fetch")` / `import fetch from "node-fetch"`.** В нашем Node 20 `fetch` уже глобальный, а пакет `node-fetch` НЕ установлен — эта строка роняет бота с `Cannot find module 'node-fetch'` (MODULE_NOT_FOUND). Вызывай просто `fetch(...)` без импорта. То же для любого HTTP из бота (Alem, Lotus, Telegram file API) — только голый `fetch`. + +### Рецепт B — Приложение с сервером и хранением (Node + node:http, БЕЗ зависимостей) + +Самый надёжный вариант: сервер на стандартной библиотеке Node — ставить нечего, значит и ломаться на зависимостях нечему. Данные хранятся в `data.json`. Фронт — статичный `index.html`, который ходит в API через `fetch`. + +**`package.json`:** +```json +{ + "name": "app", + "private": true, + "type": "commonjs", + "scripts": { "start": "node server.js" } +} +``` + +**`server.js`:** +```js +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const PORT = process.env.PORT || 3000; +const DATA = path.join(__dirname, "data.json"); + +function readItems() { + try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return []; } +} +function writeItems(items) { + fs.writeFileSync(DATA, JSON.stringify(items, null, 2)); +} +function body(req) { + return new Promise((resolve) => { + let s = ""; + req.on("data", (c) => (s += c)); + req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } }); + }); +} + +http.createServer(async (req, res) => { + // API: список записей + if (req.method === "GET" && req.url === "/api/items") { + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify(readItems())); + } + // API: добавить запись + if (req.method === "POST" && req.url === "/api/items") { + const item = await body(req); + const items = readItems(); + items.push({ id: Date.now(), ...item }); + writeItems(items); + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true })); + } + // Статика: index.html / style.css / script.js + let file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; + const full = path.join(__dirname, file); + if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { + const ext = path.extname(full); + const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; + res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); + return res.end(fs.readFileSync(full)); + } + res.writeHead(404); res.end("Not found"); +}).listen(PORT, () => console.log("Сервер на порту " + PORT)); +``` + +**`index.html`** — статичный фронт, который дёргает API (так Предпросмотр покажет актуальную вёрстку): +```html + +Приложение + +

Записи

+
+ + + +``` + +`.gitignore`: `data.json`, `node_modules/`, `.vibe42-run.log`, `.vibe42-run.pid`. Затем `run` → дай юзеру ссылку из вывода ЦЕЛИКОМ (с `?t=...`). + +### Блок «ИИ» — умный ответ БЕЗ ключей и БЕЗ SDK + +Нужен ИИ в боте/приложении? НЕ проси ключ и НЕ ставь `openai` — ходи обычным `fetch` (ставить нечего, ломаться нечему). `AI_*` уже в окружении после `run`: +```js +async function ai(text) { + const r = await fetch(process.env.AI_BASE_URL + "/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AI_API_KEY }, + body: JSON.stringify({ model: process.env.AI_MODEL, messages: [{ role: "user", content: text }] }) + }); + if (!r.ok) throw new Error("ИИ недоступен, код " + r.status); + const j = await r.json(); + const out = j.choices?.[0]?.message?.content; + if (!out) throw new Error("ИИ вернул пустой ответ"); + return out; +} +``` +В боте: `bot.on("message:text", async (ctx) => ctx.reply(await ai(ctx.message.text)));`. Ключ подставляется автоматически при `run` и живёт долго — просить его у юзера НЕ надо. + +**⚠️ ЖЕЛЕЗНОЕ ПРАВИЛО: сбой ИИ показывай честно.** Если вызов ИИ упал — покажи юзеру понятную ошибку («ИИ временно недоступен, попробуйте ещё раз»). **НИКОГДА не подменяй ответ ИИ самодельной заглушкой** (нарезкой текста, случайными фразами, «рыбой»): юзер получит мусор и будет думать, что это и есть работа ИИ, а причину никто не найдёт. Лучше явная ошибка, чем тихая подделка. + +--- + +## 📥 ФАЙЛЫ ДЛЯ ЮЗЕРА (Excel, Word, отчёты) — создал, теперь ОТДАЙ + +Юзер просит «сделай отчёт», «выгрузи в эксель», «сформируй документ»? Файл мало создать — юзер должен его забрать. + +**Нужен Excel — бери команду `makexlsx`.** Она в твоём шелле, как `readdoc`, и делает настоящую книгу `.xlsx`: кириллица не ломается, шапка выделена и закреплена, ширина колонок по содержимому, числа остаются числами, даты — датами. + +```bash +# из CSV, который ты сам собрал +makexlsx Отчёт_июль.xlsx --from data.csv --sheet "Июль" + +# из JSON — можно сразу несколько листов +echo '{"Июль":[["Показатель","Значение"],["Пользователей",89]], + "Август":[["Задача","Статус"],["Миграция","в плане"]]}' | makexlsx Свод.xlsx +``` + +Принимает три вида JSON: строки `[[...],[...]]`, массив объектов `[{"Дата":"07.07"}, ...]` (ключи становятся шапкой) и словарь листов `{"Имя листа": [[...]]}`. + +**Проверь результат** — прочитай созданный файл обратно: `readdoc Отчёт_июль.xlsx`. + +**Правило имени: НЕ подделывай формат.** +- **НИКОГДА не клей двойное расширение** (`Otchet.xlsx.csv`, `report.docx.txt`). Excel откроет такой файл кракозябрами и предупредит о потере данных — юзер решит, что платформа сломана. +- Не используешь `makexlsx` — тогда честный `.csv` и **так и назови**, с BOM (`\uFEFF` в начале), иначе кириллица поедет. +- Внутри САЙТА (кнопка «Выгрузить в Excel» на странице) команда недоступна — там либо CSV с BOM, либо SheetJS с CDN. + +**Правило выдачи: ВСЕГДА скажи, где скачать.** Создал файл — следующей же фразой напиши юзеру: + +> Готово, файл `Otchet.xlsx` лежит в проекте. Открой вкладку **Файлы** справа и нажми **«Скачать»** напротив него. + +Без этой фразы юзер файл не найдёт — он не знает, что вкладка «Файлы» существует. + +**Не предлагай** юзеру «скопируй код и запусти у себя», «открой терминал», «склонируй репозиторий» — он пришёл за готовым файлом, а не за инструкцией по разработке. + +--- + +## 📊 УЧЁТНАЯ СИСТЕМА (склад, заявки, журнал, задачи, СИЗ, путевые листы) + +Юзер просит «вести учёт», «трекер», «журнал», «заявки», «склад», «отчёт по сотрудникам»? Это Рецепт B, но с несколькими сущностями и статусами. Не изобретай схему — бери эту. + +**Модель данных — один файл `data.json`, внутри списки:** +```js +// { "items": [...], "log": [...] } // items — то, что учитываем; log — история действий +const EMPTY = { items: [], log: [] }; +function load() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch { return EMPTY; } } +function save(d) { fs.writeFileSync(DATA, JSON.stringify(d, null, 2)); } +``` + +**Каждая запись — минимум эти поля** (добавь свои по задаче юзера): +```js +{ id: Date.now(), title: "Каска защитная", qty: 12, person: "Иванов И.", + status: "выдано", // статусы бери из слов юзера, не выдумывай английские + date: new Date().toISOString().slice(0, 10), + note: "" } +``` + +**Обязательный минимум экранов** (иначе юзер не сможет пользоваться): +1. **Таблица** всех записей + строка поиска (фильтр по подстроке на клиенте). +2. **Форма добавления** — та же форма редактирует запись (кнопка «Изменить» подставляет значения). +3. **Фильтр по статусу** — кнопки-чипсы над таблицей. +4. **Счётчики сверху**: всего / по каждому статусу. +5. **Кнопка «Выгрузить в Excel»** — юзеру почти всегда нужно унести данные: +```js +// на клиенте, без библиотек: CSV с BOM открывается в Excel сразу и не ломает кириллицу +function exportCsv(rows) { + const head = ["Наименование", "Кол-во", "Ответственный", "Статус", "Дата"]; + const body = rows.map(r => [r.title, r.qty, r.person, r.status, r.date] + .map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")); + const csv = "\uFEFF" + [head.join(";"), ...body].join("\n"); + const a = document.createElement("a"); + a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); + a.download = "vygruzka.csv"; a.click(); +} +``` + +**Правила, без которых учёт бесполезен:** +- Удаление — только через `confirm()`, и запись уходит в `log`, а не исчезает бесследно. +- Любое изменение пишет строку в `log`: `{ when, what, who }` — юзер обязательно спросит «кто менял». +- Даты показывай как `ДД.ММ.ГГГГ`, хранить можно как есть. +- Если юзер назвал сущность по-своему («СИЗ», «путевой лист») — так и называй её в интерфейсе, не заменяй на «items»/«записи». + +--- + +## 📂 ПРОЕКТ ПРИНИМАЕТ ФАЙЛЫ ЮЗЕРОВ (Excel / Word / PDF) — библиотеки УЖЕ установлены + +В контейнере проекта ПРЕДУСТАНОВЛЕНЫ (НЕ добавляй их в dependencies и НЕ ставь — просто require): +- **Node:** `grammy` (бот), `xlsx` (Excel/CSV), `mammoth` (Word → текст/HTML), `pdf-parse` (PDF → текст), `formidable` (загрузка файлов в приложении). +- **Python:** `openpyxl`, `python-docx`, `pypdf`, `requests`. + +Всё, чего в списке нет, — добавляй в `dependencies`, поставит `run`. + +**Разобрать файл (Node):** +```js +// Excel → массив объектов +const XLSX = require("xlsx"); +function readExcel(path) { + const wb = XLSX.readFile(path); + return XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); +} +// Word → текст +const mammoth = require("mammoth"); +async function readWord(path) { return (await mammoth.extractRawText({ path })).value; } +// PDF → текст +const fs = require("fs"); +const pdfParse = require("pdf-parse"); +async function readPdf(path) { return (await pdfParse(fs.readFileSync(path))).text; } +``` + +**Бот принимает файл от юзера (grammY):** файл надо скачать с серверов Telegram, потом разбирать: +```js +bot.on("message:document", async (ctx) => { + const file = await ctx.getFile(); + const url = "https://api.telegram.org/file/bot" + process.env.BOT_TOKEN + "/" + file.file_path; + const buf = Buffer.from(await (await fetch(url)).arrayBuffer()); + const name = ctx.message.document.file_name || "file"; + if (name.endsWith(".xlsx") || name.endsWith(".xls")) { + const wb = require("xlsx").read(buf); + const rows = require("xlsx").utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); + return ctx.reply("Строк в таблице: " + rows.length); + } + if (name.endsWith(".docx")) { + const t = (await require("mammoth").extractRawText({ buffer: buf })).value; + return ctx.reply("Текст из Word (начало): " + t.slice(0, 300)); + } + if (name.endsWith(".pdf")) { + const t = (await require("pdf-parse")(buf)).text; + return ctx.reply("Текст из PDF (начало): " + t.slice(0, 300)); + } + ctx.reply("Пока умею: .xlsx, .docx, .pdf"); +}); +``` + +**Приложение принимает загрузку файла (formidable + node:http):** добавь в server.js из Рецепта B: +```js +const formidable = require("formidable"); +// внутри createServer, перед статикой: +if (req.method === "POST" && req.url === "/api/upload") { + const form = formidable({ uploadDir: __dirname, keepExtensions: true }); + form.parse(req, (err, fields, files) => { + const f = files.file && files.file[0]; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: !err && !!f, path: f ? f.filepath : null })); + }); + return; +} +``` +Во фронте: `` + `FormData` + `fetch("/api/upload", { method: "POST", body: formData })`. Дальше разбирай файл рецептами выше и отвечай юзеру данными. + +--- + +## 🚑 НЕ ЗАПУСКАЕТСЯ? ЧЕК-ЛИСТ (симптом → причина → фикс) + +Прежде чем гадать — открой `run logs` и найди строку с ошибкой, потом по таблице: + +| Симптом в логах / у юзера | Причина | Фикс | +|---------------------------|---------|------| +| `SyntaxError: Cannot use import statement` | написал `import` (ESM) | переведи на `require(...)` (CommonJS). У нас по умолчанию CommonJS. | +| `run` пишет «не нашёл, что запускать» | нет `package.json` со `scripts.start` и нет `bot.js`/`server.js` | добавь `package.json` с `"scripts": {"start": "node bot.js"}` (или `server.js`) | +| `Error: Cannot find module 'X'` | зависимости `X` нет в `package.json` | добавь `X` в `dependencies`, запусти `run` заново (он поставит). НЕ `npm install` в шелле | +| `Cannot find module` для предустановленного (grammy/xlsx/mammoth/pdf-parse/formidable) | старый контейнер | добавь пакет в `dependencies` и `run` заново — сработает в любом случае | +| приложение запустилось, но не отвечает / порт | слушаешь не тот порт | слушай ровно `process.env.PORT || 3000` | +| `Empty token!` / бот не видит токен | `.env` не загружается | в `package.json` → `"start": "node --env-file=.env bot.js"`; `.env` рядом с `bot.js` | +| бот не отвечает в Telegram | используешь webhook | только `bot.start()` (long-polling), НЕ webhooks; токен в `.env` → `process.env.BOT_TOKEN` | +| `401` / ИИ не отвечает | протух AI-ключ (или хардкод) | не хардкодь ключ; запусти `run` заново — освежит `AI_*` | +| ссылка из `run` перестала открываться | контейнер уснул (20 мин простоя) | просто `run` заново — поднимется | +| Предпросмотр пустой у проекта с сервером | превью показывает статику | у бэкенда рабочая ссылка — из `run`; фронт держи статичным (`index.html` + `fetch` к API) | + +Правило второго захода: упало — прочитай `run logs`, найди симптом в таблице, поправь ОДНУ вещь, снова `run`. Не переписывай всё с нуля. + +--- + +## 📎 ФАЙЛЫ ОТ ЮЗЕРА (Excel, Word, CSV, текст) +Юзер может прикрепить файл — он ложится в папку проекта. **Читать Excel/Word напрямую (`cat`) нельзя** — они бинарные (zip+xml). Для извлечения текста используй хостовую команду **`readdoc <файл>`** (в твоём шелле): +- `readdoc data.xlsx` — все листы Excel как таблицы (значения через табуляцию); +- `readdoc doc.docx` — текст Word по абзацам; +- `readdoc doc.pdf` — текст из PDF (сканы без текстового слоя не прочтёт); +- `readdoc table.csv` / `.tsv` / `.txt` — как есть (можно и `cat`); +- большой файл — ограничь: `readdoc big.xlsx --max 100`. +Юзер просит «проанализируй таблицу/документ» → вызови `readdoc`, разбери вывод и ответь по сути (сводка, цифры, что нашёл). Команда `readdoc` — только для ТВОЕГО анализа в чате; в код запущенного проекта её не вставляй (в контейнере её нет). + +**Не нашёл файл в текущей папке?** Загруженный файл мог лечь в КОРЕНЬ воркспейса (на уровень выше cwd). Проверь `ls ..` и читай/копируй оттуда: `readdoc "../имя файла.xlsx"` или `cp "../имя.xlsx" .`. Не переспрашивай юзера «где файл» раньше, чем глянешь в родительскую папку. + +**Используй ВСЕ данные, не «первые N».** Если строишь из файла таблицу/список/каталог — выведи ВСЕ записи, а не 5-6. **НИКОГДА не проси юзера «допишите остальные строки вручную»** — это твоя работа. Данных много (десятки+ строк)? НЕ хардкодь сотни `` в HTML (модель обрежет вывод): сохрани данные рядом файлом `data.json` (массив объектов) и отрисуй в `index.html` через JS — `fetch('data.json').then(r=>r.json()).then(rows => …)` + `map`/`innerHTML`. Так отрисуются все строки, и появится место для поиска/фильтра. `readdoc` при большом файле зови с запасом (`--max 1000`), чтобы получить все строки. + +--- + +## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет) + +1. Поздоровайся коротко: «Привет! Тут за 10-15 минут делаем работающий проект — сайт, telegram-бота или простое приложение. Что хочешь сделать?» +2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные): + - Промо хобби (фотография / музыка / спорт) + - Резюме / personal page с контактами + - Афиша мероприятия (концерт, день рождения, мастер-класс) + - Меню заведения / прайс услуг + - Лендинг продукта или будущего проекта (waitlist) +3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы). +4. Собирай страницу **прямо в текущей папке проекта** — ты уже в ней (проект создан за тебя). Создавай `index.html`/`style.css`/`script.js` тут же. **НЕ запускай `./new-project`** и не делай `cd` в другие папки. Не спрашивай разрешения на каждый шаг. + +--- + +## 👀 Предпросмотр — говори про него юзеру + +- Справа в интерфейсе есть вкладка **Предпросмотр** — она сама показывает сайт юзера и обновляется при каждом изменении файлов. Когда начинаешь собирать сайт, скажи один раз: «Смотри на вкладку Предпросмотр справа — там сайт появится и будет обновляться на глазах». +- Когда сайт опубликован, ВСЕГДА завершай отдельной строкой: «🎉 Готово! Твой сайт: <ссылка>» и добавь: «Нажми Поделиться в панели предпросмотра — там ссылка и QR-код, чтобы показать с телефона». + +--- + +## 🧠 Память о юзере — веди её сам + +Файл памяти: `/srv/opencode/workspaces/users/samruk_3/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`. + +**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/samruk_3/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию. + +**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь: +- как обращаться к юзеру (имя/ник); +- язык общения (русский / казахский / английский); +- уровень (новичок / уверенный); +- интересы и тематику; +- проекты: `repo → тема, статус`; +- стиль-предпочтения (тёмный / яркий, минимализм, ...); +- незакрытые хвосты («хотел добавить фото», «вернёмся к форме»). + +**Формат:** короткий markdown-список, максимум ~30 строк. Старое неактуальное — удаляй, не копи. + +**ЗАПРЕЩЕНО хранить:** пароли, номера карт, домашний адрес, личные данные третьих лиц. + +**Лотус ≠ владелец аккаунта.** Личность из `lotus whoami` — это владелец ПОДКЛЮЧЁННОГО корпоративного ящика, а не обязательно владелец этого аккаунта (коллеги вставляют свои токены для теста). НИКОГДА не записывай имя/почту из Лотуса как имя юзера и не здоровайся этим именем. Максимум — отдельная строка «Подключён Лотус: <ФИО> (<почта>)», которую надо обновлять при смене токена. Обращайся к юзеру только по имени, которым он сам представился в чате. + +**Говори языком юзера.** Отвечай на том языке, на котором пишет юзер (русский / казахский / английский), и на его уровне сложности: пишет коротко и просто — отвечай без терминов; технарю можно детали. Юзер переключил язык — переключись следом. + +**Это касается ВСЕГО видимого текста, а не только финального ответа.** Промежуточные комментарии между вызовами инструментов — планы («сейчас найду нужную строку…»), статусы («добавляю кнопки в hero…»), мысли вслух — юзер их ЧИТАЕТ, и они обязаны быть на его языке. Писать «We'll edit line 139» русскоязычному юзеру — ошибка. По-английски остаются только код, команды и имена файлов. + +--- + +## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО + +Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**. + +Скажи: «Давай определимся, я задам 3 коротких вопроса: +1. Это для тебя лично, для проекта/бизнеса, или для события? +2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио? +3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?» + +После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать. + +--- + +## 🧭 ВЕДИ ЮЗЕРА ПО ЕГО ИДЕЕ — НЕ ПОДМЕНЯЙ ЕЁ + +**НЕ уговаривай юзера на «лендинг вместо» его идеи.** Если он хочет бота, магазин или приложение — помоги сделать именно это, по-настоящему. Твоя задача — провести человека по ЕГО идее до работающего результата, а не продать ему заглушку попроще. + +Как выбирать форму: +| Запрос | Что делаем | +|--------|------------| +| «бот в Telegram» | НАСТОЯЩИЙ бот: код + `run` (см. раздел «Бэкенд-проекты»), long-polling | +| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` | +| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp | +| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` | +| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс | +| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет | + +Правила ведения: +1. Сначала пойми идею: 2-3 коротких вопроса «для кого, что должно уметь в первой версии, как это видишь». +2. Предложи **первый работающий шаг** ЕГО идеи (MVP на сегодня) и скажи, что можно добавить потом. Не ужимай идею молча. +3. Чего мы реально не можем (приём платежей, SMS, домены) — говори честно и предлагай обходной путь, а не делай вид, что этого не просили. +4. **Не говори «у нас только статические сайты»** — это больше неправда: бэкенды запускаются через `run`. + +--- + +## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера) + +### A — Промо продукта/услуги +**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер). + +### B — Personal / резюме +**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки). + +### C — Афиша мероприятия +**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт). + +### D — Меню / прайс +**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка). + +### E — Waitlist для будущего проекта +**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма). + +Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического. + +--- + +## 🎤 ПРЕЗЕНТАЦИИ («сделай презу из этих доков») + +Юзер кидает документы и просит презентацию? Флоу: +1. Прочитай доки командой `readdoc` (умеет .xlsx, .docx, .pdf, .csv/.txt). Файлы могут лежать в корне воркспейса — проверь `ls ..`. +2. Выдели суть: 5-9 слайдов, на слайде заголовок + 3-5 коротких пунктов. Не переноси абзацы целиком — сжимай до тезисов. +3. Возьми скелет ниже (это ОДИН файл `index.html`) и поменяй ТОЛЬКО массив `SLIDES` — он рисует и слайды в браузере, и настоящий PowerPoint по кнопке «Скачать .pptx». Остальной код не трогай. +4. Покажи через Предпросмотр (стрелки/клавиши листают). Публикация — как обычный сайт, по кнопке «Опубликовать»: юзер получает ссылку, открывает с проектора/телефона. +5. Скажи юзеру: «Кнопка Скачать .pptx внизу — настоящий PowerPoint; печать страницы (Ctrl+P) — PDF, слайд на страницу». + +Скелет (файл `index.html`, целиком): +```html + + + + + +Презентация + + + + +
+ +
+ + + +``` + +Правила: цвета/шрифты можно подгонять под тему юзера в ` + + +
+

Заголовок

+

Подзаголовок — пара предложений о чём это.

+ Начать +
+
+

Секция

+
Контент карточки.
+
Контент карточки.
+
+ + +``` + +## Что НЕ юзать + +- Bootstrap / Material UI / Chakra / Ant Design — слишком тяжело +- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG +- jQuery — vanilla JS достаточно + +## Что МОЖНО (если очень надо) + +- **Tailwind через CDN**: `` +- **Placeholder картинки**: `https://picsum.photos/800/600` +- **Google Fonts через ``** в head + +## 📦 Готовые рецепты (используй их, не изобретай) + +### Фото без ключей +- Детерминированные (одно и то же слово → одна и та же картинка): `https://picsum.photos/seed/coffee/800/600`. Меняй `coffee` на любое слово-seed. +- Тематические (по тегу): `https://loremflickr.com/800/600/coffee` — тег прямо в URL (`/coffee`, `/mountains`, `/food`). Тема не гарантируется на 100%, но обычно в кассу. + +### Кнопка WhatsApp +```html +Написать в WhatsApp +``` +`7XXXXXXXXXX` — номер в межд. формате без `+`. `text=` — предзаполненное сообщение. + +### Форма заявки без бэкенда +```html +
+ + + +
+``` +Скажи юзеру: замени `EMAIL` на свою почту — заявки будут приходить туда (первая заявка активирует, письмо-подтверждение придёт один раз). + +### Google Fonts (максимум 2 шрифта: один display + один text) +```html + + +``` +В CSS: `font-family:'Unbounded',sans-serif` для заголовков, `'Manrope',sans-serif` для текста. + +### Favicon-эмодзи (inline SVG data-URI, без файла) +```html + +``` +Меняй эмодзи `☕` на подходящее теме. + +### Карта (Google Maps embed, без ключа) +На maps.google.com найди место → «Поделиться» → «Встроить карту» → скопируй iframe. Вставь как есть: +```html + +``` + +### Плавные появления при скролле (IntersectionObserver) +```html + + +``` +Добавь класс `reveal` любой секции/карточке — появится плавно при прокрутке. diff --git a/index.html b/index.html new file mode 100644 index 0000000..6d13758 --- /dev/null +++ b/index.html @@ -0,0 +1,174 @@ + + + + + + Заявки на подключение — Алматы + + + +
+
+

Заявки на подключение

+

Отдел продаж филиала Алматы

+
+
+ + + +
+
+ +
+
+
+
0
+
Новые
+
+
+
0
+
В работе
+
+
+
0
+
Дубли
+
+
+
0
+
Просрочка
+
+
+
0
+
Всего
+
+
+ +
+ +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + +
ДатаФИОТелефонАдресТарифМенеджерСтатусДействия
+
+
+ + + + + + + + + + diff --git a/passport.md b/passport.md new file mode 100644 index 0000000..35f0b54 --- /dev/null +++ b/passport.md @@ -0,0 +1,158 @@ +# ПАСПОРТ ИИ-АГЕНТА + +| Поле | Значение | +|---|---| +| Наименование | ИИ-агент для заявок на подключение | +| Категория | Высокая (ПДн клиентов, цена ошибки – потеря заявки, двойной звонок, искажённый отчёт) | +| Сложность | Высокая (поток десятков заявок в день, общий реестр, построчная проверка, несколько ролей, внутренние источники) | +| Исполнитель | Департамент AI (ДЦБ) АО «Казахтелеком» | +| Заказчик / Владелец | Ержан Асанов, Дивизион по трансформации, отдел продаж филиала Алматы, @erjan_a, +7 701 555 3311 | +| Дата внедрения | – | +| Пользователь | Менеджер отдела продаж, старший менеджер, руководитель отдела | + +## 1. Предпосылки +- В отделе продаж заявки на подключение теряются между Lotus-почтой и Excel, а клиент может ждать взятия в работу полдня вместо нормы 30 минут. +- Ручной поток создаёт дубли, повторные звонки клиенту и искажённую картину для руководителя. +- Один общий Excel-файл на сетевом диске создаёт ожидание, потому что одновременно работать с ним неудобно. +- Статусы обновляются с задержкой 2-3 дня, поэтому управление идёт по устаревшей картине. +- В пиковые периоды, когда заявок вдвое больше, процесс разваливается сильнее всего. +- Половина менеджеров возрастные, поэтому интерфейс должен быть проще WhatsApp, иначе решение не взлетит. + +## 2. Входные данные и источники +- Lotus-почта `sales.almaty@kt.kz` – источник входящих заявок с сайта, письма приходят в общий ящик. +- Excel-файл `zayavki_2026.xlsx` – рабочий реестр заявок, используется как общий файл команды. +- Сетевой диск `\\ktfs01\sales\заявки` – точка доступа к Excel-файлу. +- Поля заявки: ФИО, адрес, тариф, статус, менеджер, дата. +- В заявках есть ПДн: ФИО, адреса, телефоны клиентов. +- Коммерческой тайны в этом процессе нет, но наружу данные выносить нельзя – только внутренний контур КТ. +- API у Lotus и Excel не подтверждено – уточнит Динара / ИТ-сопровождение. +- Точная точка входа рабочего экрана для менеджеров не подтверждена – уточнит Департамент AI (ДЦБ) при проектировании панели. + +## 3. Процессы / функции для внедрения ИИ +- Приём заявки из Lotus или из ручного ввода после звонка клиента. +- Первичная регистрация заявки в рабочем списке. +- Поиск дублей по ФИО, адресу, телефону и другим доступным признакам. +- Выделение спорных случаев для подтверждения человеком. +- Обновление статуса заявки в общем реестре. +- Формирование сводки для руководителя в Telegram. +- Подготовка Excel-выгрузки как привычного дополнительного формата. + +## 4. Шаги процесса +- Триггер: заявка приходит из общего Lotus-ящика или менеджер записывает её после звонка клиента. +- Агент забирает заявку и проверяет, есть ли она уже в потоке. +- Агент сопоставляет данные и ищет возможный дубль. +- Если совпадение уверенное, заявка идёт в рабочий список. +- Если совпадение спорное, агент отдаёт заявку менеджеру на подтверждение. +- Менеджер подтверждает, отклоняет или дорабатывает спорную заявку. +- Агент обновляет статус в общем реестре и готовит сводку. +- Руководитель получает автоматическую сводку в Telegram. +- Финальный результат – заявка в работе и актуальный статус в реестре. + +## 5. Результат работы агента +- Отдельный экран со списком заявок и статусами для менеджеров. +- Автоматическая сводка в Telegram для Ержана. +- Excel-выгрузка как дополнительный привычный формат. +- Статусы: новая, в работе, на проверке, спорная, просрочка. +- Критерий качества результата – заявка должна попадать в работу не позже чем через 30 минут. +- Критерий качества результата – доля дублей должна удерживаться в пределах пары процентов. + +## 6. Ожидаемый эффект +- Снижение трудозатрат на 25-35 часов в неделю за счёт приёма из почты, первичной регистрации и поиска дублей. +- Сокращение личной ручной нагрузки Ержана примерно на 1 час в день. +- Снижение потерь заявок из почты и спама. +- Снижение повторных звонков клиентам. +- Ускорение взятия заявки в работу с полудня до нормы 30 минут. +- Повышение актуальности статусов в день поступления. + +## 7. Экономический эффект +- Оценка экономии в часах подтверждена: 25-35 часов в неделю. +- Полный эффект в тенге не считался. +- Источник расчёта эффекта – объём 212 заявок за прошлую неделю, 10-15 минут ручной работы на заявку и подтверждение владельца. +- Дополнительный эффект – почти полное снятие личного часа Ержана на ежедневный отчёт. + +## 8. Контроль и риски +- Агент не должен сам принимать спорные дубли без подтверждения человека. +- Агент не должен выносить ПДн наружу. +- Ошибка агента может привести к двойному звонку клиенту. +- Ошибка агента может привести к потере заявки. +- Ошибка агента может исказить отчёт наверх. +- В пиковые дни риск возрастает, потому что поток удваивается. +- При разном написании фамилии дубль может быть не распознан без дополнительной проверки. +- Интерфейс должен быть максимально простым, иначе менеджеры не будут им пользоваться. + +## 9. Меры по снижению рисков +- Все спорные дубли передаются менеджеру на подтверждение. +- Все данные остаются во внутреннем контуре КТ. +- Для менеджеров нужен отдельный экран с построчной работой, а не сложный интерфейс. +- Для руководителя нужна автоматическая сводка, чтобы убрать ручной сбор. +- Дубли допустимы только в пределах пары процентов. +- Статусы должны обновляться в день поступления, чтобы не копить устаревшие записи. + +## 10. Рекомендуемый формат реализации +- Маршрут: `Alem + веб-панель` – отдельный экран нужен менеджерам, а руководителю нужна автоматическая сводка в Telegram; процесс построчный, с общим состоянием и несколькими ролями. +- Почему подходит: есть поток десятков заявок в день, работа идёт по каждой заявке отдельно, и менеджерам нужен рабочий экран, а не только чат или сводка. +- Что делает Alem: агент читает Lotus и Excel, ищет дубли, формирует карточки, отдаёт спорные случаи на подтверждение и готовит сводку. +- Что вне Alem / требует подтверждения: точная точка входа панели, доступы к источникам, правила пометки спорных случаев и интеграционные детали. +- Не обещать запуск: маршрут – рекомендация; до production нужны подтверждения доступов, ИБ, интеграций и бюджета; финальное решение – Департамент AI (ДЦБ). + +### Карточка агента Alem (черновик) +- Название: ИИ-агент для заявок на подключение. +- Тип агента: agent_with_approval. +- Суть системного промпта: забирай заявки из Lotus и Excel, ищи дубли, помечай спорные случаи, обновляй статусы, готовь сводку для руководителя, не выноси ПДн наружу. +- Базы знаний: регламент обработки заявок, правила дублей, шаблоны статусов, примеры рабочих заявок, список полей ФИО / адрес / тариф / статус / менеджер / дата. +- Инструменты / MCP: Lotus-почта, Excel-реестр на `\\ktfs01\sales\заявки`, Telegram-уведомления, рабочий экран / панель. +- Примеры диалога: + - «Покажи новые заявки за сегодня». + - «Какие заявки спорные и требуют подтверждения?» + - «Сформируй утреннюю сводку для Ержана». +- Правила draft-подтверждения: + - Спорные дубли не подтверждать автоматически. + - Любое действие с внешним эффектом – только после подтверждения человека. + - ПДн не выводить наружу. +- Как протестировать: + - Заявка должна попасть в работу не позже 30 минут. + - Доля дублей должна оставаться в пределах пары процентов. + - Спорные случаи должны уходить человеку. + - Сводка должна показывать новые, зависшие и дубли. + +## 11. Метрики успешности проекта и ожидаемый эффект +- Время от поступления заявки до взятия в работу – не более 30 минут. +- Доля дублей – в пределах пары процентов. +- Снижение ручной нагрузки – 25-35 часов в неделю. +- Статусы обновляются в день поступления. +- Потери заявок из почты и спама должны уйти. +- Ержан должен получать автоматическую сводку без ручного сбора. +- Личная нагрузка на отчёт – около 1 часа в день – должна почти исчезнуть. + +## 12. Риски, SLA и ограничения +- ПДн есть: ФИО, адреса, телефоны клиентов, поэтому решение должно оставаться во внутреннем контуре КТ. +- Цена ошибки – клиент может получить два звонка, заявка может потеряться, отчёт наверх может исказиться. +- SLA по скорости есть: заявка должна быть взята в работу максимум за 30 минут. +- В акции объём удваивается, и именно тогда процесс разваливается. +- Половина менеджеров возрастные, поэтому интерфейс должен быть проще WhatsApp. +- Письма с сайта могут попадать в спам общего ящика. +- **Оценка аналитика: возможно более простое решение** + - Слишком простой вариант без рабочего экрана не подходит: у менеджеров поток заявок построчный и общий статус должен быть виден в работе, а не только в сводке. + +## 13. Передача в работу +- Access contacts: + - Ержан Асанов, @erjan_a, +7 701 555 3311. +- IT contacts: + - Динара из Excel – поднимет точную цифру по прошлой неделе. +- Approvers: + - Руководитель отдела продаж. +- Previous automation attempts: + - не подтверждено – уточнит Ержан или Серик. +- Рабочие источники: + - Lotus-ящик `sales.almaty@kt.kz`. + - Excel-файл `zayavki_2026.xlsx`. + - Сетевой диск `\\ktfs01\sales\заявки`. + +## 14. Что осталось уточнить +- Точная цифра заявок за прошлую неделю – уточнит Динара. +- API или иные точки интеграции у Lotus и Excel – уточнит ИТ-сопровождение. +- Прежние попытки автоматизации – не подтверждено – уточнит Ержан или Серик. +- Кто именно будет принимать работу по решению – уточнит руководитель отдела продаж. +- Точный допустимый процент дублей в числах – уточнит Ержан, если потребуется формализация. +- Детали маршрута передачи спорных случаев – уточнит Серик. +- Дата старта проекта – не подтверждено – уточнит Департамент AI (ДЦБ). \ No newline at end of file diff --git a/scheme.md b/scheme.md new file mode 100644 index 0000000..9b47614 --- /dev/null +++ b/scheme.md @@ -0,0 +1,45 @@ +```mermaid +flowchart LR + subgraph init["Инициатор / клиент"] + start(["Заявка с сайта или звонок"]) + end + + subgraph ai["ИИ-агент"] + a1["Забрать заявку"] + a2["Проверить дубли"] + a3["Сформировать карточку"] + a4["Подготовить сводку"] + end + + subgraph human["Владелец / менеджер"] + h1{"Дубль или спорный случай?"} + h2["Подтвердить или отклонить"] + h3["Работать по заявке"] + h4["Получить сводку"] + end + + subgraph sys["Системы КТ"] + s1["Lotus sales.almaty@kt.kz"] + s2["Excel zayavki_2026.xlsx"] + s3["\\ktfs01\\sales\\заявки"] + s4["Telegram Ержана"] + end + + start --> s1 + s1 --> a1 + a1 --> a2 + a2 --> h1 + h1 -- "да" --> h2 + h1 -- "нет" --> h3 + h2 --> h3 + a2 --> a3 + a3 --> s2 + a3 --> s3 + a2 --> a4 + a4 --> s4 + h3 --> s2 + h3 --> s3 + s2 --> h4 + s3 --> h4 + h4 --> final(["Статус обновлён"]) +``` diff --git a/script.js b/script.js new file mode 100644 index 0000000..af65ee0 --- /dev/null +++ b/script.js @@ -0,0 +1,367 @@ +// Демо-данные: ~30 заявок как в брифе (30-50 в день, пик до 100) +const DEMO_DATA = [ + { id: 1, name: "Алиев Арман Берикович", phone: "+7 777 123 45 67", address: "г. Алматы, ул. Абая 10, кв. 5", tariff: "Домашний интернет 100 Мбит/с", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false }, + { id: 2, name: "Байкова Гульнара Талгатовна", phone: "+7 701 234 56 78", address: "г. Алматы, пр. Достык 45, офис 12", tariff: "ТВ + Интернет", manager: "Айгуль К.", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 3, name: "Васильченко Игорь Петрович", phone: "+7 778 345 67 89", address: "г. Алматы, ул. Толе би 88, кв. 42", tariff: "Домашний интернет 500 Мбит/с", manager: "", status: "новая", comment: "Клиент просит перезвонить после 18:00", date: "2026-08-28", isDuplicate: false }, + { id: 4, name: "Гребенюк Сергей Владимирович", phone: "+7 702 456 78 90", address: "г. Алматы, мкр. Аксай-2, д. 15", tariff: "Тройной пакет", manager: "Серик Ж.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false }, + { id: 5, name: "Дюсенова Айнур Маратовна", phone: "+7 777 567 89 01", address: "г. Алматы, ул. Сатпаева 22, кв. 18", tariff: "Домашний интернет 50 Мбит/с", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false }, + { id: 6, name: "Ержанов Максат", phone: "+7 701 678 90 12", address: "г. Алматы, ул. Рыскулова 100, кв. 55", tariff: "Бизнес тариф", manager: "", status: "спорная", comment: "Нужно уточнить юридический адрес", date: "2026-08-25", isDuplicate: false }, + { id: 7, name: "Жуманова Лейла Ахметовна", phone: "+7 778 789 01 23", address: "г. Алматы, пр. Абая 150, кв. 88", tariff: "Домашний интернет 100 Мбит/с", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-24", isDuplicate: false }, + { id: 8, name: "Зинченко Александр", phone: "+7 702 890 12 34", address: "г. Алматы, ул. Макатаева 35, офис 5", tariff: "ТВ + Интернет", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 9, name: "Ибраева Сауле", phone: "+7 777 901 23 45", address: "г. Алматы, мкр. Коктем-3, д. 20, кв. 10", tariff: "Домашний интернет 500 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 10, name: "Ким Дмитрий Юрьевич", phone: "+7 701 012 34 56", address: "г. Алматы, ул. Бейбитшилик 12, кв. 30", tariff: "Домашний интернет 100 Мбит/с", manager: "Бауыржан Т.", status: "на проверке", comment: "Клиент на больничном", date: "2026-08-26", isDuplicate: false }, + { id: 11, name: "Лебедева Наталья", phone: "+7 778 123 45 67", address: "г. Алматы, пр. Райымбека 200, кв. 75", tariff: "Тройной пакет", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 12, name: "Мамедов Руслан", phone: "+7 702 234 56 78", address: "г. Алматы, ул. Желтоксан 55, офис 8", tariff: "Бизнес тариф", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false }, + { id: 13, name: "Нургалиева Айжан", phone: "+7 777 345 67 89", address: "г. Алматы, мкр. Таугуль-3, д. 8А, кв. 22", tariff: "Домашний интернет 50 Мбит/с", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-23", isDuplicate: false }, + { id: 14, name: "Омаров Ерлан", phone: "+7 701 456 78 90", address: "г. Алматы, ул. Панфилова 90, кв. 15", tariff: "ТВ + Интернет", manager: "", status: "спорная", comment: "Неправильный адрес, уточняется", date: "2026-08-25", isDuplicate: false }, + { id: 15, name: "Петрова Елена Сергеевна", phone: "+7 778 567 89 01", address: "г. Алматы, пр. Абая 50, кв. 100", tariff: "Домашний интернет 500 Мбит/с", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 16, name: "Рахимов Берик", phone: "+7 702 678 90 12", address: "г. Алматы, ул. Кабанбай батыра 70, кв. 45", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 17, name: "Смирнова Ольга", phone: "+7 777 789 01 23", address: "г. Алматы, мкр. Орбита-2, д. 55, кв. 8", tariff: "Тройной пакет", manager: "Серик Ж.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false }, + { id: 18, name: "Турсунов Марат", phone: "+7 701 890 12 34", address: "г. Алматы, ул. Пушкина 25, офис 12", tariff: "Бизнес тариф", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-22", isDuplicate: false }, + { id: 19, name: "Уалиева Жанар", phone: "+7 778 901 23 45", address: "г. Алматы, пр. Достык 100, кв. 60", tariff: "Домашний интернет 50 Мбит/с", manager: "", status: "новая", comment: "Заявка с сайта", date: "2026-08-28", isDuplicate: false }, + { id: 20, name: "Федоров Виктор", phone: "+7 702 012 34 56", address: "г. Алматы, ул. Богенбай батыра 40, кв. 20", tariff: "ТВ + Интернет", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false }, + { id: 21, name: "Хасанова Мадина", phone: "+7 777 123 45 67", address: "г. Алматы, мкр. Нуркент, д. 12, кв. 35", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "спорная", comment: "Дубль? Тот же телефон что у Алиева А.", date: "2026-08-27", isDuplicate: true }, + { id: 22, name: "Иванов Петр", phone: "+7 701 333 22 11", address: "г. Алматы, ул. Тимирязева 80, кв. 50", tariff: "Домашний интернет 500 Мбит/с", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 23, name: "Козлов Андрей", phone: "+7 778 444 33 22", address: "г. Алматы, пр. Абая 200, кв. 120", tariff: "Бизнес тариф", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 24, name: "Алиев Арман Берикович", phone: "+7 777 123 45 67", address: "г. Алматы, ул. Абая 10, кв. 5", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "спорная", comment: "ВОЗМОЖНЫЙ ДУБЛЬ - заявка #1", date: "2026-08-28", isDuplicate: true }, + { id: 25, name: "Шарипова Гульмира", phone: "+7 702 555 44 33", address: "г. Алматы, ул. Сейфуллина 150, кв. 70", tariff: "Тройной пакет", manager: "Айгуль К.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false }, + { id: 26, name: "Яковлев Михаил", phone: "+7 777 666 55 44", address: "г. Алматы, мкр. Самал-2, д. 30, кв. 15", tariff: "Домашний интернет 50 Мбит/с", manager: "", status: "новая", comment: "Клиент из рекламы", date: "2026-08-28", isDuplicate: false }, + { id: 27, name: "Есенбаева Айгуль", phone: "+7 701 777 66 55", address: "г. Алматы, ул. Масанчи 85, кв. 40", tariff: "ТВ + Интернет", manager: "Бауыржан Т.", status: "завершена", comment: "", date: "2026-08-21", isDuplicate: false }, + { id: 28, name: "Морозов Игорь", phone: "+7 778 888 77 66", address: "г. Алматы, пр. Райымбека 300, офис 20", tariff: "Бизнес тариф", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false }, + { id: 29, name: "Кенжебаева Сауле", phone: "+7 702 999 88 77", address: "г. Алматы, мкр. Айнабулак, д. 5, кв. 25", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false }, + { id: 30, name: "Ткаченко Владимир", phone: "+7 777 000 99 88", address: "г. Алматы, ул. Курмангазы 60, кв. 90", tariff: "Домашний интернет 500 Мбит/с", manager: "Айгуль К.", status: "просрочка", comment: "Клиент не отвечает", date: "2026-08-20", isDuplicate: false } +]; + +let requests = []; +let currentFilter = 'all'; +let duplicateCheckId = null; + +// Инициализация +function init() { + const stored = localStorage.getItem('uchet_requests'); + if (stored) { + requests = JSON.parse(stored); + } else { + requests = [...DEMO_DATA]; + saveToStorage(); + } + renderDashboard(); + renderTable(); +} + +function saveToStorage() { + localStorage.setItem('uchet_requests', JSON.stringify(requests)); +} + +// Дашборд +function renderDashboard() { + const today = new Date().toISOString().slice(0, 10); + const counts = { + 'новая': 0, + 'в работе': 0, + 'на проверке': 0, + 'спорная': 0, + 'завершена': 0, + 'просрочка': 0, + 'total': requests.length + }; + + requests.forEach(r => { + if (counts[r.status] !== undefined) counts[r.status]++; + // Просрочка: старше 3 дней и не завершена + if (r.status !== 'завершена' && r.status !== 'просрочка') { + const daysOld = daysBetween(r.date, today); + if (daysOld > 3) { + counts['просрочка']++; + } + } + }); + + document.getElementById('count-new').textContent = counts['новая']; + document.getElementById('count-work').textContent = counts['в работе']; + document.getElementById('count-duplicate').textContent = requests.filter(r => r.isDuplicate).length; + document.getElementById('count-overdue').textContent = counts['просрочка']; + document.getElementById('count-total').textContent = counts.total; +} + +function daysBetween(date1, date2) { + const d1 = new Date(date1); + const d2 = new Date(date2); + return Math.round((d2 - d1) / (1000 * 60 * 60 * 24)); +} + +// Фильтры +function setFilter(status) { + currentFilter = status; + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.status === status); + }); + renderTable(); +} + +// Таблица +function renderTable() { + const tbody = document.getElementById('table-body'); + const search = document.getElementById('search').value.toLowerCase(); + + let filtered = requests.filter(r => { + const matchesSearch = r.name.toLowerCase().includes(search) || + r.phone.toLowerCase().includes(search) || + r.address.toLowerCase().includes(search) || + (r.manager && r.manager.toLowerCase().includes(search)); + const matchesStatus = currentFilter === 'all' || r.status === currentFilter; + return matchesSearch && matchesStatus; + }); + + // Сортировка: новые сверху + filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); + + if (filtered.length === 0) { + tbody.innerHTML = ` + + +
📭
+

Нет заявок по выбранным фильтрам

+ + + `; + return; + } + + tbody.innerHTML = filtered.map(r => ` + + ${formatDate(r.date)} + + ${escapeHtml(r.name)} + ${r.isDuplicate ? 'ДУБЛЬ' : ''} + + ${escapeHtml(r.phone)} + ${escapeHtml(r.address)} + ${escapeHtml(r.tariff)} + ${r.manager || '—'} + ${r.status} + + + ${r.status === 'новая' ? `` : ''} + ${!r.isDuplicate && r.status !== 'завершена' ? `` : ''} + + + `).join(''); +} + +function formatDate(dateStr) { + const [y, m, d] = dateStr.split('-'); + return `${d}.${m}.${y}`; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Модальное окно +function openModal() { + document.getElementById('modal-title').textContent = 'Новая заявка'; + document.getElementById('request-form').reset(); + document.getElementById('req-id').value = ''; + document.getElementById('modal').classList.add('active'); +} + +function closeModal() { + document.getElementById('modal').classList.remove('active'); +} + +function editRequest(id) { + const r = requests.find(x => x.id === id); + if (!r) return; + + document.getElementById('modal-title').textContent = 'Редактирование заявки'; + document.getElementById('req-id').value = r.id; + document.getElementById('req-name').value = r.name; + document.getElementById('req-phone').value = r.phone; + document.getElementById('req-address').value = r.address; + document.getElementById('req-tariff').value = r.tariff; + document.getElementById('req-manager').value = r.manager || ''; + document.getElementById('req-status').value = r.status; + document.getElementById('req-comment').value = r.comment || ''; + document.getElementById('modal').classList.add('active'); +} + +function saveRequest(e) { + e.preventDefault(); + const id = document.getElementById('req-id').value; + + const data = { + name: document.getElementById('req-name').value.trim(), + phone: document.getElementById('req-phone').value.trim(), + address: document.getElementById('req-address').value.trim(), + tariff: document.getElementById('req-tariff').value, + manager: document.getElementById('req-manager').value.trim(), + status: document.getElementById('req-status').value, + comment: document.getElementById('req-comment').value.trim(), + date: new Date().toISOString().slice(0, 10) + }; + + if (id) { + const idx = requests.findIndex(r => r.id == id); + if (idx !== -1) { + requests[idx] = { ...requests[idx], ...data }; + } + } else { + const newId = Math.max(...requests.map(r => r.id), 0) + 1; + requests.unshift({ id: newId, ...data, isDuplicate: false }); + } + + saveToStorage(); + closeModal(); + renderDashboard(); + renderTable(); +} + +// Действия +function takeToWork(id) { + const r = requests.find(x => x.id === id); + if (r) { + r.status = 'в работе'; + if (!r.manager) r.manager = prompt('Введите ФИО менеджера:'); + saveToStorage(); + renderDashboard(); + renderTable(); + } +} + +function checkDuplicate(id) { + const r = requests.find(x => x.id === id); + if (!r) return; + + // Ищем дубли по телефону или ФИО + const duplicates = requests.filter(x => + x.id !== id && + (x.phone === r.phone || normalizeName(x.name) === normalizeName(r.name)) + ); + + if (duplicates.length > 0) { + duplicateCheckId = id; + const dup = duplicates[0]; + document.getElementById('duplicate-content').innerHTML = ` +

Найдено совпадение с заявкой #${dup.id}:

+
+ ${escapeHtml(dup.name)}
+ Тел: ${escapeHtml(dup.phone)}
+ Адрес: ${escapeHtml(dup.address)}
+ Статус: ${dup.status}
+ Дата: ${formatDate(dup.date)} +
+

Пометить текущую заявку как дубль?

+ `; + document.getElementById('duplicate-modal').classList.add('active'); + } else { + alert('Дубли не найдены'); + } +} + +function normalizeName(name) { + return name.toLowerCase().replace(/[^а-яёa-z]/g, '').trim(); +} + +function confirmDuplicate() { + if (duplicateCheckId) { + const r = requests.find(x => x.id === duplicateCheckId); + if (r) { + r.isDuplicate = true; + r.status = 'спорная'; + r.comment = (r.comment || '') + ' Помечена как дубль вручную.'; + saveToStorage(); + renderDashboard(); + renderTable(); + } + closeDuplicateModal(); + } +} + +function keepBoth() { + closeDuplicateModal(); +} + +function closeDuplicateModal() { + document.getElementById('duplicate-modal').classList.remove('active'); + duplicateCheckId = null; +} + +// Telegram сводка +function showTelegramSummary() { + const today = new Date().toISOString().slice(0, 10); + const counts = { + 'новая': requests.filter(r => r.status === 'новая').length, + 'в работе': requests.filter(r => r.status === 'в работе').length, + 'на проверке': requests.filter(r => r.status === 'на проверке').length, + 'спорная': requests.filter(r => r.status === 'спорная').length, + 'завершена': requests.filter(r => r.status === 'завершена').length, + 'дубли': requests.filter(r => r.isDuplicate).length + }; + + const managers = {}; + requests.filter(r => r.manager && r.status !== 'завершена').forEach(r => { + managers[r.manager] = (managers[r.manager] || 0) + 1; + }); + + const text = `📊 *Сводка по заявкам на ${formatDate(today)}* + +🔹 Новые: ${counts['новая']} +🔹 В работе: ${counts['в работе']} +🔹 На проверке: ${counts['на проверке']} +🔹 Спорные: ${counts['спорная']} +🔹 Дубли: ${counts['дубли']} +🔹 Завершено: ${counts['завершена']} + +📈 *Всего в работе:* ${counts['новая'] + counts['в работе'] + counts['на проверке'] + counts['спорная']} + +👥 *По менеджерам:* +${Object.entries(managers).map(([m, c]) => `${m}: ${c} заяв.`).join('\n') || '—'} + +⚠️ *Просрочка:* ${requests.filter(r => r.status === 'просрочка').length} заявок старше 3 дней`; + + document.getElementById('telegram-content').textContent = text; + document.getElementById('telegram-modal').classList.add('active'); +} + +function closeTelegramModal() { + document.getElementById('telegram-modal').classList.remove('active'); +} + +function copyTelegram() { + const text = document.getElementById('telegram-content').textContent; + navigator.clipboard.writeText(text).then(() => { + alert('Сводка скопирована в буфер обмена!'); + }); +} + +// Экспорт в Excel (CSV с BOM) +function exportToExcel() { + const headers = ['ID', 'Дата', 'ФИО', 'Телефон', 'Адрес', 'Тариф', 'Менеджер', 'Статус', 'Комментарий', 'Дубль']; + const rows = requests.map(r => [ + r.id, + r.date, + `"${r.name.replace(/"/g, '""')}"`, + `"${r.phone.replace(/"/g, '""')}"`, + `"${r.address.replace(/"/g, '""')}"`, + `"${r.tariff.replace(/"/g, '""')}"`, + `"${(r.manager || '').replace(/"/g, '""')}"`, + r.status, + `"${(r.comment || '').replace(/"/g, '""')}"`, + r.isDuplicate ? 'Да' : 'Нет' + ]); + + const csv = '\uFEFF' + [headers.join(';'), ...rows.map(r => r.join(';'))].join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `zayavki_${new Date().toISOString().slice(0, 10)}.csv`; + a.click(); + URL.revokeObjectURL(url); +} + +// Старт +init(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..7a1d431 --- /dev/null +++ b/style.css @@ -0,0 +1,423 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: #f5f7fa; + color: #1a1a2e; + line-height: 1.6; +} + +.header { + background: linear-gradient(135deg, #0066cc 0%, #004499 100%); + color: white; + padding: 24px 32px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); +} + +.header-left h1 { + font-size: 28px; + font-weight: 600; + margin-bottom: 4px; +} + +.subtitle { + font-size: 14px; + opacity: 0.85; +} + +.header-right { + display: flex; + gap: 12px; +} + +.btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.btn-primary { + background: #00cc88; + color: white; +} + +.btn-primary:hover { + background: #00aa77; +} + +.btn-secondary { + background: rgba(255,255,255,0.15); + color: white; + border: 1px solid rgba(255,255,255,0.3); +} + +.btn-secondary:hover { + background: rgba(255,255,255,0.25); +} + +.btn-warning { + background: #ff9800; + color: white; +} + +.btn-warning:hover { + background: #f57c00; +} + +.btn-sm { + padding: 6px 12px; + font-size: 13px; +} + +.main { + padding: 24px 32px; + max-width: 1400px; + margin: 0 auto; +} + +.dashboard { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: white; + border-radius: 12px; + padding: 20px; + text-align: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: transform 0.2s; +} + +.stat-card:hover { + transform: translateY(-2px); +} + +.stat-value { + font-size: 36px; + font-weight: 700; + margin-bottom: 4px; +} + +.stat-label { + font-size: 13px; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.stat-new .stat-value { color: #2196f3; } +.stat-work .stat-value { color: #ff9800; } +.stat-duplicate .stat-value { color: #f44336; } +.stat-overdue .stat-value { color: #9c27b0; } +.stat-total .stat-value { color: #00cc88; } + +.filters { + background: white; + border-radius: 12px; + padding: 16px 20px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.search-box { + flex: 1; + min-width: 280px; +} + +.search-box input { + width: 100%; + padding: 10px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 14px; + transition: border-color 0.2s; +} + +.search-box input:focus { + outline: none; + border-color: #0066cc; +} + +.status-filters { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.filter-btn { + padding: 8px 16px; + border: 1px solid #e0e0e0; + background: white; + border-radius: 20px; + font-size: 13px; + cursor: pointer; + transition: all 0.2s; +} + +.filter-btn:hover { + background: #f5f7fa; +} + +.filter-btn.active { + background: #0066cc; + color: white; + border-color: #0066cc; +} + +.table-section { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + overflow: hidden; +} + +.requests-table { + width: 100%; + border-collapse: collapse; +} + +.requests-table th, +.requests-table td { + padding: 14px 16px; + text-align: left; + border-bottom: 1px solid #f0f0f0; +} + +.requests-table th { + background: #f8f9fa; + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #555; +} + +.requests-table tbody tr:hover { + background: #f8f9fa; +} + +.status-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-новая { background: #e3f2fd; color: #1976d2; } +.status-в работе { background: #fff3e0; color: #f57c00; } +.status-на проверке { background: #f3e5f5; color: #7b1fa2; } +.status-спорная { background: #ffebee; color: #d32f2f; } +.status-завершена { background: #e8f5e9; color: #388e3c; } + +.duplicate-flag { + background: #ffebee; + color: #d32f2f; + font-size: 11px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 6px; +} + +.actions { + display: flex; + gap: 6px; +} + +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: white; + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); +} + +.modal-small { + max-width: 450px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #f0f0f0; +} + +.modal-header h2 { + font-size: 20px; + font-weight: 600; +} + +.close-btn { + background: none; + border: none; + font-size: 28px; + cursor: pointer; + color: #999; + line-height: 1; +} + +.close-btn:hover { + color: #333; +} + +#request-form { + padding: 24px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + font-size: 13px; + font-weight: 500; + color: #555; + margin-bottom: 6px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px 14px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 14px; + font-family: inherit; + transition: border-color 0.2s; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #0066cc; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; +} + +.telegram-content { + padding: 24px; + background: #f5f7fa; + font-family: monospace; + font-size: 13px; + line-height: 1.8; + white-space: pre-wrap; +} + +.duplicate-content { + padding: 24px; +} + +.duplicate-content p { + margin-bottom: 12px; +} + +.duplicate-card { + background: #fff3e0; + border: 1px solid #ffe0b2; + border-radius: 8px; + padding: 12px; + margin-bottom: 12px; +} + +.duplicate-card strong { + color: #f57c00; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #999; +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + gap: 16px; + padding: 20px; + } + + .header-right { + flex-wrap: wrap; + justify-content: center; + } + + .main { + padding: 16px; + } + + .form-row { + grid-template-columns: 1fr; + } + + .requests-table { + font-size: 13px; + } + + .requests-table th, + .requests-table td { + padding: 10px 12px; + } +} diff --git a/ui_concept.json b/ui_concept.json new file mode 100644 index 0000000..7e5393a --- /dev/null +++ b/ui_concept.json @@ -0,0 +1 @@ +{"archetype":"queue","product":{"name":"Заявки на подключение","purpose":"Очередь приёма и проверки дублей","entity":"заявка","createLabel":"Новая заявка"},"user":{"name":"Серик Жумабаев","role":"Старший менеджер"},"nav":[{"label":"Очередь","icon":"home"},{"label":"Проблемные","icon":"risk"},{"label":"Реестр","view":"registry","icon":"file"}],"kpis":[{"value":"30 мин вместо 2-3 дн.","label":"Взятие в работу"},{"value":"212","label":"За неделю"},{"value":"7","label":"Спорных"},{"value":"4","label":"Дубля"}],"scenario":{"headline":"Очередь заявок","control":"Подтвердить","confirmText":"Заявка уйдёт в рабочий список и статус обновится.","reject":"Отклонить","approveTo":"done","rejectTo":"manual","actions":["Подтвердить","Отклонить","Доработать"]},"statuses":{"done":{"label":"В работе","tone":"ok"},"manual":{"label":"На проверке","tone":"warn"},"attention":{"label":"Спорная","tone":"warn"},"overdue":{"label":"Просрочка","tone":"risk"},"new":{"label":"Новая","tone":"info"}},"banner":{"text":"7 заявок требуют подтверждения сегодня","tone":"warn","action":"Открыть проблемные","actionNav":1},"table":{"columns":[{"key":"request","label":"Заявка"},{"key":"source","label":"Источник","sub":"channel"},{"key":"client","label":"Клиент"},{"key":"tariff","label":"Тариф"},{"key":"date","label":"Дата","type":"date"},{"key":"status","label":"Статус","type":"status"}],"rows":[{"request":"З-26/041","source":"Lotus","channel":"сайт","client":"Айгуль Сейдахметова","tariff":"Базовый 300","date":"2026-07-01","status":"attention","_flags":["Рекомендация: Доработать","Совпадает по телефону с заявкой З-26/018","ФИО в письме написано иначе, нужен человек на подтверждение"]},{"request":"З-26/042","source":"Lotus","channel":"звонок","client":"Данияр Кенжебаев","tariff":"Оптика 500","date":"2026-07-01","status":"done","_flags":["Рекомендация: Подтвердить","Дубль не найден по ФИО, адресу и телефону","Карточка готова к работе"]},{"request":"З-26/043","source":"Lotus","channel":"сайт","client":"Гульмира Нурланова","tariff":"Базовый 300","date":"2026-07-01","status":"manual","_flags":["Рекомендация: Подтвердить","Письмо пришло из спама общего ящика","Агент не уверен в совпадении адреса"]},{"request":"З-26/044","source":"Excel","channel":"звонок","client":"Ермек Тлеубергенов","tariff":"Оптика 500","date":"2026-07-01","status":"overdue","_flags":["Рекомендация: Отклонить","Статус не обновлялся 3 дня","Нужна проверка менеджером"]},{"request":"З-26/045","source":"Lotus","channel":"сайт","client":"Алина Омарова","tariff":"Базовый 300","date":"2026-07-01","status":"new","_flags":["Рекомендация: Подтвердить","Новая заявка из общего ящика","Карточка создана автоматически"]},{"request":"З-26/046","source":"Excel","channel":"сайт","client":"Руслан Ахметов","tariff":"Оптика 500","date":"2026-07-01","status":"attention","_flags":["Рекомендация: Доработать","Разное написание фамилии в источниках","Нужна ручная сверка"]},{"request":"З-26/047","source":"Lotus","channel":"звонок","client":"Мадина Сарсенова","tariff":"Базовый 300","date":"2026-07-01","status":"manual","_flags":["Рекомендация: Подтвердить","В потоке есть похожая заявка от сайта","Спорный случай передан человеку"]}]},"drawer":{"titleKey":"request","fields":[{"key":"source","label":"Источник"},{"key":"channel","label":"Канал"},{"key":"client","label":"Клиент"},{"key":"tariff","label":"Тариф"},{"key":"date","label":"Дата"}],"flagsLabel":"Что сделал агент"},"ai":{"intro":"Собираю очередь заявок из Lotus и Excel, отмечаю дубли и спорные случаи, а менеджер подтверждает только сомнительные строки.","suggestions":["Почему З-26/041 спорная?","Какие заявки можно брать сразу?","Что попало в просрочку?"],"answers":{"Почему З-26/041 спорная?":"У заявки З-26/041 совпадает телефон с З-26/018, но ФИО в письме написано иначе. Поэтому карточка идёт на подтверждение менеджеру.","Какие заявки можно брать сразу?":"З-26/042 и З-26/045 уже готовы к работе: дубль не найден или заявка создана автоматически без конфликта.","Что попало в просрочку?":"З-26/044 не обновлялась 3 дня, поэтому агент пометил её как просрочку и вынес в проблемные."}},"layout":"queue","notifications":[]} \ No newline at end of file