HR dashboard: companies, tasks, FAQ

This commit is contained in:
Dinara 2026-06-01 05:15:22 +00:00
parent 1f670c6d6e
commit 312cbcf7d9
3 changed files with 996 additions and 0 deletions

173
AGENTS.md Normal file
View File

@ -0,0 +1,173 @@
<!-- vibe42-agents-version: v3-guided-2026-06-01 -->
# Vibe42 — учебная песочница для лендингов
Workspace юзера `Dinara`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт.
---
## 🎯 ТВОЯ РОЛЬ
Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с:
1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/Dinara/<repo>/`,
2. ощущением «это было легко» — без серверов, БД, токенов, конфигов.
Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код.
---
## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет)
1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?»
2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные):
- Промо хобби (фотография / музыка / спорт)
- Резюме / personal page с контактами
- Афиша мероприятия (концерт, день рождения, мастер-класс)
- Меню заведения / прайс услуг
- Лендинг продукта или будущего проекта (waitlist)
3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы).
4. Сразу делай `./new-project <name>` и собирай страницу. Не спрашивай разрешения на каждый шаг.
---
## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО
Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**.
Скажи: «Давай определимся, я задам 3 коротких вопроса:
1. Это для тебя лично, для проекта/бизнеса, или для события?
2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио?
3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?»
После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать.
---
## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ
| Запрос | Что делаем вместо |
|--------|-------------------|
| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram |
| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) |
| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium |
| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp |
| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) |
| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` |
**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут.
---
## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера)
### 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. **Сразу запушь:**
```bash
git add -A
git commit -m "v1"
git push origin HEAD:pages
```
2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**:
> 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/Dinara/<repo>/**
3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?»
4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push.
---
## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда)
1. **Только статика — HTML + CSS + JS в браузере.**
2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis.
3. **Никакой аутентификации / OAuth / JWT.**
4. **Никакого Docker, nginx, sudo, системных настроек.**
5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN.
6. **НИКОГДА `git init` в workspace root (`/workspaces/Dinara`)** — это папка-контейнер юзера, не репозиторий.
---
## ✅ ВСЕГДА работай через `./new-project`
Если юзер сказал «сделай сайт NAME» / «создай проект NAME»:
```bash
cd /workspaces/Dinara
./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/
cd NAME
# теперь создавай index.html / style.css / script.js внутри ./NAME
```
`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`.
---
## 🌐 Git и публикация
**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz**
- Профиль юзера: https://git.vibe42.kz/Dinara
- Pages (живые лендинги): https://pages.git.vibe42.kz/Dinara/<repo>/
- Креды уже в `/workspaces/Dinara/.git-credentials` — git push/clone работают без пароля
- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно
### Опубликовать лендинг
```bash
git add -A
git commit -m "site"
git push origin HEAD:pages
```
Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно.
---
## 🔧 Когда что-то идёт не так
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ Создавать `server.js` / `app.py` / `main.go` как backend
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено
- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз)
- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано
- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML
- ❌ Говорить «это сложно» — переформулируй в простое
- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй
---
## 🎨 design.md
Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие.

110
design.md Normal file
View File

@ -0,0 +1,110 @@
<!-- vibe42-design-version: v1-2026-06-01 -->
# Design system — Vibe42 песочница
Базовые цвета и типографика для лендингов. Можно отклоняться, но начинай с этого.
## Палитра
| Token | Hex | Использование |
|-------|-----|---------------|
| `--ink` | `#0F1218` | Тёмный фон / основной текст |
| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) |
| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов |
| `--white` | `#FFFFFF` | Основной фон |
| `--gray-500` | `#5B6573` | Вторичный текст |
| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны |
## Типографика
```css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
```
| Уровень | Размер | Вес | line-height |
|---------|--------|-----|-------------|
| h1 (hero) | 56px | 800 | 1.05 |
| h2 (section) | 36px | 700 | 1.15 |
| h3 | 22px | 700 | 1.3 |
| body | 17px | 400 | 1.6 |
| small | 14px | 400 | 1.5 |
На мобиле — h1 уменьши до 36px, h2 до 28px.
## Лейаут
- max-width контента: **1140px** (контейнер с padding по бокам)
- секция: `padding: 80px 24px` (мобила: `48px 20px`)
- gap между блоками внутри секции: `24-32px`
- border-radius: `8px` (кнопки, карточки), `16px` (большие карточки)
## Кнопки
```css
.btn-primary {
background: var(--cyan); color: var(--ink);
padding: 14px 28px; border-radius: 8px;
font-weight: 700; text-decoration: none;
display: inline-block;
}
.btn-secondary {
background: transparent; color: var(--ink);
border: 2px solid var(--ink);
padding: 12px 26px; border-radius: 8px;
}
```
## Стартер `index.html`
```html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Мой проект</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white)}
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;margin-bottom:32px}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;text-decoration:none}
.btn:hover{background:#1be5ff}
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
@media (max-width:640px){.hero h1{font-size:36px}.section h2{font-size:28px}.container{padding:48px 20px}}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<h1>Заголовок проекта</h1>
<p>Подзаголовок — пара предложений о чём это.</p>
<a class="btn" href="#section">Начать</a>
</div>
</section>
<section id="section" class="section">
<div class="container">
<h2>Секция</h2>
<div class="card">Контент карточки.</div>
<div class="card">Контент карточки.</div>
</div>
</section>
</body>
</html>
```
## Чем НЕ пользоваться
- Bootstrap, Material UI, Chakra, Ant Design — слишком тяжело и не нужно для лендинга
- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG
- jQuery — vanilla JS более чем достаточно
## Чем МОЖНО (если очень надо)
- **Tailwind через CDN**: `<script src="https://cdn.tailwindcss.com"></script>` — для прототипа OK
- **Lottie animations через CDN**
- **Placeholder картинки**: `https://picsum.photos/800/600`, `https://placehold.co/600x400`
- **Шрифты Google Fonts через `<link>`** в head

713
index.html Normal file
View File

@ -0,0 +1,713 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>HR Ассистент — контроль поручений</title>
<style>
:root {
--ink: #0F1218;
--cyan: #00E5FF;
--cyan-50: #E8FCFF;
--white: #fff;
--gray-500: #5B6573;
--gray-100: #F2F4F7;
--gray-200: #E4E7EC;
--green: #10B981;
--green-bg: #ECFDF5;
--amber: #F59E0B;
--amber-bg: #FFFBEB;
--red: #EF4444;
--red-bg: #FEF2F2;
--indigo: #4F46E5;
--indigo-bg: #EEF2FF;
--radius: 12px;
--shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
--shadow-lg: 0 4px 24px rgba(0,0,0,.08);
}
* { box-sizing: border-box; margin: 0; padding: 0 }
body {
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
color: var(--ink);
background: var(--gray-100);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* NAV */
.nav {
position: sticky; top: 0; z-index: 100;
background: var(--white);
border-bottom: 1px solid var(--gray-200);
backdrop-filter: blur(12px);
}
.nav .container {
display: flex; align-items: center; justify-content: space-between;
height: 60px;
}
.nav-logo {
font-size: 18px; font-weight: 700;
display: flex; align-items: center; gap: 10px;
text-decoration: none; color: var(--ink);
}
.nav-logo .dot {
width: 32px; height: 32px;
background: var(--cyan);
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
}
.nav-links { display: flex; gap: 8px; flex-wrap: wrap }
.nav-links a {
text-decoration: none; color: var(--gray-500);
font-size: 14px; font-weight: 500;
padding: 6px 14px; border-radius: 8px;
transition: all .15s;
}
.nav-links a:hover, .nav-links a.active { background: var(--gray-100); color: var(--ink) }
/* HERO / STATS */
.hero { padding: 48px 0 32px }
.hero h1 { font-size: 36px; font-weight: 800; line-height: 1.2; margin-bottom: 8px }
.hero .sub { color: var(--gray-500); font-size: 17px; margin-bottom: 36px }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px }
.stat-card {
background: var(--white); border-radius: var(--radius);
padding: 24px; box-shadow: var(--shadow);
display: flex; flex-direction: column; gap: 8px;
}
.stat-card .label { font-size: 13px; font-weight: 600; color: var(--gray-500); text-transform: uppercase; letter-spacing: .4px }
.stat-card .value { font-size: 36px; font-weight: 800 }
.stat-card .dot-indicator {
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
}
/* SECTION HEADER */
.section-header {
display: flex; align-items: center; justify-content: space-between;
margin: 56px 0 24px; flex-wrap: wrap; gap: 12px;
}
.section-header h2 { font-size: 24px; font-weight: 700 }
.search-bar {
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
}
.search-bar input {
padding: 10px 16px; border: 1px solid var(--gray-200);
border-radius: 8px; font-size: 14px; outline: none;
min-width: 220px; background: var(--white);
}
.search-bar input:focus { border-color: var(--cyan) }
.search-bar select {
padding: 10px 14px; border: 1px solid var(--gray-200);
border-radius: 8px; font-size: 14px; outline: none;
background: var(--white); cursor: pointer;
}
/* BUTTONS */
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 20px; border-radius: 8px;
font-size: 14px; font-weight: 600; text-decoration: none;
border: none; cursor: pointer;
transition: all .15s;
}
.btn-primary { background: var(--cyan); color: var(--ink) }
.btn-primary:hover { background: #1be5ff }
.btn-outline { background: var(--white); color: var(--ink); border: 1.5px solid var(--gray-200) }
.btn-outline:hover { border-color: var(--gray-500) }
.btn-sm { padding: 6px 14px; font-size: 13px }
.btn-danger { background: var(--red-bg); color: var(--red) }
.btn-danger:hover { background: #FEE2E2 }
/* COMPANIES GRID */
.companies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.company-card {
background: var(--white); border-radius: var(--radius);
padding: 24px; box-shadow: var(--shadow);
transition: box-shadow .2s; cursor: pointer;
border: 2px solid transparent;
position: relative;
}
.company-card:hover { box-shadow: var(--shadow-lg) }
.company-card.selected { border-color: var(--cyan) }
.company-card .company-name {
font-size: 17px; font-weight: 700; margin-bottom: 4px;
display: flex; align-items: center; gap: 8px;
}
.company-card .company-avatar {
width: 40px; height: 40px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 700; color: var(--white);
flex-shrink: 0;
}
.company-avatar.g1 { background: var(--indigo) }
.company-avatar.g2 { background: #0891B2 }
.company-avatar.g3 { background: #D946EF }
.company-avatar.g4 { background: #EA580C }
.company-avatar.g5 { background: #16A34A }
.company-card .task-counts {
display: flex; gap: 12px; margin-top: 12px; font-size: 13px;
}
.company-card .task-counts span { display: flex; align-items: center; gap: 4px }
.company-card .badge {
display: inline-block; padding: 3px 10px; border-radius: 20px;
font-size: 12px; font-weight: 600;
}
.company-card .actions { margin-top: 16px; display: flex; gap: 8px }
/* TASK TABLE */
.task-table-wrap { overflow-x: auto }
.task-table {
width: 100%; border-collapse: collapse;
background: var(--white); border-radius: var(--radius);
box-shadow: var(--shadow); overflow: hidden;
}
.task-table th {
text-align: left; padding: 14px 16px;
font-size: 12px; font-weight: 700; color: var(--gray-500);
text-transform: uppercase; letter-spacing: .5px;
border-bottom: 1px solid var(--gray-200);
background: var(--gray-100);
white-space: nowrap;
}
.task-table td {
padding: 14px 16px; font-size: 14px;
border-bottom: 1px solid var(--gray-100);
white-space: nowrap;
}
.task-table tr:hover td { background: var(--cyan-50) }
.task-table tr:last-child td { border-bottom: none }
.status-pill {
display: inline-block; padding: 4px 12px; border-radius: 20px;
font-size: 12px; font-weight: 600;
}
.status-pending { background: var(--amber-bg); color: var(--amber) }
.status-progress { background: var(--indigo-bg); color: var(--indigo) }
.status-done { background: var(--green-bg); color: var(--green) }
.status-overdue { background: var(--red-bg); color: var(--red) }
.priority-dot {
width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px;
}
.priority-high { background: var(--red) }
.priority-mid { background: var(--amber) }
.priority-low { background: var(--gray-200) }
/* FAQ */
.faq-item {
background: var(--white); border-radius: var(--radius);
box-shadow: var(--shadow); margin-bottom: 8px; overflow: hidden;
}
.faq-q {
padding: 18px 24px; cursor: pointer;
display: flex; align-items: center; justify-content: space-between;
font-weight: 600; font-size: 16px;
user-select: none;
}
.faq-q:hover { background: var(--gray-100) }
.faq-q .arrow { transition: transform .2s; font-size: 12px; color: var(--gray-500) }
.faq-item.open .faq-q .arrow { transform: rotate(180deg) }
.faq-a {
padding: 0 24px 18px; font-size: 15px; color: var(--gray-500);
line-height: 1.7; display: none;
}
.faq-item.open .faq-a { display: block }
/* MODAL */
.modal-overlay {
position: fixed; inset: 0; z-index: 200;
background: rgba(15,18,24,.5);
backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center;
display: none;
}
.modal-overlay.open { display: flex }
.modal {
background: var(--white); border-radius: 16px;
padding: 32px; max-width: 500px; width: 90%;
box-shadow: var(--shadow-lg); max-height: 85vh; overflow-y: auto;
}
.modal h3 { font-size: 20px; margin-bottom: 20px }
.modal label {
display: block; font-size: 13px; font-weight: 600;
color: var(--gray-500); margin-bottom: 4px; margin-top: 16px;
}
.modal input, .modal select, .modal textarea {
width: 100%; padding: 10px 14px; border: 1px solid var(--gray-200);
border-radius: 8px; font-size: 14px; outline: none; font-family: inherit;
}
.modal input:focus, .modal select:focus, .modal textarea:focus { border-color: var(--cyan) }
.modal textarea { resize: vertical; min-height: 80px }
.modal .btn-row { display: flex; gap: 10px; margin-top: 24px; justify-content: flex-end }
/* EMPTY */
.empty { text-align: center; padding: 60px 20px; color: var(--gray-500) }
.empty .icon { font-size: 48px; margin-bottom: 12px }
/* FOOTER */
.footer {
text-align: center; padding: 32px 24px;
color: var(--gray-500); font-size: 13px;
border-top: 1px solid var(--gray-200); margin-top: 64px;
}
/* TABS */
.tabs { display: flex; gap: 4px; margin-bottom: 24px; background: var(--white); border-radius: 10px; padding: 4px; box-shadow: var(--shadow); width: fit-content }
.tab-btn {
padding: 10px 20px; border-radius: 8px; border: none;
background: transparent; font-size: 14px; font-weight: 600;
cursor: pointer; color: var(--gray-500); transition: all .15s;
}
.tab-btn.active { background: var(--ink); color: var(--white) }
.tab-content { display: none }
.tab-content.active { display: block }
@media (max-width: 768px) {
.stats { grid-template-columns: repeat(2, 1fr) }
.hero h1 { font-size: 28px }
.companies-grid { grid-template-columns: 1fr }
.nav-links { display: none }
}
@media (max-width: 480px) {
.stats { grid-template-columns: 1fr }
.section-header { flex-direction: column; align-items: flex-start }
}
</style>
</head>
<body>
<nav class="nav">
<div class="container">
<a href="#" class="nav-logo">
<div class="dot">📋</div>
HR Ассистент
</a>
<div class="nav-links">
<a href="#companies" class="active">Компании</a>
<a href="#tasks">Поручения</a>
<a href="#faq">HR FAQ</a>
</div>
</div>
</nav>
<section class="hero">
<div class="container">
<h1>Контроль поручений дочерним компаниям</h1>
<p class="sub">Отслеживайте задачи по всем дочерним организациям в одном окне</p>
<div class="stats" id="stats"></div>
</div>
</section>
<div class="container">
<!-- COMPANIES SECTION -->
<div id="companies">
<div class="section-header">
<h2>Дочерние компании</h2>
<div class="search-bar">
<input type="text" id="companySearch" placeholder="Поиск по названию..." oninput="renderCompanies()">
<button class="btn btn-primary" onclick="openModal('company')">+ Добавить компанию</button>
</div>
</div>
<div class="companies-grid" id="companiesGrid"></div>
</div>
<!-- TASKS SECTION -->
<div id="tasks" style="margin-top:56px">
<div class="section-header">
<h2>Поручения</h2>
<div class="search-bar">
<input type="text" id="taskSearch" placeholder="Поиск поручения..." oninput="renderTasks()">
<select id="taskFilterCompany" onchange="renderTasks()">
<option value="">Все компании</option>
</select>
<select id="taskFilterStatus" onchange="renderTasks()">
<option value="">Все статусы</option>
<option value="pending">Ожидает</option>
<option value="progress">В работе</option>
<option value="done">Готово</option>
<option value="overdue">Просрочено</option>
</select>
<button class="btn btn-primary" onclick="openModal('task')">+ Добавить поручение</button>
</div>
</div>
<div class="task-table-wrap" id="taskTableWrap"></div>
</div>
<!-- FAQ -->
<div id="faq" style="margin-top:56px">
<div class="section-header"><h2>HR FAQ</h2></div>
<div id="faqList"></div>
</div>
</div>
<footer class="footer">
HR Ассистент — инструмент для контроля поручений &copy; 2026
</footer>
<!-- MODALS -->
<div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
<div class="modal" id="modalContent"></div>
</div>
<script>
// ====== DATA MODEL (localStorage) ======
const defaultCompanies = [
{ id: 'c1', name: 'ТОО «КазМунайГаз Онимдери»', color: 'g1' },
{ id: 'c2', name: 'ТОО «КМГ Инжиниринг»', color: 'g2' },
{ id: 'c3', name: 'ТОО «Казахстанский трубопровод»', color: 'g3' },
{ id: 'c4', name: 'ТОО «Эмбамунайгаз»', color: 'g4' },
{ id: 'c5', name: 'ТОО «Мангистаумунайгаз»', color: 'g5' },
{ id: 'c6', name: 'ТОО «Атырауский НПЗ»', color: 'g1' },
{ id: 'c7', name: 'ТОО «Павлодарский НХЗ»', color: 'g2' },
{ id: 'c8', name: 'ТОО «Шымкентский НПЗ»', color: 'g3' },
{ id: 'c9', name: 'ТОО «КазТрансОйл»', color: 'g4' },
{ id: 'c10', name: 'ТОО «КазРосГаз»', color: 'g5' },
{ id: 'c11', name: 'ТОО «KMG International»', color: 'g1' },
{ id: 'c12', name: 'ТОО «КМГ-Аэро»', color: 'g2' },
{ id: 'c13', name: 'ТОО «КМГ Сервис»', color: 'g3' },
{ id: 'c14', name: 'ТОО «ЭнергоТранс»', color: 'g4' },
{ id: 'c15', name: 'ТОО «Каспийский Трубопровод»', color: 'g5' },
{ id: 'c16', name: 'ТОО «Управление активами»', color: 'g1' },
{ id: 'c17', name: 'ТОО «КМГ Безопасность»', color: 'g2' },
{ id: 'c18', name: 'ТОО «ГеоСервис»', color: 'g3' },
];
const defaultTasks = [
{ id: 't1', companyId: 'c1', title: 'Подготовить отчёт по охране труда за Q1', status: 'pending', deadline: '2026-06-20', priority: 'high' },
{ id: 't2', companyId: 'c2', title: 'Обновить штатное расписание', status: 'progress', deadline: '2026-06-25', priority: 'mid' },
{ id: 't3', companyId: 'c3', title: 'Согласовать KPI руководителей', status: 'done', deadline: '2026-06-01', priority: 'high' },
{ id: 't4', companyId: 'c1', title: 'Провести аудит условий труда', status: 'overdue', deadline: '2026-05-15', priority: 'high' },
{ id: 't5', companyId: 'c4', title: 'Организовать обучение по ТБ', status: 'pending', deadline: '2026-07-01', priority: 'mid' },
{ id: 't6', companyId: 'c5', title: 'Собрать данные по текучести кадров', status: 'progress', deadline: '2026-06-18', priority: 'low' },
{ id: 't7', companyId: 'c6', title: 'Проверить трудовые договоры', status: 'pending', deadline: '2026-06-30', priority: 'mid' },
{ id: 't8', companyId: 'c2', title: 'Запустить программу наставничества', status: 'progress', deadline: '2026-06-10', priority: 'high' },
{ id: 't9', companyId: 'c8', title: 'Внедрить систему оценки персонала', status: 'overdue', deadline: '2026-04-20', priority: 'high' },
{ id: 't10', companyId: 'c9', title: 'Актуализировать должностные инструкции', status: 'pending', deadline: '2026-07-10', priority: 'low' },
];
function loadData() {
const c = localStorage.getItem('hr-companies');
const t = localStorage.getItem('hr-tasks');
return {
companies: c ? JSON.parse(c) : defaultCompanies,
tasks: t ? JSON.parse(t) : defaultTasks,
};
}
function saveData(companies, tasks) {
localStorage.setItem('hr-companies', JSON.stringify(companies));
localStorage.setItem('hr-tasks', JSON.stringify(tasks));
}
let { companies, tasks } = loadData();
// ====== HELPERS ======
function getCompany(id) { return companies.find(c => c.id === id) }
function taskCount(cid, status) { return tasks.filter(t => t.companyId === cid && t.status === status).length }
function totalFor(status) { return tasks.filter(t => t.status === status).length }
function openModal(type) {
const overlay = document.getElementById('modalOverlay');
const content = document.getElementById('modalContent');
if (type === 'company') {
content.innerHTML = `
<h3>Добавить компанию</h3>
<label>Название компании</label>
<input type="text" id="mCompanyName" placeholder="ТОО «...»">
<div class="btn-row">
<button class="btn btn-outline" onclick="closeModal()">Отмена</button>
<button class="btn btn-primary" onclick="addCompany()">Добавить</button>
</div>
`;
} else {
content.innerHTML = `
<h3>Добавить поручение</h3>
<label>Компания</label>
<select id="mTaskCompany">
${companies.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('')}
</select>
<label>Описание поручения</label>
<input type="text" id="mTaskTitle" placeholder="Что нужно сделать?">
<label>Дедлайн</label>
<input type="date" id="mTaskDeadline">
<label>Приоритет</label>
<select id="mTaskPriority">
<option value="high">Высокий</option>
<option value="mid" selected>Средний</option>
<option value="low">Низкий</option>
</select>
<label>Статус</label>
<select id="mTaskStatus">
<option value="pending">Ожидает</option>
<option value="progress">В работе</option>
<option value="done">Готово</option>
<option value="overdue">Просрочено</option>
</select>
<div class="btn-row">
<button class="btn btn-outline" onclick="closeModal()">Отмена</button>
<button class="btn btn-primary" onclick="addTask()">Добавить</button>
</div>
`;
}
overlay.classList.add('open');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('open');
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function uid() { return 'x' + Date.now() + Math.random().toString(36).slice(2, 8) }
function addCompany() {
const name = document.getElementById('mCompanyName').value.trim();
if (!name) return;
const colors = ['g1','g2','g3','g4','g5'];
companies.push({ id: uid(), name, color: colors[Math.floor(Math.random() * colors.length)] });
saveData(companies, tasks);
closeModal();
renderAll();
}
function addTask() {
const companyId = document.getElementById('mTaskCompany').value;
const title = document.getElementById('mTaskTitle').value.trim();
const deadline = document.getElementById('mTaskDeadline').value;
const priority = document.getElementById('mTaskPriority').value;
const status = document.getElementById('mTaskStatus').value;
if (!title) return;
tasks.push({ id: uid(), companyId, title, deadline, priority, status });
saveData(companies, tasks);
closeModal();
renderAll();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveData(companies, tasks);
renderAll();
}
function deleteCompany(id) {
if (!confirm('Удалить компанию и все её поручения?')) return;
companies = companies.filter(c => c.id !== id);
tasks = tasks.filter(t => t.companyId !== id);
saveData(companies, tasks);
renderAll();
}
function changeTaskStatus(id, status) {
const t = tasks.find(t => t.id === id);
if (t) { t.status = status; saveData(companies, tasks); renderAll(); }
}
// ====== RENDER ======
const priorityLabels = { high: 'Высокий', mid: 'Средний', low: 'Низкий' };
const statusLabels = { pending: 'Ожидает', progress: 'В работе', done: 'Готово', overdue: 'Просрочено' };
function renderStats() {
const totalCompanies = companies.length;
const active = totalFor('pending') + totalFor('progress');
const done = totalFor('done');
const overdue = totalFor('overdue');
document.getElementById('stats').innerHTML = `
<div class="stat-card">
<div class="label">Компаний</div>
<div class="value" style="color:var(--indigo)">${totalCompanies}</div>
</div>
<div class="stat-card">
<div class="label">Активных поручений</div>
<div class="value" style="color:var(--amber)">${active}</div>
</div>
<div class="stat-card">
<div class="label">Выполнено</div>
<div class="value" style="color:var(--green)">${done}</div>
</div>
<div class="stat-card">
<div class="label">Просрочено</div>
<div class="value" style="color:var(--red)">${overdue}</div>
</div>
`;
}
function renderCompanies() {
const search = (document.getElementById('companySearch')?.value || '').toLowerCase();
const filtered = companies.filter(c => c.name.toLowerCase().includes(search));
const grid = document.getElementById('companiesGrid');
if (filtered.length === 0) {
grid.innerHTML = '<div class="empty"><div class="icon">🏢</div><p>Компании не найдены</p></div>';
return;
}
grid.innerHTML = filtered.map(c => {
const pending = taskCount(c.id, 'pending');
const progress = taskCount(c.id, 'progress');
const done = taskCount(c.id, 'done');
const overdue = taskCount(c.id, 'overdue');
return `
<div class="company-card" onclick="document.getElementById('taskFilterCompany').value='${c.id}';document.getElementById('tasks').scrollIntoView({behavior:'smooth'});renderTasks()">
<div style="display:flex;align-items:center;justify-content:space-between">
<div style="display:flex;align-items:center;gap:12px;min-width:0">
<div class="company-avatar ${c.color}">${c.name.charAt(0)}</div>
<div class="company-name" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(c.name)}</div>
</div>
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();deleteCompany('${c.id}')" title="Удалить">🗑</button>
</div>
<div class="task-counts">
<span title="Ожидает"><span class="dot-indicator" style="background:var(--amber)"></span>${pending} ожидает</span>
<span title="В работе"><span class="dot-indicator" style="background:var(--indigo)"></span>${progress} в работе</span>
<span title="Готово"><span class="dot-indicator" style="background:var(--green)"></span>${done} готово</span>
<span title="Просрочено"><span class="dot-indicator" style="background:var(--red)"></span>${overdue} просрочено</span>
</div>
${overdue > 0 ? `<div style="margin-top:12px"><span class="badge" style="background:var(--red-bg);color:var(--red)">⚠️ Есть просроченные</span></div>` : ''}
</div>
`;
}).join('');
}
function renderTasks() {
const searchText = (document.getElementById('taskSearch')?.value || '').toLowerCase();
const filterCompany = document.getElementById('taskFilterCompany')?.value || '';
const filterStatus = document.getElementById('taskFilterStatus')?.value || '';
let filtered = tasks;
if (searchText) filtered = filtered.filter(t => t.title.toLowerCase().includes(searchText));
if (filterCompany) filtered = filtered.filter(t => t.companyId === filterCompany);
if (filterStatus) filtered = filtered.filter(t => t.status === filterStatus);
// update company filter dropdown
const sel = document.getElementById('taskFilterCompany');
if (sel) {
const cur = sel.value;
sel.innerHTML = '<option value="">Все компании</option>' +
companies.map(c => `<option value="${c.id}" ${c.id === cur ? 'selected' : ''}>${esc(c.name)}</option>`).join('');
}
const wrap = document.getElementById('taskTableWrap');
if (filtered.length === 0) {
wrap.innerHTML = '<div class="empty"><div class="icon">📝</div><p>Поручений нет</p></div>';
return;
}
wrap.innerHTML = `
<table class="task-table">
<thead><tr>
<th>Компания</th>
<th>Поручение</th>
<th>Дедлайн</th>
<th>Приоритет</th>
<th>Статус</th>
<th></th>
</tr></thead>
<tbody>
${filtered.map(t => {
const c = getCompany(t.companyId);
const deadlineDisplay = t.deadline ? new Date(t.deadline).toLocaleDateString('ru-RU') : '—';
return `
<tr>
<td style="font-weight:600">${c ? esc(c.name) : '—'}</td>
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis">${esc(t.title)}</td>
<td>${deadlineDisplay}</td>
<td><span class="priority-dot priority-${t.priority}"></span>${priorityLabels[t.priority] || t.priority}</td>
<td>
<select class="status-pill status-${t.status}" style="border:none;cursor:pointer;font-family:inherit;font-weight:600;font-size:12px;padding:4px 12px;border-radius:20px;background:var(--${t.status === 'pending' ? 'amber' : t.status === 'progress' ? 'indigo' : t.status === 'done' ? 'green' : 'red'}-bg);color:var(--${t.status === 'pending' ? 'amber' : t.status === 'progress' ? 'indigo' : t.status === 'done' ? 'green' : 'red'})" onchange="changeTaskStatus('${t.id}', this.value)">
<option value="pending" ${t.status==='pending'?'selected':''}>Ожидает</option>
<option value="progress" ${t.status==='progress'?'selected':''}>В работе</option>
<option value="done" ${t.status==='done'?'selected':''}>Готово</option>
<option value="overdue" ${t.status==='overdue'?'selected':''}>Просрочено</option>
</select>
</td>
<td><button class="btn btn-sm btn-danger" onclick="deleteTask('${t.id}')">🗑</button></td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
// ====== FAQ ======
const faqData = [
{ q: 'Как оформить приказ о приёме на работу?', a: 'Приказ оформляется по форме Т-1 в течение 3 рабочих дней после подписания трудового договора. Обязательно ознакомить сотрудника под подпись.' },
{ q: 'Какие сроки уведомления о сокращении?', a: 'Работодатель обязан письменно уведомить сотрудника не менее чем за 1 месяц до расторжения трудового договора по сокращению штата (ст. 53 ТК РК).' },
{ q: 'Как рассчитать отпускные?', a: 'Средний дневной заработок = сумма зарплаты за 12 месяцев / количество рабочих дней. Умножаем на количество дней отпуска. Выплатить не позднее чем за 3 дня до начала.' },
{ q: 'Обязан ли работодатель проводить медосмотр?', a: 'Да, для сотрудников занятых на тяжёлых работах, с вредными/опасными условиями труда, а также для работников пищевой промышленности, медицины и образования — обязательно.' },
{ q: 'Как перевести сотрудника в другую дочернюю компанию?', a: 'Перевод оформляется через увольнение из одной компании и приём в другую, либо через трёхстороннее соглашение о переводе (ст. 42 ТК РК). В обоих случаях требуется письменное согласие сотрудника.' },
{ q: 'Что делать при несчастном случае на производстве?', a: 'Оказать первую помощь → вызвать скорую → сохранить место происшествия → создать комиссию по расследованию → уведомить госинспекцию труда в течение суток → оформить акт Н-1.' },
{ q: 'Можно ли установить испытательный срок для руководителя?', a: 'Да, до 3 месяцев. Для руководителей организаций и их заместителей, главных бухгалтеров — до 6 месяцев (ст. 36 ТК РК).' },
{ q: 'Как контролировать исполнение поручений дочерним компаниям?', a: 'Рекомендуется: 1) Письменная фиксация поручений с дедлайнами, 2) Еженедельный мониторинг статусов, 3) Единая система отчётности, 4) Регулярные статус-встречи с руководителями ДК.' },
];
function renderFAQ() {
document.getElementById('faqList').innerHTML = faqData.map((item, i) => `
<div class="faq-item">
<div class="faq-q" onclick="this.parentElement.classList.toggle('open')">
<span>${esc(item.q)}</span>
<span class="arrow"></span>
</div>
<div class="faq-a">${esc(item.a)}</div>
</div>
`).join('');
}
// ====== RENDER ALL ======
function renderAll() {
renderStats();
renderCompanies();
renderTasks();
}
renderAll();
renderFAQ();
// Nav smooth scroll
document.querySelectorAll('.nav-links a').forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const target = document.querySelector(a.getAttribute('href'));
if (target) target.scrollIntoView({ behavior: 'smooth' });
document.querySelectorAll('.nav-links a').forEach(x => x.classList.remove('active'));
a.classList.add('active');
});
});
// Keyboard shortcut
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('taskSearch').focus();
}
});
</script>
</body>
</html>