Бланк ПАБ: 7 категорий с чекбоксами, таблица нарушений, дашборд
This commit is contained in:
parent
0beb7d36eb
commit
b6bf029c01
173
AGENTS.md
Normal file
173
AGENTS.md
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<!-- vibe42-agents-version: v3-guided-2026-06-01 -->
|
||||||
|
# Vibe42 — учебная песочница для лендингов
|
||||||
|
|
||||||
|
Workspace юзера `Yershat_Baitayev`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ТВОЯ РОЛЬ
|
||||||
|
|
||||||
|
Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с:
|
||||||
|
1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/Yershat_Baitayev/<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/Yershat_Baitayev/<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/Yershat_Baitayev`)** — это папка-контейнер юзера, не репозиторий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ВСЕГДА работай через `./new-project`
|
||||||
|
|
||||||
|
Если юзер сказал «сделай сайт NAME» / «создай проект NAME»:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /workspaces/Yershat_Baitayev
|
||||||
|
./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/Yershat_Baitayev
|
||||||
|
- Pages (живые лендинги): https://pages.git.vibe42.kz/Yershat_Baitayev/<repo>/
|
||||||
|
- Креды уже в `/workspaces/Yershat_Baitayev/.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
110
design.md
Normal 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
|
||||||
964
index.html
Normal file
964
index.html
Normal file
@ -0,0 +1,964 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Поведенческий аудит безопасности (ПАБ)</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--ink:#0F1218; --cyan:#00B4D8; --cyan-light:#48CAE4; --cyan-bg:#E0F7FA;
|
||||||
|
--white:#FFFFFF; --gray-500:#5B6573; --gray-100:#F2F4F7; --gray-200:#E2E6EB;
|
||||||
|
--red:#E63946; --red-bg:#FFEBED; --green:#2D6A4F; --green-bg:#EDF7F0;
|
||||||
|
--radius:8px; --radius-lg:14px; --shadow:0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{
|
||||||
|
font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
|
||||||
|
color:var(--ink); background:var(--gray-100); min-height:100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== LOGIN ===== */
|
||||||
|
.login-screen{
|
||||||
|
display:flex; align-items:center; justify-content:center; min-height:100vh;
|
||||||
|
background:linear-gradient(135deg, var(--ink) 0%, #1a2332 100%);
|
||||||
|
}
|
||||||
|
.login-card{
|
||||||
|
background:var(--white); border-radius:var(--radius-lg); padding:48px 40px;
|
||||||
|
width:100%; max-width:440px; box-shadow:0 8px 40px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.login-card .logo{text-align:center; margin-bottom:32px}
|
||||||
|
.login-card .logo .icon{font-size:48px; display:block; margin-bottom:8px}
|
||||||
|
.login-card .logo h1{font-size:22px; font-weight:800; color:var(--ink)}
|
||||||
|
.login-card .logo p{font-size:14px; color:var(--gray-500); margin-top:4px}
|
||||||
|
.form-group{margin-bottom:18px}
|
||||||
|
.form-group label{display:block; font-size:12px; font-weight:700; color:var(--gray-500);
|
||||||
|
margin-bottom:5px; text-transform:uppercase; letter-spacing:0.5px}
|
||||||
|
.form-group input,.form-group select,.form-group textarea{
|
||||||
|
width:100%; padding:10px 12px; border:2px solid var(--gray-200); border-radius:var(--radius);
|
||||||
|
font-size:14px; font-family:inherit; color:var(--ink); background:var(--white);
|
||||||
|
transition:border-color .2s; outline:none;
|
||||||
|
}
|
||||||
|
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--cyan)}
|
||||||
|
.form-group textarea{resize:vertical; min-height:60px}
|
||||||
|
.btn{
|
||||||
|
display:inline-flex; align-items:center; justify-content:center; gap:6px;
|
||||||
|
padding:10px 20px; border-radius:var(--radius); font-size:14px; font-weight:700;
|
||||||
|
border:none; cursor:pointer; text-decoration:none; font-family:inherit;
|
||||||
|
transition:all .2s; white-space:nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary{background:var(--cyan); color:var(--white)}
|
||||||
|
.btn-primary:hover{background:var(--cyan-light)}
|
||||||
|
.btn-danger{background:var(--red); color:var(--white)}
|
||||||
|
.btn-danger:hover{background:#c1121f}
|
||||||
|
.btn-outline{background:transparent; color:var(--ink); border:2px solid var(--gray-200)}
|
||||||
|
.btn-outline:hover{border-color:var(--cyan); color:var(--cyan)}
|
||||||
|
.btn-sm{padding:6px 14px; font-size:12px}
|
||||||
|
.btn-block{width:100%}
|
||||||
|
.login-error{color:var(--red); font-size:13px; text-align:center; margin-top:12px; display:none}
|
||||||
|
|
||||||
|
/* ===== APP ===== */
|
||||||
|
.app-screen{display:none}
|
||||||
|
.app-header{
|
||||||
|
background:var(--ink); color:var(--white); padding:0 24px;
|
||||||
|
display:flex; align-items:center; justify-content:space-between; height:56px;
|
||||||
|
position:sticky; top:0; z-index:100; box-shadow:0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.app-header .logo-area{display:flex; align-items:center; gap:8px; font-weight:700; font-size:15px}
|
||||||
|
.app-header .logo-area .icon{font-size:22px}
|
||||||
|
.app-header nav{display:flex; gap:4px}
|
||||||
|
.app-header nav a{
|
||||||
|
color:#9aa3b2; text-decoration:none; padding:7px 14px; border-radius:var(--radius);
|
||||||
|
font-size:13px; font-weight:600; transition:all .2s;
|
||||||
|
}
|
||||||
|
.app-header nav a:hover,.app-header nav a.active{color:var(--white); background:rgba(255,255,255,0.08)}
|
||||||
|
.app-header .user-area{display:flex; align-items:center; gap:10px; font-size:13px}
|
||||||
|
.app-header .user-area .role{color:var(--cyan-light); font-weight:600}
|
||||||
|
.app-content{max-width:1100px; margin:0 auto; padding:28px 24px}
|
||||||
|
.panel{display:none}
|
||||||
|
.panel.active{display:block}
|
||||||
|
|
||||||
|
/* ===== PAGE HEADER ===== */
|
||||||
|
.page-header{margin-bottom:24px}
|
||||||
|
.page-header h2{font-size:26px; font-weight:800; margin-bottom:6px}
|
||||||
|
.page-header p{color:var(--gray-500); font-size:15px}
|
||||||
|
|
||||||
|
/* ===== AUDIT FORM ===== */
|
||||||
|
.audit-form{max-width:900px}
|
||||||
|
|
||||||
|
/* Header block */
|
||||||
|
.form-header{
|
||||||
|
background:var(--white); border-radius:var(--radius-lg); padding:24px 28px;
|
||||||
|
box-shadow:var(--shadow); margin-bottom:16px;
|
||||||
|
}
|
||||||
|
.form-header h3{font-size:17px; font-weight:700; margin-bottom:16px; color:var(--ink)}
|
||||||
|
.header-grid{display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px}
|
||||||
|
.header-grid.col2{grid-template-columns:1fr 1fr}
|
||||||
|
.header-grid.col4{grid-template-columns:1fr 1fr 1fr 1fr}
|
||||||
|
.header-grid .fg label{font-size:11px; font-weight:700; color:var(--gray-500); display:block; margin-bottom:3px; text-transform:uppercase}
|
||||||
|
.header-grid .fg input,.header-grid .fg select{
|
||||||
|
width:100%; padding:8px 10px; border:2px solid var(--gray-200); border-radius:var(--radius);
|
||||||
|
font-size:13px; font-family:inherit; outline:none; background:var(--white);
|
||||||
|
}
|
||||||
|
.header-grid .fg input:focus,.header-grid .fg select:focus{border-color:var(--cyan)}
|
||||||
|
|
||||||
|
/* Overall safe/danger toggle */
|
||||||
|
.overall-toggle{display:flex; gap:12px; margin-top:12px}
|
||||||
|
.toggle-btn{
|
||||||
|
flex:1; padding:10px; border:2px solid var(--gray-200); border-radius:var(--radius);
|
||||||
|
text-align:center; cursor:pointer; font-size:13px; font-weight:700; background:var(--white); transition:all .2s;
|
||||||
|
}
|
||||||
|
.toggle-btn.safe.selected{border-color:var(--green); background:var(--green-bg); color:var(--green)}
|
||||||
|
.toggle-btn.danger.selected{border-color:var(--red); background:var(--red-bg); color:var(--red)}
|
||||||
|
|
||||||
|
/* Category section */
|
||||||
|
.cat-section{
|
||||||
|
background:var(--white); border-radius:var(--radius-lg); box-shadow:var(--shadow);
|
||||||
|
margin-bottom:12px; overflow:hidden;
|
||||||
|
}
|
||||||
|
.cat-header{
|
||||||
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
|
padding:14px 20px; background:var(--gray-100); cursor:pointer; user-select:none;
|
||||||
|
border-bottom:1px solid var(--gray-200); transition:background .2s;
|
||||||
|
}
|
||||||
|
.cat-header:hover{background:var(--gray-200)}
|
||||||
|
.cat-header .cat-title{font-size:15px; font-weight:700; display:flex; align-items:center; gap:8px}
|
||||||
|
.cat-header .cat-badge{
|
||||||
|
font-size:11px; font-weight:700; padding:3px 10px; border-radius:20px;
|
||||||
|
background:var(--red-bg); color:var(--red);
|
||||||
|
}
|
||||||
|
.cat-header .cat-badge.all-safe{background:var(--green-bg); color:var(--green)}
|
||||||
|
.cat-header .cat-arrow{font-size:12px; transition:transform .3s; color:var(--gray-500)}
|
||||||
|
.cat-header.open .cat-arrow{transform:rotate(180deg)}
|
||||||
|
.cat-body{display:none; padding:16px 20px}
|
||||||
|
.cat-body.open{display:block}
|
||||||
|
|
||||||
|
/* Checklist */
|
||||||
|
.checklist{display:grid; grid-template-columns:1fr 1fr; gap:6px 24px}
|
||||||
|
.checklist.col3{grid-template-columns:1fr 1fr 1fr}
|
||||||
|
.checklist.col1{grid-template-columns:1fr}
|
||||||
|
.check-item{display:flex; align-items:flex-start; gap:8px; padding:6px 0; font-size:13px; cursor:pointer}
|
||||||
|
.check-item input[type=checkbox]{margin-top:2px; width:16px; height:16px; accent-color:var(--red); cursor:pointer; flex-shrink:0}
|
||||||
|
.check-item.checked label{color:var(--red); font-weight:600}
|
||||||
|
.check-item label{cursor:pointer; flex:1}
|
||||||
|
.check-item .other-input{width:100%; margin-top:4px; padding:6px 8px; border:1px solid var(--gray-200); border-radius:4px; font-size:12px; display:none}
|
||||||
|
.check-item.checked .other-input.visible{display:block}
|
||||||
|
|
||||||
|
/* Category footer */
|
||||||
|
.cat-footer{
|
||||||
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
|
padding:10px 20px; background:var(--gray-100); border-top:1px solid var(--gray-200);
|
||||||
|
font-size:12px; color:var(--gray-500); font-weight:600;
|
||||||
|
}
|
||||||
|
.cat-footer .total-count{color:var(--red); font-weight:700}
|
||||||
|
.cat-footer .total-count.zero{color:var(--green)}
|
||||||
|
.all-safe-toggle{
|
||||||
|
display:flex; align-items:center; gap:6px; cursor:pointer; font-size:12px; font-weight:700;
|
||||||
|
padding:4px 12px; border-radius:20px; transition:all .2s;
|
||||||
|
}
|
||||||
|
.all-safe-toggle.active{background:var(--green-bg); color:var(--green)}
|
||||||
|
.all-safe-toggle input{display:none}
|
||||||
|
|
||||||
|
/* Violation table */
|
||||||
|
.violations-block{
|
||||||
|
background:var(--white); border-radius:var(--radius-lg); padding:20px 24px;
|
||||||
|
box-shadow:var(--shadow); margin-top:16px;
|
||||||
|
}
|
||||||
|
.violations-block h3{font-size:15px; font-weight:700; margin-bottom:14px}
|
||||||
|
.vio-grid{display:grid; grid-template-columns:40px 1.3fr 1fr 0.8fr 1fr 1fr 1fr 0.8fr 30px; gap:6px; margin-bottom:6px; align-items:end}
|
||||||
|
.vio-grid.header-row{font-size:11px; font-weight:700; color:var(--gray-500); text-transform:uppercase; margin-bottom:4px}
|
||||||
|
.vio-grid input,.vio-grid select{
|
||||||
|
padding:7px 8px; border:1px solid var(--gray-200); border-radius:var(--radius);
|
||||||
|
font-size:12px; font-family:inherit; outline:none; width:100%;
|
||||||
|
}
|
||||||
|
.vio-grid input:focus,.vio-grid select:focus{border-color:var(--cyan)}
|
||||||
|
.vio-row-num{font-size:12px; font-weight:700; color:var(--gray-500); text-align:center; padding-top:8px}
|
||||||
|
.remove-vio-btn{background:none; border:none; color:var(--red); cursor:pointer; font-size:18px; padding:4px}
|
||||||
|
|
||||||
|
.form-actions{display:flex; gap:10px; margin-top:20px}
|
||||||
|
.form-success{
|
||||||
|
background:var(--green-bg); border:1px solid var(--green); border-radius:var(--radius);
|
||||||
|
padding:14px 18px; color:var(--green); font-weight:600; margin-top:14px; display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== DASHBOARD ===== */
|
||||||
|
.stats-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:16px; margin-bottom:24px}
|
||||||
|
.stat-card{
|
||||||
|
background:var(--white); border-radius:var(--radius-lg); padding:20px;
|
||||||
|
box-shadow:var(--shadow);
|
||||||
|
}
|
||||||
|
.stat-card .stat-label{font-size:12px; font-weight:700; color:var(--gray-500); text-transform:uppercase; margin-bottom:4px}
|
||||||
|
.stat-card .stat-value{font-size:32px; font-weight:800; line-height:1}
|
||||||
|
.stat-card.green .stat-value{color:var(--green)}
|
||||||
|
.stat-card.red .stat-value{color:var(--red)}
|
||||||
|
.stat-card.blue .stat-value{color:var(--cyan)}
|
||||||
|
.charts-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; margin-bottom:24px}
|
||||||
|
.chart-card{
|
||||||
|
background:var(--white); border-radius:var(--radius-lg); padding:20px;
|
||||||
|
box-shadow:var(--shadow);
|
||||||
|
}
|
||||||
|
.chart-card h3{font-size:15px; font-weight:700; margin-bottom:14px}
|
||||||
|
.chart-card canvas{max-height:260px}
|
||||||
|
|
||||||
|
/* ===== HISTORY ===== */
|
||||||
|
.table-filters{display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap}
|
||||||
|
.table-filters select,.table-filters input{
|
||||||
|
padding:8px 12px; border:2px solid var(--gray-200); border-radius:var(--radius);
|
||||||
|
font-size:13px; font-family:inherit; outline:none; background:var(--white);
|
||||||
|
}
|
||||||
|
.table-filters select:focus,.table-filters input:focus{border-color:var(--cyan)}
|
||||||
|
.table-wrap{overflow-x:auto}
|
||||||
|
.data-table{
|
||||||
|
width:100%; border-collapse:collapse; background:var(--white);
|
||||||
|
border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow); font-size:13px;
|
||||||
|
}
|
||||||
|
.data-table th{
|
||||||
|
background:var(--ink); color:var(--white); padding:11px 14px; text-align:left;
|
||||||
|
font-size:12px; font-weight:700; text-transform:uppercase;
|
||||||
|
}
|
||||||
|
.data-table td{padding:10px 14px; border-bottom:1px solid var(--gray-100)}
|
||||||
|
.data-table tr:hover td{background:var(--gray-100)}
|
||||||
|
.badge{
|
||||||
|
display:inline-block; padding:3px 10px; border-radius:20px; font-size:11px; font-weight:700;
|
||||||
|
}
|
||||||
|
.badge-safe{background:var(--green-bg); color:var(--green)}
|
||||||
|
.badge-danger{background:var(--red-bg); color:var(--red)}
|
||||||
|
.no-data{text-align:center; padding:40px 20px; color:var(--gray-500)}
|
||||||
|
.no-data .icon{font-size:40px; display:block; margin-bottom:10px}
|
||||||
|
|
||||||
|
/* Risk bar */
|
||||||
|
.risk-bar{display:flex; height:20px; border-radius:10px; overflow:hidden; margin-top:6px}
|
||||||
|
.risk-safe{background:var(--green); transition:width .5s}
|
||||||
|
.risk-unsafe{background:var(--red); transition:width .5s}
|
||||||
|
.risk-labels{display:flex; justify-content:space-between; font-size:11px; color:var(--gray-500); margin-top:3px}
|
||||||
|
|
||||||
|
.view-link{color:var(--cyan); cursor:pointer; font-weight:600; text-decoration:none}
|
||||||
|
.view-link:hover{text-decoration:underline}
|
||||||
|
|
||||||
|
@media (max-width:768px){
|
||||||
|
.login-card{padding:28px 20px; margin:12px}
|
||||||
|
.app-header{padding:0 12px; height:auto; flex-wrap:wrap; gap:6px; padding-top:8px; padding-bottom:8px}
|
||||||
|
.app-header nav{order:3; width:100%; overflow-x:auto}
|
||||||
|
.app-content{padding:16px 12px}
|
||||||
|
.header-grid{grid-template-columns:1fr 1fr}
|
||||||
|
.header-grid.col4{grid-template-columns:1fr 1fr}
|
||||||
|
.checklist{grid-template-columns:1fr}
|
||||||
|
.checklist.col3{grid-template-columns:1fr 1fr}
|
||||||
|
.charts-grid{grid-template-columns:1fr}
|
||||||
|
.stats-grid{grid-template-columns:1fr 1fr}
|
||||||
|
.vio-grid{grid-template-columns:30px 1fr 1fr; row-gap:4px}
|
||||||
|
.vio-grid.header-row{display:none}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ========== LOGIN ========== -->
|
||||||
|
<div id="loginScreen" class="login-screen">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="icon">🛡️</span>
|
||||||
|
<h1>Поведенческий аудит безопасности</h1>
|
||||||
|
<p>Система учёта и аналитики ПАБ</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Логин</label>
|
||||||
|
<input type="text" id="loginUser" placeholder="Введите логин" autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Пароль</label>
|
||||||
|
<input type="password" id="loginPass" placeholder="Введите пароль" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-block" onclick="doLogin()">Войти</button>
|
||||||
|
<div id="loginError" class="login-error">Неверный логин или пароль</div>
|
||||||
|
<p style="text-align:center;margin-top:14px;font-size:11px;color:var(--gray-500)">
|
||||||
|
Демо: <b>admin / admin</b> | <b>auditor / auditor</b> | <b>ivanov / 1234</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== APP ========== -->
|
||||||
|
<div id="appScreen" class="app-screen">
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="logo-area"><span class="icon">🛡️</span> ПАБ Система</div>
|
||||||
|
<nav>
|
||||||
|
<a href="#" data-panel="newAudit" class="active" onclick="switchPanel('newAudit',this)">Новый аудит</a>
|
||||||
|
<a href="#" data-panel="dashboard" onclick="switchPanel('dashboard',this)">Дашборд</a>
|
||||||
|
<a href="#" data-panel="history" onclick="switchPanel('history',this)">История</a>
|
||||||
|
</nav>
|
||||||
|
<div class="user-area">
|
||||||
|
<span class="role" id="displayName"></span>
|
||||||
|
<button class="btn btn-outline btn-sm" style="color:#9aa3b2;border-color:#3a4452" onclick="doLogout()">Выход</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="app-content">
|
||||||
|
|
||||||
|
<!-- ============ NEW AUDIT ============ -->
|
||||||
|
<div id="panelNewAudit" class="panel active">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>📋 Бланк поведенческого аудита безопасности</h2>
|
||||||
|
<p>Заполните все категории наблюдения</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="audit-form" id="auditForm">
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="form-header">
|
||||||
|
<h3>📝 Данные аудита</h3>
|
||||||
|
<div class="header-grid col4">
|
||||||
|
<div class="fg"><label>Бланк ПАБ №</label><input id="pabNumber" placeholder="Номер"></div>
|
||||||
|
<div class="fg"><label>Дата проведения</label><input type="date" id="pabDate"></div>
|
||||||
|
<div class="fg"><label>Начало</label><input type="time" id="pabTimeStart"></div>
|
||||||
|
<div class="fg"><label>Конец</label><input type="time" id="pabTimeEnd"></div>
|
||||||
|
</div>
|
||||||
|
<div class="header-grid" style="margin-top:12px">
|
||||||
|
<div class="fg"><label>Место проведения</label><input id="pabLocation" placeholder="Цех, участок"></div>
|
||||||
|
<div class="fg"><label>Тип работы</label><input id="pabWorkType" placeholder="Напр: ремонт линий связи"></div>
|
||||||
|
<div class="fg"><label>Кол-во наблюдаемых</label><input type="number" id="pabWorkerCount" min="1" value="1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="header-grid col2" style="margin-top:12px">
|
||||||
|
<div class="fg"><label>ФИО наблюдателя</label><input id="pabObserver" placeholder="ФИО"></div>
|
||||||
|
<div class="fg"><label>Должность наблюдателя</label><input id="pabObserverRole" placeholder="Должность"></div>
|
||||||
|
</div>
|
||||||
|
<div class="header-grid col2" style="margin-top:12px">
|
||||||
|
<div class="fg"><label>ФИО руководителя работ</label><input id="pabSupervisor" placeholder="ФИО"></div>
|
||||||
|
<div class="fg"><label>Должность руководителя</label><input id="pabSupervisorRole" placeholder="Должность"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top:14px;margin-bottom:0">
|
||||||
|
<label>Отметка для передачи в отдел БиОТ ДПБ</label>
|
||||||
|
<div class="overall-toggle">
|
||||||
|
<div class="toggle-btn safe selected" id="overallSafe" onclick="setOverall('safe')">✅ ВСЕ БЕЗОПАСНО</div>
|
||||||
|
<div class="toggle-btn danger" id="overallDanger" onclick="setOverall('danger')">☐ ЕСТЬ ОПАСНО</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CATEGORY SECTIONS generated by JS -->
|
||||||
|
<div id="categorySections"></div>
|
||||||
|
|
||||||
|
<!-- VIOLATIONS TABLE -->
|
||||||
|
<div class="violations-block">
|
||||||
|
<h3>📄 Несоответствия и корректирующие меры</h3>
|
||||||
|
<div class="vio-grid header-row" style="display:grid">
|
||||||
|
<span>№</span><span>Несоответствие</span><span>Исполнитель</span><span>Вид нарушения</span><span>Меры</span><span>Ответственное лицо</span><span>Дата</span><span>Форма завершения</span><span></span>
|
||||||
|
</div>
|
||||||
|
<div id="vioRows"></div>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="addVioRow()" style="margin-top:8px">+ Добавить строку</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SAVE -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" onclick="submitAudit()">💾 Сохранить аудит</button>
|
||||||
|
<button class="btn btn-outline" onclick="resetAuditForm()">🗑️ Очистить форму</button>
|
||||||
|
</div>
|
||||||
|
<div id="formSuccess" class="form-success">✅ Аудит сохранён! Данные доступны в Дашборде и Истории.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ DASHBOARD ============ -->
|
||||||
|
<div id="panelDashboard" class="panel">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>📊 Дашборд статистики ПАБ</h2>
|
||||||
|
<p>Аналитика по всем аудитам</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card"><div class="stat-label">Всего аудитов</div><div class="stat-value" id="statTotal">0</div></div>
|
||||||
|
<div class="stat-card green"><div class="stat-label">Всего безопасно</div><div class="stat-value" id="statAllSafe">0</div></div>
|
||||||
|
<div class="stat-card red"><div class="stat-label">С нарушениями</div><div class="stat-value" id="statWithDanger">0</div></div>
|
||||||
|
<div class="stat-card blue"><div class="stat-label">Выявлено нарушений</div><div class="stat-value" id="statViolations">0</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card" style="margin-bottom:16px">
|
||||||
|
<h3>🟢🔴 Соотношение аудитов: безопасные / с нарушениями</h3>
|
||||||
|
<div class="risk-bar"><div class="risk-safe" id="riskSafeBar" style="width:50%"></div><div class="risk-unsafe" id="riskUnsafeBar" style="width:50%"></div></div>
|
||||||
|
<div class="risk-labels"><span>Безопасные: <span id="riskSafeLabel">0</span></span><span>С нарушениями: <span id="riskUnsafeLabel">0</span></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="charts-grid">
|
||||||
|
<div class="chart-card"><h3>📂 Нарушения по категориям</h3><canvas id="chartCategories"></canvas></div>
|
||||||
|
<div class="chart-card"><h3>📅 Динамика по датам</h3><canvas id="chartTimeline"></canvas></div>
|
||||||
|
<div class="chart-card"><h3>👤 Топ наблюдателей</h3><canvas id="chartObservers"></canvas></div>
|
||||||
|
<div class="chart-card"><h3>🔝 Топ-10 нарушений</h3><canvas id="chartTopItems"></canvas></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ HISTORY ============ -->
|
||||||
|
<div id="panelHistory" class="panel">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>📁 История аудитов</h2>
|
||||||
|
<p>Архив всех проведённых ПАБ</p>
|
||||||
|
</div>
|
||||||
|
<div class="table-filters">
|
||||||
|
<select id="filterOverall" onchange="renderHistory()">
|
||||||
|
<option value="all">Все аудиты</option>
|
||||||
|
<option value="safe">Только «ВСЕ БЕЗОПАСНО»</option>
|
||||||
|
<option value="danger">Только с нарушениями</option>
|
||||||
|
</select>
|
||||||
|
<input type="date" id="filterDate" onchange="renderHistory()">
|
||||||
|
<input type="text" id="filterLocation" onchange="renderHistory()" placeholder="Поиск по месту...">
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="exportCSV()">📥 Экспорт CSV</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Бланк №</th><th>Дата</th><th>Время</th><th>Место</th><th>Наблюдатель</th>
|
||||||
|
<th>Тип работы</th><th>Статус</th><th>Нарушений</th><th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="no-data" id="noDataRow" style="display:none">
|
||||||
|
<span class="icon">📭</span><p>Нет записей. Создайте первый аудит!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== DATA ==================== -->
|
||||||
|
<script>
|
||||||
|
// ========== CATEGORIES DEFINITION (matching Word doc) ==========
|
||||||
|
const CATEGORIES = [
|
||||||
|
{
|
||||||
|
id:'reaction', title:'1. Реакция работника',
|
||||||
|
items:[
|
||||||
|
'Приводит в порядок СИЗ','Меняет положение','Перестраивает работу','Прекращает работу',
|
||||||
|
'Наклоняется, прячется','Меняет инструмент','Подсоединяет или устанавливает необходимые защитные устройства','Другое'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:'posture', title:'2. Положение/поза работника',
|
||||||
|
items:[
|
||||||
|
'Столкновения и удары','Защемление предметом','Падение','Повторяющиеся движения',
|
||||||
|
'Статичные позы','Другое'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:'ppe', title:'3. Отсутствие СИЗ',
|
||||||
|
items:[
|
||||||
|
'Голова (каски, подшлемник и т.д.)','Уши (беруши, наушники)','Глаза и лицо (щитки, очки, маски и т.д.)',
|
||||||
|
'Органы дыхания (противогазы, респираторы, маски и т.п.)','Руки (перчатки, рукавицы и т.д.)',
|
||||||
|
'Тела (спецодежда, фартук, страховочный пояс)','Ноги (спец обувь)','Другое'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:'tools', title:'4. Инструменты и оборудование',
|
||||||
|
items:[
|
||||||
|
'Используется самодельный инструмент','Инструменты в ненадлежащем состоянии','Инструменты используются не по назначению',
|
||||||
|
'Оборудование находится в ненадлежащем состоянии','Лестницы и стремянки отсутствуют, используются неправильно или находятся в ненадлежащем состоянии',
|
||||||
|
'Ограждения отсутствуют, используются неправильно или находятся в ненадлежащем состоянии','Переносное освещение находится в ненадлежащем состоянии','Другое'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:'rules', title:'5. Инструкции и правила',
|
||||||
|
items:[
|
||||||
|
'Отсутствие наряда','Инструкции не соответствуют выполняемым работам',
|
||||||
|
'Требования инструкций и/или правил безопасности не соблюдаются','Инструктажи не проведены',
|
||||||
|
'В недостаточной степени прописаны и выполнены технические мероприятия','В недостаточной степени выполнены подготовка рабочего места и допуск',
|
||||||
|
'В недостаточной степени заполнен наряд (необходимые к заполнению графы)',
|
||||||
|
'Отсутствие удостоверения у работника','Неприменение СИЗ при их наличии во время аудита','Другое'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:'conditions', title:'6. Условия на рабочем месте',
|
||||||
|
items:[
|
||||||
|
'Шум','Освещенность','Пыль','Задымленность','Беспорядок на рабочем месте',
|
||||||
|
'Загромождение путей прохода','Нерациональное размещение инструментов, приборов, оборудования',
|
||||||
|
'Повышенная температура/Пониженная температура','Другое'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:'transport', title:'7. Транспорт',
|
||||||
|
items:[
|
||||||
|
'Ремни безопасности отсутствуют, неисправны или не используются',
|
||||||
|
'Опасный стиль вождения (резкий разгон/торможение, опасное маневрирование, создание аварийной ситуации)',
|
||||||
|
'Состояние водителя не соответствует требованиям',
|
||||||
|
'Использование мобильного средства связи во время движения',
|
||||||
|
'Несоблюдение правил дорожного движения (Скоростной режим, несоблюдение знаков и дорожной разметки)',
|
||||||
|
'Состояние транспортного средства не соответствует требованиям безопасности','Другое'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========== USERS ==========
|
||||||
|
const USERS = {
|
||||||
|
admin:{pass:'admin',name:'Администратор',role:'Руководитель'},
|
||||||
|
auditor:{pass:'auditor',name:'Петров П.П.',role:'Аудитор'},
|
||||||
|
ivanov:{pass:'1234',name:'Иванов И.И.',role:'Бригадир'}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== STATE ==========
|
||||||
|
let currentUser=null,currentPanel='newAudit',editId=null,charts={};
|
||||||
|
|
||||||
|
// ========== INIT ==========
|
||||||
|
function init(){
|
||||||
|
document.getElementById('pabDate').value=new Date().toISOString().split('T')[0];
|
||||||
|
buildCategorySections();
|
||||||
|
initVioRows();
|
||||||
|
if(localStorage.getItem('safetyAuditUser')){
|
||||||
|
currentUser=JSON.parse(localStorage.getItem('safetyAuditUser'));
|
||||||
|
document.getElementById('pabObserver').value=currentUser.name;
|
||||||
|
showApp();
|
||||||
|
}
|
||||||
|
document.getElementById('loginUser').addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();});
|
||||||
|
document.getElementById('loginPass').addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();});
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
|
||||||
|
// ========== BUILD FORM SECTIONS ==========
|
||||||
|
function buildCategorySections(){
|
||||||
|
const container=document.getElementById('categorySections');
|
||||||
|
let html='';
|
||||||
|
CATEGORIES.forEach((cat,idx)=>{
|
||||||
|
const colClass=cat.items.length>7?'col3':(cat.items.length<=4?'col1':'');
|
||||||
|
html+=`
|
||||||
|
<div class="cat-section" id="cat-${cat.id}">
|
||||||
|
<div class="cat-header" onclick="toggleCat('${cat.id}')">
|
||||||
|
<span class="cat-title">${cat.title}</span>
|
||||||
|
<span class="cat-badge all-safe" id="badge-${cat.id}">ВСЕ БЕЗОПАСНО</span>
|
||||||
|
<span class="cat-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="cat-body open" id="body-${cat.id}">
|
||||||
|
<div class="all-safe-toggle active" id="allSafeToggle-${cat.id}" onclick="toggleAllSafe('${cat.id}')">
|
||||||
|
<input type="checkbox" checked id="allSafeCb-${cat.id}"> ВСЕ БЕЗОПАСНО
|
||||||
|
</div>
|
||||||
|
<div class="checklist ${colClass}">
|
||||||
|
${cat.items.map((item,i)=>`
|
||||||
|
<div class="check-item" id="item-${cat.id}-${i}">
|
||||||
|
<input type="checkbox" id="cb-${cat.id}-${i}" onchange="onCheckItem('${cat.id}',${i})">
|
||||||
|
<div>
|
||||||
|
<label for="cb-${cat.id}-${i}">${item}</label>
|
||||||
|
${item==='Другое'?`<input class="other-input" id="other-${cat.id}" placeholder="Укажите..." onchange="onCheckItem('${cat.id}',${i})">`:''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cat-footer">
|
||||||
|
<span>Итого количество: <span class="total-count zero" id="total-${cat.id}">0</span></span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML=html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCat(id){
|
||||||
|
document.getElementById('body-'+id).classList.toggle('open');
|
||||||
|
document.querySelector('#cat-'+id+' .cat-header').classList.toggle('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAllSafe(catId){
|
||||||
|
const cb=document.getElementById('allSafeCb-'+catId);
|
||||||
|
const toggle=document.getElementById('allSafeToggle-'+catId);
|
||||||
|
const isAllSafe=!cb.checked;
|
||||||
|
cb.checked=isAllSafe;
|
||||||
|
if(isAllSafe){
|
||||||
|
toggle.classList.add('active');
|
||||||
|
CATEGORIES.find(c=>c.id===catId).items.forEach((_,i)=>{
|
||||||
|
const el=document.getElementById('cb-'+catId+'-'+i);
|
||||||
|
if(el){el.checked=false; updateCheckItemUI(catId,i);}
|
||||||
|
const other=document.getElementById('other-'+catId);
|
||||||
|
if(other)other.style.display='none';
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
toggle.classList.remove('active');
|
||||||
|
}
|
||||||
|
updateCatTotal(catId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCheckItem(catId,idx){
|
||||||
|
const cb=document.getElementById('cb-'+catId+'-'+idx);
|
||||||
|
updateCheckItemUI(catId,idx);
|
||||||
|
// Uncheck all-safe if any item checked
|
||||||
|
if(cb.checked){
|
||||||
|
document.getElementById('allSafeCb-'+catId).checked=false;
|
||||||
|
document.getElementById('allSafeToggle-'+catId).classList.remove('active');
|
||||||
|
}
|
||||||
|
// Show/hide "other" input
|
||||||
|
const cat=CATEGORIES.find(c=>c.id===catId);
|
||||||
|
if(cat&&cat.items[idx]==='Другое'){
|
||||||
|
const other=document.getElementById('other-'+catId);
|
||||||
|
if(other){other.style.display=cb.checked?'block':'none'; other.classList.toggle('visible',cb.checked);}
|
||||||
|
}
|
||||||
|
updateCatTotal(catId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCheckItemUI(catId,idx){
|
||||||
|
const el=document.getElementById('item-'+catId+'-'+idx);
|
||||||
|
const cb=document.getElementById('cb-'+catId+'-'+idx);
|
||||||
|
if(cb.checked){el.classList.add('checked');}else{el.classList.remove('checked');}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCatTotal(catId){
|
||||||
|
const cat=CATEGORIES.find(c=>c.id===catId);
|
||||||
|
let count=0;
|
||||||
|
cat.items.forEach((_,i)=>{
|
||||||
|
if(document.getElementById('cb-'+catId+'-'+i)?.checked)count++;
|
||||||
|
});
|
||||||
|
const totalEl=document.getElementById('total-'+catId);
|
||||||
|
totalEl.textContent=count;
|
||||||
|
totalEl.classList.toggle('zero',count===0);
|
||||||
|
const badge=document.getElementById('badge-'+catId);
|
||||||
|
if(count===0){badge.textContent='ВСЕ БЕЗОПАСНО'; badge.classList.add('all-safe');}
|
||||||
|
else{badge.textContent='Нарушений: '+count; badge.classList.remove('all-safe');}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOverall(type){
|
||||||
|
document.getElementById('overallSafe').classList.toggle('selected',type==='safe');
|
||||||
|
document.getElementById('overallDanger').classList.toggle('selected',type==='danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== VIOLATIONS TABLE ==========
|
||||||
|
let vioRowCount=6;
|
||||||
|
function initVioRows(){
|
||||||
|
const container=document.getElementById('vioRows');
|
||||||
|
let html='';
|
||||||
|
for(let i=0;i<vioRowCount;i++){
|
||||||
|
html+=makeVioRow(i+1);
|
||||||
|
}
|
||||||
|
container.innerHTML=html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeVioRow(num){
|
||||||
|
return `<div class="vio-grid" id="vioRow${num}" style="display:grid">
|
||||||
|
<span class="vio-row-num">${num}</span>
|
||||||
|
<input placeholder="Описание несоответствия" class="v-nc">
|
||||||
|
<input placeholder="Исполнитель" class="v-exec">
|
||||||
|
<select class="v-type"><option>Нарушение</option><option>Замечание</option><option>Риск</option></select>
|
||||||
|
<input placeholder="Корректирующие меры" class="v-measure">
|
||||||
|
<input placeholder="Ответственный" class="v-resp">
|
||||||
|
<input type="date" class="v-date">
|
||||||
|
<input placeholder="Форма завершения" class="v-done">
|
||||||
|
<button class="remove-vio-btn" onclick="removeVioRow(${num})" title="Удалить">×</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVioRow(){
|
||||||
|
vioRowCount++;
|
||||||
|
const row=makeVioRow(vioRowCount);
|
||||||
|
document.getElementById('vioRows').insertAdjacentHTML('beforeend',row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeVioRow(num){
|
||||||
|
if(vioRowCount<=1)return;
|
||||||
|
document.getElementById('vioRow'+num)?.remove();
|
||||||
|
// Reindex
|
||||||
|
document.querySelectorAll('#vioRows .vio-grid').forEach((row,i)=>{
|
||||||
|
row.id='vioRow'+(i+1);
|
||||||
|
row.querySelector('.vio-row-num').textContent=i+1;
|
||||||
|
row.querySelector('.remove-vio-btn').setAttribute('onclick','removeVioRow('+(i+1)+')');
|
||||||
|
});
|
||||||
|
vioRowCount=document.querySelectorAll('#vioRows .vio-grid').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVioRows(){
|
||||||
|
const rows=[];
|
||||||
|
document.querySelectorAll('#vioRows .vio-grid').forEach(row=>{
|
||||||
|
const nc=row.querySelector('.v-nc')?.value?.trim();
|
||||||
|
if(!nc)return;
|
||||||
|
rows.push({
|
||||||
|
nc:nc,
|
||||||
|
executor:row.querySelector('.v-exec')?.value?.trim()||'',
|
||||||
|
type:row.querySelector('.v-type')?.value||'Нарушение',
|
||||||
|
measure:row.querySelector('.v-measure')?.value?.trim()||'',
|
||||||
|
responsible:row.querySelector('.v-resp')?.value?.trim()||'',
|
||||||
|
date:row.querySelector('.v-date')?.value||'',
|
||||||
|
done:row.querySelector('.v-done')?.value?.trim()||''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== AUDIT SUBMIT ==========
|
||||||
|
function submitAudit(){
|
||||||
|
const location=document.getElementById('pabLocation').value.trim();
|
||||||
|
if(!location){alert('Укажите место проведения ПАБ');return;}
|
||||||
|
|
||||||
|
const cats={};
|
||||||
|
let totalViolations=0;
|
||||||
|
CATEGORIES.forEach(cat=>{
|
||||||
|
const checked=[];
|
||||||
|
cat.items.forEach((item,i)=>{
|
||||||
|
const cb=document.getElementById('cb-'+cat.id+'-'+i);
|
||||||
|
if(cb&&cb.checked){
|
||||||
|
const otherVal=item==='Другое'?document.getElementById('other-'+cat.id)?.value?.trim()||'':null;
|
||||||
|
checked.push({item,other:otherVal});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cats[cat.id]={items:checked,allSafe:checked.length===0};
|
||||||
|
totalViolations+=checked.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const overallSafe=document.getElementById('overallSafe').classList.contains('selected');
|
||||||
|
|
||||||
|
const entry={
|
||||||
|
id:editId||Date.now(),
|
||||||
|
number:document.getElementById('pabNumber').value.trim(),
|
||||||
|
date:document.getElementById('pabDate').value,
|
||||||
|
timeStart:document.getElementById('pabTimeStart').value,
|
||||||
|
timeEnd:document.getElementById('pabTimeEnd').value,
|
||||||
|
location:location,
|
||||||
|
workType:document.getElementById('pabWorkType').value.trim(),
|
||||||
|
workerCount:parseInt(document.getElementById('pabWorkerCount').value)||1,
|
||||||
|
observer:document.getElementById('pabObserver').value.trim()||currentUser.name,
|
||||||
|
observerRole:document.getElementById('pabObserverRole').value.trim(),
|
||||||
|
supervisor:document.getElementById('pabSupervisor').value.trim(),
|
||||||
|
supervisorRole:document.getElementById('pabSupervisorRole').value.trim(),
|
||||||
|
overallSafe:overallSafe,
|
||||||
|
categories:cats,
|
||||||
|
totalViolations:totalViolations,
|
||||||
|
violations:getVioRows(),
|
||||||
|
createdBy:currentUser.login,
|
||||||
|
createdAt:new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
let audits=getAudits();
|
||||||
|
if(editId){audits=audits.map(a=>a.id===editId?entry:a);editId=null;}
|
||||||
|
else{audits.unshift(entry);}
|
||||||
|
saveAudits(audits);
|
||||||
|
resetAuditForm();
|
||||||
|
const s=document.getElementById('formSuccess');
|
||||||
|
s.style.display='block';
|
||||||
|
setTimeout(()=>s.style.display='none',3000);
|
||||||
|
window.scrollTo({top:0,behavior:'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAuditForm(){
|
||||||
|
document.getElementById('pabNumber').value='';
|
||||||
|
document.getElementById('pabDate').value=new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('pabTimeStart').value='';
|
||||||
|
document.getElementById('pabTimeEnd').value='';
|
||||||
|
document.getElementById('pabLocation').value='';
|
||||||
|
document.getElementById('pabWorkType').value='';
|
||||||
|
document.getElementById('pabWorkerCount').value='1';
|
||||||
|
document.getElementById('pabObserver').value=currentUser?currentUser.name:'';
|
||||||
|
document.getElementById('pabObserverRole').value='';
|
||||||
|
document.getElementById('pabSupervisor').value='';
|
||||||
|
document.getElementById('pabSupervisorRole').value='';
|
||||||
|
setOverall('safe');
|
||||||
|
editId=null;
|
||||||
|
CATEGORIES.forEach(cat=>{
|
||||||
|
document.getElementById('allSafeCb-'+cat.id).checked=true;
|
||||||
|
document.getElementById('allSafeToggle-'+cat.id).classList.add('active');
|
||||||
|
cat.items.forEach((_,i)=>{
|
||||||
|
const cb=document.getElementById('cb-'+cat.id+'-'+i);
|
||||||
|
if(cb){cb.checked=false; updateCheckItemUI(cat.id,i);}
|
||||||
|
const other=document.getElementById('other-'+cat.id);
|
||||||
|
if(other){other.value='';other.style.display='none';other.classList.remove('visible');}
|
||||||
|
});
|
||||||
|
updateCatTotal(cat.id);
|
||||||
|
});
|
||||||
|
document.getElementById('vioRows').innerHTML='';
|
||||||
|
vioRowCount=6;
|
||||||
|
initVioRows();
|
||||||
|
document.getElementById('formSuccess').style.display='none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DATA STORAGE ==========
|
||||||
|
function getAudits(){try{return JSON.parse(localStorage.getItem('safetyAudits')||'[]')}catch(e){return[]}}
|
||||||
|
function saveAudits(data){localStorage.setItem('safetyAudits',JSON.stringify(data))}
|
||||||
|
|
||||||
|
// ========== LOGIN ==========
|
||||||
|
function doLogin(){
|
||||||
|
const u=document.getElementById('loginUser').value.trim().toLowerCase();
|
||||||
|
const p=document.getElementById('loginPass').value.trim();
|
||||||
|
const err=document.getElementById('loginError');
|
||||||
|
if(!USERS[u]||USERS[u].pass!==p){err.style.display='block';return;}
|
||||||
|
err.style.display='none';
|
||||||
|
currentUser={login:u,...USERS[u]};
|
||||||
|
localStorage.setItem('safetyAuditUser',JSON.stringify(currentUser));
|
||||||
|
document.getElementById('pabObserver').value=currentUser.name;
|
||||||
|
showApp();
|
||||||
|
}
|
||||||
|
function doLogout(){
|
||||||
|
localStorage.removeItem('safetyAuditUser');currentUser=null;
|
||||||
|
document.getElementById('loginScreen').style.display='flex';
|
||||||
|
document.getElementById('appScreen').style.display='none';
|
||||||
|
document.getElementById('loginUser').value='';document.getElementById('loginPass').value='';
|
||||||
|
}
|
||||||
|
function showApp(){
|
||||||
|
document.getElementById('loginScreen').style.display='none';
|
||||||
|
document.getElementById('appScreen').style.display='block';
|
||||||
|
document.getElementById('displayName').textContent=currentUser.login+' ('+currentUser.role+')';
|
||||||
|
switchPanel('newAudit',document.querySelector('[data-panel="newAudit"]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== PANELS ==========
|
||||||
|
function switchPanel(name,el){
|
||||||
|
currentPanel=name;
|
||||||
|
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
document.getElementById('panel'+name.charAt(0).toUpperCase()+name.slice(1)).classList.add('active');
|
||||||
|
document.querySelectorAll('nav a').forEach(a=>a.classList.remove('active'));
|
||||||
|
if(el)el.classList.add('active');
|
||||||
|
if(name==='dashboard')renderDashboard();
|
||||||
|
if(name==='history')renderHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DASHBOARD ==========
|
||||||
|
function renderDashboard(){
|
||||||
|
const audits=getAudits();
|
||||||
|
const total=audits.length;
|
||||||
|
const allSafe=audits.filter(a=>a.overallSafe).length;
|
||||||
|
const withDanger=audits.filter(a=>!a.overallSafe).length;
|
||||||
|
const totalVio=audits.reduce((s,a)=>s+(a.totalViolations||0),0);
|
||||||
|
|
||||||
|
document.getElementById('statTotal').textContent=total;
|
||||||
|
document.getElementById('statAllSafe').textContent=allSafe;
|
||||||
|
document.getElementById('statWithDanger').textContent=withDanger;
|
||||||
|
document.getElementById('statViolations').textContent=totalVio;
|
||||||
|
|
||||||
|
const sp=total>0?(allSafe/total*100):50;
|
||||||
|
const dp=total>0?(withDanger/total*100):50;
|
||||||
|
document.getElementById('riskSafeBar').style.width=sp+'%';
|
||||||
|
document.getElementById('riskUnsafeBar').style.width=dp+'%';
|
||||||
|
document.getElementById('riskSafeLabel').textContent=allSafe;
|
||||||
|
document.getElementById('riskUnsafeLabel').textContent=withDanger;
|
||||||
|
|
||||||
|
Object.values(charts).forEach(c=>{try{c.destroy()}catch(e){}});
|
||||||
|
charts={};
|
||||||
|
|
||||||
|
// By category
|
||||||
|
const catLabels=CATEGORIES.map(c=>c.title.split('. ')[1]);
|
||||||
|
const catCounts=CATEGORIES.map(cat=>{
|
||||||
|
return audits.reduce((s,a)=>{
|
||||||
|
const c=a.categories&&a.categories[cat.id];
|
||||||
|
return s+(c?c.items.length:0);
|
||||||
|
},0);
|
||||||
|
});
|
||||||
|
const ctx1=document.getElementById('chartCategories').getContext('2d');
|
||||||
|
charts.cat=new Chart(ctx1,{
|
||||||
|
type:'bar',data:{labels:catLabels,datasets:[{label:'Нарушений',data:catCounts,backgroundColor:'#E63946',borderRadius:6}]},
|
||||||
|
options:{responsive:true,plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{stepSize:1}}}}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeline
|
||||||
|
const dates={};
|
||||||
|
audits.forEach(a=>{
|
||||||
|
if(!dates[a.date])dates[a.date]=0;
|
||||||
|
dates[a.date]+=(a.totalViolations||0);
|
||||||
|
});
|
||||||
|
const sd=Object.keys(dates).sort();
|
||||||
|
const ctx2=document.getElementById('chartTimeline').getContext('2d');
|
||||||
|
charts.tl=new Chart(ctx2,{
|
||||||
|
type:'line',data:{labels:sd,datasets:[{label:'Нарушений',data:sd.map(d=>dates[d]),borderColor:'#E63946',backgroundColor:'rgba(230,57,70,0.08)',fill:true,tension:0.3,pointRadius:5}]},
|
||||||
|
options:{responsive:true,plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{stepSize:1}}}}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top observers
|
||||||
|
const obs={};
|
||||||
|
audits.forEach(a=>{obs[a.observer]=(obs[a.observer]||0)+1});
|
||||||
|
const obsS=Object.entries(obs).sort((a,b)=>b[1]-a[1]).slice(0,5);
|
||||||
|
const ctx3=document.getElementById('chartObservers').getContext('2d');
|
||||||
|
charts.obs=new Chart(ctx3,{
|
||||||
|
type:'bar',data:{labels:obsS.map(o=>o[0]),datasets:[{label:'Аудитов',data:obsS.map(o=>o[1]),backgroundColor:['#00B4D8','#48CAE4','#90E0EF','#0077B6','#023E8A'],borderRadius:6}]},
|
||||||
|
options:{indexAxis:'y',responsive:true,plugins:{legend:{display:false}},scales:{x:{beginAtZero:true,ticks:{stepSize:1}}}}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top violation items
|
||||||
|
const itemCounts={};
|
||||||
|
audits.forEach(a=>{
|
||||||
|
if(a.categories){
|
||||||
|
Object.values(a.categories).forEach(cat=>{
|
||||||
|
if(cat.items){
|
||||||
|
cat.items.forEach(it=>{
|
||||||
|
const key=it.item;
|
||||||
|
itemCounts[key]=(itemCounts[key]||0)+1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const topItems=Object.entries(itemCounts).sort((a,b)=>b[1]-a[1]).slice(0,10);
|
||||||
|
const ctx4=document.getElementById('chartTopItems').getContext('2d');
|
||||||
|
charts.top=new Chart(ctx4,{
|
||||||
|
type:'bar',data:{labels:topItems.map(i=>i[0].length>30?i[0].slice(0,30)+'...':i[0]),datasets:[{label:'Раз',data:topItems.map(i=>i[1]),backgroundColor:topItems.map((_,i)=>['#E63946','#E76F51','#F4A261','#E9C46A','#2A9D8F','#264653','#00B4D8','#0077B6','#023E8A','#6C757D'][i]||'#E63946'),borderRadius:4}]},
|
||||||
|
options:{indexAxis:'y',responsive:true,plugins:{legend:{display:false}},scales:{x:{beginAtZero:true,ticks:{stepSize:1}}}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== HISTORY ==========
|
||||||
|
function renderHistory(){
|
||||||
|
let audits=getAudits();
|
||||||
|
const fOverall=document.getElementById('filterOverall').value;
|
||||||
|
const fDate=document.getElementById('filterDate').value;
|
||||||
|
const fLoc=document.getElementById('filterLocation').value.toLowerCase();
|
||||||
|
if(fOverall==='safe')audits=audits.filter(a=>a.overallSafe);
|
||||||
|
if(fOverall==='danger')audits=audits.filter(a=>!a.overallSafe);
|
||||||
|
if(fDate)audits=audits.filter(a=>a.date===fDate);
|
||||||
|
if(fLoc)audits=audits.filter(a=>a.location.toLowerCase().includes(fLoc));
|
||||||
|
|
||||||
|
const tbody=document.getElementById('historyBody');
|
||||||
|
const noData=document.getElementById('noDataRow');
|
||||||
|
if(audits.length===0){tbody.innerHTML='';noData.style.display='block';return;}
|
||||||
|
noData.style.display='none';
|
||||||
|
tbody.innerHTML=audits.map(a=>`
|
||||||
|
<tr>
|
||||||
|
<td>${a.number||'—'}</td>
|
||||||
|
<td>${a.date}</td>
|
||||||
|
<td>${a.timeStart||'—'} — ${a.timeEnd||'—'}</td>
|
||||||
|
<td>${a.location}</td>
|
||||||
|
<td>${a.observer}</td>
|
||||||
|
<td>${a.workType||'—'}</td>
|
||||||
|
<td><span class="badge ${a.overallSafe?'badge-safe':'badge-danger'}">${a.overallSafe?'Безопасно':'Нарушения'}</span></td>
|
||||||
|
<td>${a.totalViolations||0}</td>
|
||||||
|
<td>
|
||||||
|
<a class="view-link" onclick="viewAudit(${a.id})">👁️</a>
|
||||||
|
<button class="btn btn-danger btn-sm" style="margin-left:6px" onclick="deleteAudit(${a.id})">🗑️</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewAudit(id){
|
||||||
|
const a=getAudits().find(x=>x.id===id);
|
||||||
|
if(!a)return;
|
||||||
|
let text=`БЛАНК ПАБ №${a.number||'—'}\n`;
|
||||||
|
text+=`Дата: ${a.date} | Время: ${a.timeStart||'—'} — ${a.timeEnd||'—'}\n`;
|
||||||
|
text+=`Место: ${a.location} | Тип работы: ${a.workType||'—'}\n`;
|
||||||
|
text+=`Наблюдатель: ${a.observer} (${a.observerRole||'—'})\n`;
|
||||||
|
text+=`Руководитель: ${a.supervisor||'—'} (${a.supervisorRole||'—'})\n`;
|
||||||
|
text+=`Статус: ${a.overallSafe?'ВСЕ БЕЗОПАСНО':'ЕСТЬ ОПАСНО'}\n`;
|
||||||
|
text+=`Всего нарушений: ${a.totalViolations||0}\n\n`;
|
||||||
|
text+=`=== КАТЕГОРИИ ===\n`;
|
||||||
|
CATEGORIES.forEach(cat=>{
|
||||||
|
const cdata=a.categories&&a.categories[cat.id];
|
||||||
|
const items=cdata?cdata.items:[];
|
||||||
|
text+=`\n${cat.title}: ${items.length===0?'ВСЕ БЕЗОПАСНО':items.length+' наруш.'}\n`;
|
||||||
|
items.forEach(it=>text+=` ☒ ${it.item}${it.other?' — '+it.other:''}\n`);
|
||||||
|
});
|
||||||
|
if(a.violations&&a.violations.length>0){
|
||||||
|
text+=`\n\n=== НЕСООТВЕТСТВИЯ ===\n`;
|
||||||
|
a.violations.forEach((v,i)=>text+=`${i+1}. ${v.nc} | Исп: ${v.executor} | Меры: ${v.measure} | Отв: ${v.responsible}\n`);
|
||||||
|
}
|
||||||
|
alert(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAudit(id){
|
||||||
|
if(!confirm('Удалить этот аудит?'))return;
|
||||||
|
saveAudits(getAudits().filter(a=>a.id!==id));
|
||||||
|
renderHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== EXPORT CSV ==========
|
||||||
|
function exportCSV(){
|
||||||
|
const audits=getAudits();
|
||||||
|
if(audits.length===0){alert('Нет данных');return;}
|
||||||
|
const header='Бланк №;Дата;Время;Место;Тип работы;Наблюдатель;Должность;Руководитель;Должность;Статус;Нарушений всего;Категории с нарушениями';
|
||||||
|
const rows=audits.map(a=>{
|
||||||
|
const catsWithVio=CATEGORIES.filter(cat=>{
|
||||||
|
const c=a.categories&&a.categories[cat.id];
|
||||||
|
return c&&c.items.length>0;
|
||||||
|
}).map(c=>c.title.split('. ')[1]).join(', ');
|
||||||
|
return `${a.number||''};${a.date};${a.timeStart||''}-${a.timeEnd||''};"${a.location}";"${a.workType||''}";"${a.observer}";"${a.observerRole||''}";"${a.supervisor||''}";"${a.supervisorRole||''}";${a.overallSafe?'Безопасно':'Нарушения'};${a.totalViolations||0};"${catsWithVio}"`;
|
||||||
|
});
|
||||||
|
const csv='\uFEFF'+header+'\n'+rows.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='pab-audit.csv';a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user