From 0f3e811cab0081fbdcccc8647ab28ef4849084df Mon Sep 17 00:00:00 2001 From: samruk_3 Date: Fri, 28 Aug 2026 07:23:29 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 844 ++++++++++++++++++++++++++++++++++++++++++++++++ alem_guide.md | 137 ++++++++ brief.md | 192 +++++++++++ design.md | 142 ++++++++ index.html | 174 ++++++++++ passport.md | 158 +++++++++ scheme.md | 45 +++ script.js | 367 +++++++++++++++++++++ style.css | 423 ++++++++++++++++++++++++ ui_concept.json | 1 + 10 files changed, 2483 insertions(+) create mode 100644 AGENTS.md create mode 100644 alem_guide.md create mode 100644 brief.md create mode 100644 design.md create mode 100644 index.html create mode 100644 passport.md create mode 100644 scheme.md create mode 100644 script.js create mode 100644 style.css create mode 100644 ui_concept.json 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