From 29b1aafb15507422e74e74af1a128ed08e22ec10 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Wed, 10 Jun 2026 11:25:45 +0000 Subject: [PATCH] refactor: remove backend, extract CSS/JS, apply design.md palette --- index.html | 694 ++++------------- script.js | 789 +++++++++++++++++++ style.css | 2147 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 3104 insertions(+), 526 deletions(-) create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html index ba399e5..4f8b249 100644 --- a/index.html +++ b/index.html @@ -4,547 +4,189 @@ ИИ-агент мониторинга ПБ — АО «Самрук-Казына» - + -
-
-
Цифровая платформа
-

ИИ-агент мониторинга производственной безопасности

-

- Единая цифровая платформа с искусственным интеллектом для автоматизации контроля исполнения мероприятий, анализа эффективности и подготовки управленческих решений в Группе компаний АО «Самрук-Казына». -

- Узнать подробнее - Возможности системы + +
+ -
+ -
-
-
-
-
-80%
-
Сокращение ручного сбора отчётности
-
-
-
100%
-
Прозрачность контроля исполнения
-
-
-
24/7
-
Мониторинг в реальном времени
-
-
-
x5
-
Быстрее подготовка отчётов
-
+ +
-
-
- -

Модули системы

-

Платформа охватывает все ключевые процессы управления производственной безопасностью — от сбора данных до отчётности перед руководством.

- -
-
-
📊
-

Централизованный сбор данных

-

Сбор отчётности от филиалов и ДО через веб-интерфейс. Единая база данных исполнения Плана.

-
-
-
📋
-

Управление мероприятиями

-

Электронный реестр, назначение ответственных, контрольные сроки и мониторинг статусов.

-
-
-
📎
-

Подтверждение исполнения

-

Загрузка фото, актов, протоколов, приказов, презентаций и видео. Электронный архив материалов.

-
-
-
📃
-

Цифровой паспорт мероприятия

-

Карточка с описанием, сроками, исполнителями, статусом, историей изменений и выводами ИИ.

-
-
-
🤖
-

Искусственный интеллект

-

Анализ отчётов, проверка полноты, выявление рисков, формирование выводов и рекомендаций.

-
-
-
-

Контроль дисциплины

-

Мониторинг сроков, автонапоминания, эскалация просрочек, рейтинг организаций.

-
-
-
📈
-

Аналитика и дашборды

-

Процент исполнения, рейтинг качества, анализ просрочек, карта рисков, динамика по периодам.

-
-
-
📄
-

Формирование отчётности

-

Автоматические ежемесячные, квартальные и годовые отчёты, справки и презентации.

-
-
-
👥
-

Управленческий помощник

-

Подготовка проектов поручений, выявление системных нарушений, прогноз достижения KPI.

-
-
-
-
- -
-
- -
-
-
🤖
-

ИИ-агент анализирует и рекомендует

-

Система не просто собирает данные — она помогает принимать решения.

-
-
-
    -
  • Анализ представленных отчётов и подтверждающих материалов
  • -
  • Проверка полноты и достаточности подтверждения исполнения
  • -
  • Выявление отсутствующих документов
  • -
  • Оценка рисков нарушения сроков
  • -
  • Автоматическое формирование выводов и рекомендаций
  • -
  • Подготовка кратких аналитических справок для руководства
  • -
-
-
-
-
- -
-
- -

Что изменится после внедрения

-

Цифровизация процессов производственной безопасности принесёт измеримые результаты.

- -
-
-
-80%
-

Сокращение ручного сбора отчётности и времени на подготовку сводок

-
-
-
100%
-

Достоверность и прозрачность контроля исполнения каждого мероприятия

-
-
-
⚠ 0
-

Пропущенных сроков — оперативное выявление рисков и нарушений

-
-
-
Real-time
-

Формирование управленческой отчётности в режиме реального времени

-
-
-
-
- -
-
- -

Встраивается в существующую ИТ-инфраструктуру

-

Платформа интегрируется с корпоративными системами холдинга.

-
- Корпоративная электронная почта - Системы электронного документооборота - Корпоративные BI-платформы - Excel / Word / PDF -
-
-
- -
-
-
-

Узнайте подробнее о системе

-

Оставьте контакты — мы свяжемся, чтобы провести демонстрацию платформы и обсудить внедрение.

- -
-
- - +
+ +
+
+ - + + + +
+ × +

Уведомления

+
+
+ + diff --git a/script.js b/script.js new file mode 100644 index 0000000..0db9ec3 --- /dev/null +++ b/script.js @@ -0,0 +1,789 @@ +// ─── Seed Data ────────────────────────────────────────────────────────────── +const ALL_EVENTS = [ + { + id: "evt-1", title: "Проведение вводного инструктажа по пожарной безопасности", responsible: "Ахметов Т.К.", + section: "Обучение и культура безопасности", deadline: "2026-06-15", status: "completed", + branch: "Дирекция ПБ", region: "Астана", description: "Инструктаж проведён для 45 сотрудников", + quantity: 45, subItems: [ + { id: "1.1", title: "Подготовка материалов инструктажа", status: "completed" }, + { id: "1.2", title: "Проведение практического занятия", status: "completed" } + ], history: [ + { timestamp: "2026-06-01T10:00:00", action: "created" }, + { timestamp: "2026-06-10T14:30:00", action: "updated", changes: { status: "completed" } } + ], created_at: "2026-06-01" + }, + { + id: "evt-2", title: "Проверка систем автоматического пожаротушения на объектах", responsible: "Сериков Д.А.", + section: "Техническая безопасность", deadline: "2026-09-20", status: "in_progress", + branch: "Дивизион «Сеть»", region: "Алматы", description: "Проверено 12 из 18 объектов", + quantity: 12, subItems: [ + { id: "2.1", title: "Инвентаризация систем пожаротушения", status: "completed" }, + { id: "2.2", title: "Техническое тестирование датчиков", status: "in_progress" }, + { id: "2.3", title: "Замена неисправных элементов", status: "pending" } + ], history: [ + { timestamp: "2026-05-15T09:00:00", action: "created" }, + { timestamp: "2026-06-05T11:00:00", action: "updated", changes: { status: "in_progress", quantity: 12 } } + ], created_at: "2026-05-15" + }, + { + id: "evt-3", title: "Разработка плана эвакуации для нового офисного здания", responsible: "Куанышева А.М.", + section: "Готовность к ЧС", deadline: "2026-08-01", status: "pending", + branch: "Корпоративный бизнес", region: "Шымкент", description: "Требуется разработка плана с учётом новых нормативов", + quantity: 0, subItems: [], history: [ + { timestamp: "2026-06-01T08:00:00", action: "created" } + ], created_at: "2026-06-01" + }, + { + id: "evt-4", title: "Замена огнетушителей с истекшим сроком годности", responsible: "Нурланов Е.С.", + section: "Техническая безопасность", deadline: "2026-04-15", status: "overdue", + branch: "Розничный бизнес", region: "Караганда", description: "Срок истёк 15 апреля, срочно требуется замена 67 единиц", + quantity: 67, subItems: [ + { id: "4.1", title: "Инвентаризация огнетушителей", status: "completed" }, + { id: "4.2", title: "Закупка новых огнетушителей", status: "overdue" }, + { id: "4.3", title: "Установка и утилизация старых", status: "pending" } + ], history: [ + { timestamp: "2026-03-01T10:00:00", action: "created" }, + { timestamp: "2026-05-20T09:00:00", action: "updated", changes: { status: "overdue" } } + ], created_at: "2026-03-01" + }, + { + id: "evt-5", title: "Обучение персонала оказанию первой помощи", responsible: "Мусаева Г.Р.", + section: "Обучение и культура безопасности", deadline: "2026-06-20", status: "in_progress", + branch: "Корп. университет", region: "Астана", description: "Проведено 3 из 8 запланированных тренингов", + quantity: 3, subItems: [ + { id: "5.1", title: "Программа обучения", status: "completed" }, + { id: "5.2", title: "Тренинг для руководителей", status: "completed" }, + { id: "5.3", title: "Тренинг для линейного персонала", status: "in_progress" }, + { id: "5.4", title: "Итоговая аттестация", status: "pending" } + ], history: [ + { timestamp: "2026-04-01T08:00:00", action: "created" }, + { timestamp: "2026-05-15T14:00:00", action: "updated", changes: { quantity: 3 } } + ], created_at: "2026-04-01" + }, + { + id: "evt-6", title: "Внедрение цифровой системы учёта происшествий", responsible: "Тулегенов Б.К.", + section: "Цифровизация", deadline: "2026-05-30", status: "completed", + branch: "Цифровой бизнес", region: "Алматы", description: "Система запущена в промышленную эксплуатацию, обучено 120 пользователей", + quantity: 120, subItems: [ + { id: "6.1", title: "Разработка ТЗ", status: "completed" }, + { id: "6.2", title: "Пилотное внедрение", status: "completed" }, + { id: "6.3", title: "Обучение пользователей", status: "completed" } + ], history: [ + { timestamp: "2026-02-01T09:00:00", action: "created" }, + { timestamp: "2026-05-30T16:00:00", action: "updated", changes: { status: "completed", quantity: 120 } } + ], created_at: "2026-02-01" + }, + { + id: "evt-7", title: "Проведение учений по ликвидации аварийного разлива нефтепродуктов", responsible: "Рахимов Ж.Н.", + section: "Готовность к ЧС", deadline: "2026-07-25", status: "pending", + branch: "Сервисная фабрика", region: "Атырау", description: "Согласование сценария учений с МЧС", + quantity: 0, subItems: [], history: [ + { timestamp: "2026-06-05T11:00:00", action: "created" } + ], created_at: "2026-06-05" + }, + { + id: "evt-8", title: "Аудит системы управления охраной труда в филиалах", responsible: "Садыкова Л.А.", + section: "Коммуникации", deadline: "2026-03-01", status: "overdue", + branch: "Управление проектами", region: "Актобе", description: "Аудит не проведён, требуется согласование графика", + quantity: 0, subItems: [ + { id: "8.1", title: "Формирование комиссии", status: "completed" }, + { id: "8.2", title: "Проверка документации", status: "overdue" }, + { id: "8.3", title: "Выезд на объекты", status: "pending" } + ], history: [ + { timestamp: "2026-01-10T10:00:00", action: "created" }, + { timestamp: "2026-04-01T08:00:00", action: "updated", changes: { status: "overdue" } } + ], created_at: "2026-01-10" + }, + { + id: "evt-9", title: "Разработка корпоративного стандарта по работе на высоте", responsible: "Касымов А.Д.", + section: "Техническая безопасность", deadline: "2026-10-15", status: "pending", + branch: "Телеком Комплект", region: "Астана", description: "Стандарт находится на стадии согласования", + quantity: 0, subItems: [], history: [ + { timestamp: "2026-06-01T09:00:00", action: "created" } + ], created_at: "2026-06-01" + }, + { + id: "evt-10", title: "Обновление информационных стендов по технике безопасности", responsible: "Ибраева Д.С.", + section: "Коммуникации", deadline: "2026-07-01", status: "in_progress", + branch: "Розничный бизнес", region: "Павлодар", description: "Изготовлено 15 из 24 стендов", + quantity: 15, subItems: [], history: [ + { timestamp: "2026-05-01T10:00:00", action: "created" }, + { timestamp: "2026-06-08T15:00:00", action: "updated", changes: { status: "in_progress", quantity: 15 } } + ], created_at: "2026-05-01" + } +]; + +const BRANCHES = [ + "Дирекция ПБ", "Дивизион «Сеть»", "Корпоративный бизнес", "Розничный бизнес", + "Сервисная фабрика", "Телеком Комплект", "Корп. университет", "Управление проектами", "Цифровой бизнес" +]; + +// ─── Hardcoded Users ────────────────────────────────────────────────────── +const USERS = { + "curator@sk.kz": { name: "Куратор ПБ", password: "1234" }, + "dpp@sk.kz": { name: "Директор ДПБ", password: "1234" } +}; + +// ─── State ──────────────────────────────────────────────────────────────── +let user = null; +let eventsData = []; +let editingId = null; + +// ─── Utils ──────────────────────────────────────────────────────────────── +function daysUntil(d) { + if (!d) return 999; + const t = new Date(d + "T23:59:59"); + return Math.ceil((t - new Date()) / 86400000); +} + +function esc(s) { + const d = document.createElement("div"); + d.textContent = String(s); + return d.innerHTML; +} + +// ─── Auth ───────────────────────────────────────────────────────────────── +function login() { + const email = document.getElementById("loginEmail").value.trim(); + const pass = document.getElementById("loginPass").value.trim(); + const errEl = document.getElementById("loginError"); + + if (!pass) { errEl.textContent = "Введите пароль"; return; } + + const u = USERS[email]; + if (u && u.password === pass) { + user = { email: email, name: u.name }; + localStorage.setItem("sh_token", "ok"); + localStorage.setItem("sh_user", JSON.stringify(user)); + document.getElementById("loginScreen").style.display = "none"; + document.getElementById("app").style.display = "block"; + document.getElementById("userName").textContent = user.name; + document.getElementById("userEmail").textContent = user.email; + init(); + } else { + errEl.textContent = "Неверный email или пароль"; + } +} + +function logout() { + user = null; + localStorage.removeItem("sh_token"); + localStorage.removeItem("sh_user"); + document.getElementById("app").style.display = "none"; + document.getElementById("loginScreen").style.display = "flex"; + document.getElementById("loginEmail").value = ""; + document.getElementById("loginPass").value = ""; + document.getElementById("loginError").textContent = ""; +} + +// ─── Events persistence ─────────────────────────────────────────────────── +function loadEvents() { + let saved = {}; + try { + saved = JSON.parse(localStorage.getItem("sh_events") || "{}"); + } catch (e) { /* ignore */ } + + eventsData = ALL_EVENTS.map(function (e) { + if (saved[e.id]) { + // Merge: override saved fields onto the original, but preserve + // subItems and history from saved if they were explicitly set + const over = saved[e.id]; + const merged = {}; + for (var k in e) { merged[k] = e[k]; } + for (var k in over) { merged[k] = over[k]; } + return merged; + } + // shallow-clone to avoid mutating seed + const clone = {}; + for (var k in e) { clone[k] = e[k]; } + clone.subItems = e.subItems ? e.subItems.map(function (s) { var c = {}; for (var ks in s) c[ks] = s[ks]; return c; }) : []; + clone.history = e.history ? e.history.map(function (h) { var c = {}; for (var kh in h) c[kh] = h[kh]; return c; }) : []; + return clone; + }); + + renderTable(); + renderStats(); + renderNotif(); +} + +function saveEvents() { + var changes = {}; + eventsData.forEach(function (ev) { + var orig = null; + for (var i = 0; i < ALL_EVENTS.length; i++) { + if (ALL_EVENTS[i].id === ev.id) { orig = ALL_EVENTS[i]; break; } + } + if (!orig) return; + var diff = {}; + var fields = ["status", "region", "description", "quantity", "subItems", "history"]; + var hasDiff = false; + for (var fi = 0; fi < fields.length; fi++) { + var f = fields[fi]; + if (JSON.stringify(ev[f]) !== JSON.stringify(orig[f])) { + diff[f] = JSON.parse(JSON.stringify(ev[f])); + hasDiff = true; + } + } + if (hasDiff) { + changes[ev.id] = diff; + } + }); + localStorage.setItem("sh_events", JSON.stringify(changes)); +} + +// ─── Filtering ──────────────────────────────────────────────────────────── +function getFilteredEvents() { + var list = eventsData.slice(); + var q = document.getElementById("filterSearch").value.toLowerCase(); + var st = document.getElementById("filterStatus").value; + var br = document.getElementById("filterBranch").value; + if (q) list = list.filter(function (e) { return e.title.toLowerCase().indexOf(q) !== -1; }); + if (st) list = list.filter(function (e) { return e.status === st; }); + if (br) list = list.filter(function (e) { return e.branch === br; }); + + var sort = document.getElementById("filterSort").value; + list.sort(function (a, b) { + if (sort === "deadline") return (a.deadline || "").localeCompare(b.deadline || ""); + if (sort === "title") return a.title.localeCompare(b.title); + if (sort === "status") return (a.status || "").localeCompare(b.status || ""); + return 0; + }); + return list; +} + +// ─── Render table ───────────────────────────────────────────────────────── +function renderTable() { + var list = getFilteredEvents(); + + // Update branch filter dropdown + var sel = document.getElementById("filterBranch"); + var curVal = sel.value; + var branches = []; + var seen = {}; + eventsData.forEach(function (e) { + if (e.branch && !seen[e.branch]) { seen[e.branch] = true; branches.push(e.branch); } + }); + sel.innerHTML = '' + + branches.map(function (b) { return ''; }).join(""); + sel.value = curVal; + + var tbody = document.getElementById("eventsBody"); + tbody.innerHTML = list.map(function (ev, i) { + var dd = daysUntil(ev.deadline); + var cls = ""; + if (ev.status === "overdue" || (dd <= 0 && ev.status !== "completed")) cls = "overdue"; + else if (dd <= 14 && ev.status !== "completed") cls = "warning"; + else if (ev.status === "completed") cls = "completed"; + var hasSub = ev.subItems && ev.subItems.length > 0; + var statusClass = "status-" + (ev.status || "pending"); + var subRows = ""; + if (hasSub) { + subRows = ev.subItems.map(function (s) { + return '' + + '' + esc(s.id) + '. ' + esc(s.title) + + ' ' + esc(s.status || "pending") + ''; + }).join(""); + } + var ddStr = ""; + if (dd < 999 && dd > 0) { + ddStr = ' (' + dd + ' дн.)'; + } else if (dd <= 0 && ev.status !== "completed") { + ddStr = ' (просрочено)'; + } + return '' + + '' + (hasSub ? '' : "") + (i + 1) + '' + + '' + esc(ev.title) + '' + + '' + esc(ev.responsible || "") + '' + + '' + esc(ev.section || "") + '' + + '' + esc(ev.deadline || "") + ddStr + '' + + '' + esc(ev.status || "pending") + '' + + '' + + '' + subRows; + }).join(""); + if (list.length === 0) { + tbody.innerHTML = 'Нет данных'; + } +} + +function toggleSub(id) { + var rows = document.querySelectorAll('.sub-body[data-parent="' + id + '"]'); + rows.forEach(function (r) { + r.style.display = r.style.display === "none" ? "" : "none"; + }); + // Toggle arrow direction + var toggle = document.querySelector('span.sub-toggle[data-for="' + id + '"]'); +} + +// ─── Stats ──────────────────────────────────────────────────────────────── +function renderStats() { + var total = eventsData.length; + var completed = eventsData.filter(function (e) { return e.status === "completed"; }).length; + var in_progress = eventsData.filter(function (e) { return e.status === "in_progress"; }).length; + var overdue = eventsData.filter(function (e) { + return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); + }).length; + document.getElementById("statsRow").innerHTML = + '
' + total + '
Всего мероприятий
' + + '
' + completed + '
Выполнено
' + + '
' + in_progress + '
В работе
' + + '
' + overdue + '
Просрочено
'; +} + +// ─── Edit modal ─────────────────────────────────────────────────────────── +function openEdit(id) { + editingId = id; + var ev = null; + for (var i = 0; i < eventsData.length; i++) { + if (eventsData[i].id === id) { ev = eventsData[i]; break; } + } + if (!ev) return; + document.getElementById("editStatus").value = ev.status || "pending"; + document.getElementById("editRegion").value = ev.region || ""; + document.getElementById("editDesc").value = ev.description || ""; + document.getElementById("editQty").value = ev.quantity || ""; + document.getElementById("editFile").value = ""; + renderEditFiles(id); + document.getElementById("editModal").classList.add("open"); +} + +function closeModal() { + document.getElementById("editModal").classList.remove("open"); + editingId = null; +} + +function renderEditFiles(eventId) { + var div = document.getElementById("editFileList"); + var allFiles = {}; + try { + allFiles = JSON.parse(localStorage.getItem("sh_files") || "{}"); + } catch (e) { /* ignore */ } + var files = allFiles[eventId] || []; + var html = ""; + if (files.length > 0) { + html += '
Файлы:
'; + files.forEach(function (f, idx) { + html += '
📄 ' + esc(f.name) + + ' (' + Math.round(f.size / 1024) + ' KB)' + + ' Удалить
'; + }); + } + div.innerHTML = html || '
Нет загруженных файлов
'; +} + +function deleteFile(eventId, idx) { + var allFiles = {}; + try { + allFiles = JSON.parse(localStorage.getItem("sh_files") || "{}"); + } catch (e) { /* ignore */ } + var files = allFiles[eventId] || []; + files.splice(idx, 1); + allFiles[eventId] = files; + localStorage.setItem("sh_files", JSON.stringify(allFiles)); + renderEditFiles(eventId); +} + +function saveEdit() { + var ev = null; + for (var i = 0; i < eventsData.length; i++) { + if (eventsData[i].id === editingId) { ev = eventsData[i]; break; } + } + if (!ev) return; + + // Update event fields + ev.status = document.getElementById("editStatus").value; + ev.region = document.getElementById("editRegion").value; + ev.description = document.getElementById("editDesc").value; + ev.quantity = parseInt(document.getElementById("editQty").value) || 0; + + // Add history entry + if (!ev.history) ev.history = []; + ev.history.push({ + timestamp: new Date().toISOString(), + action: "updated", + changes: { status: ev.status, region: ev.region, description: ev.description, quantity: ev.quantity } + }); + + // Handle file upload + var fileInput = document.getElementById("editFile"); + if (fileInput.files.length > 0) { + var file = fileInput.files[0]; + if (file.size > 3 * 1024 * 1024) { + alert("Файл превышает 3 МБ. Выберите файл меньшего размера."); + return; + } + var reader = new FileReader(); + reader.onload = function () { + var allFiles = {}; + try { + allFiles = JSON.parse(localStorage.getItem("sh_files") || "{}"); + } catch (e) { /* ignore */ } + if (!allFiles[editingId]) allFiles[editingId] = []; + allFiles[editingId].push({ + name: file.name, + size: file.size, + type: file.type, + data: reader.result, + uploadedAt: new Date().toISOString() + }); + localStorage.setItem("sh_files", JSON.stringify(allFiles)); + saveEvents(); + renderTable(); + renderStats(); + renderEditFiles(editingId); + document.getElementById("editFile").value = ""; + }; + reader.readAsDataURL(file); + } else { + saveEvents(); + renderTable(); + renderStats(); + closeModal(); + } +} + +// ─── Notifications ──────────────────────────────────────────────────────── +function renderNotif() { + var list = []; + eventsData.forEach(function (ev) { + if (ev.status === "completed") return; + var dd = daysUntil(ev.deadline); + if (dd <= 0) list.push({ text: "Просрочено: " + ev.title, date: ev.deadline, type: "danger" }); + else if (dd <= 1) list.push({ text: "Остался 1 день: " + ev.title, date: ev.deadline, type: "danger" }); + else if (dd <= 7) list.push({ text: "Осталось " + dd + " дн.: " + ev.title, date: ev.deadline, type: "warning" }); + else if (dd <= 14) list.push({ text: "Осталось " + dd + " дн.: " + ev.title, date: ev.deadline, type: "warning" }); + else if (dd <= 30) list.push({ text: "Осталось " + dd + " дн.: " + ev.title, date: ev.deadline, type: "info" }); + }); + list.sort(function (a, b) { return a.date.localeCompare(b.date); }); + var count = list.length; + document.getElementById("notifCount").textContent = count > 99 ? "99+" : count; + document.getElementById("notifList").innerHTML = list.slice(0, 50).map(function (n) { + var color = n.type === "danger" ? "var(--danger)" : n.type === "warning" ? "var(--warning)" : "var(--accent)"; + return '
' + esc(n.text) + '
' + esc(n.date) + '
'; + }).join("") || '
Нет уведомлений
'; +} + +function toggleNotif() { + document.getElementById("notifPanel").classList.toggle("open"); +} + +// ─── Analytics ──────────────────────────────────────────────────────────── +function renderAnalytics() { + var total = eventsData.length; + var completed = eventsData.filter(function (e) { return e.status === "completed"; }).length; + var in_progress = eventsData.filter(function (e) { return e.status === "in_progress"; }).length; + var overdue = eventsData.filter(function (e) { + return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); + }).length; + var pct = total ? Math.round(completed / total * 100) : 0; + + document.getElementById("analyticsStats").innerHTML = + '
' + pct + '%
Выполнение
' + + '
' + completed + '
Выполнено
' + + '
' + in_progress + '
В работе
' + + '
' + overdue + '
Просрочено
'; + + // Branches chart + var branches = {}; + eventsData.forEach(function (e) { + var b = e.branch || "Неизвестно"; + branches[b] = (branches[b] || 0) + 1; + }); + var maxB = Math.max.apply(null, Object.values(branches).concat([1])); + var branchKeys = Object.keys(branches).sort(function (a, b) { return branches[b] - branches[a]; }); + document.getElementById("branchChart").innerHTML = branchKeys.map(function (k) { + return '
' + esc(k) + '
' + branches[k] + '
'; + }).join(""); + + // Status chart + var statuses = { completed: 0, in_progress: 0, pending: 0, overdue: 0 }; + eventsData.forEach(function (e) { + var s = e.status || "pending"; + statuses[s] = (statuses[s] || 0) + 1; + }); + // Include overdue in the overdue count from the computed value + var od = eventsData.filter(function (e) { + return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); + }).length; + statuses.overdue = od; + + var maxS = Math.max.apply(null, Object.values(statuses).concat([1])); + var labels = { completed: "Выполнено", in_progress: "В работе", pending: "Ожидает", overdue: "Просрочено" }; + var colors = { completed: "var(--success)", in_progress: "var(--accent-bright)", pending: "var(--warning)", overdue: "var(--danger)" }; + document.getElementById("statusChart").innerHTML = Object.keys(statuses).map(function (k) { + return '
' + labels[k] + '
' + statuses[k] + '
'; + }).join(""); +} + +// ─── Reports ────────────────────────────────────────────────────────────── +function generateReportHTML() { + var now = new Date().toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" }); + var total = eventsData.length; + var completed = eventsData.filter(function (e) { return e.status === "completed"; }).length; + var in_progress = eventsData.filter(function (e) { return e.status === "in_progress"; }).length; + var overdue = eventsData.filter(function (e) { + return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); + }).length; + var pending = eventsData.filter(function (e) { return e.status === "pending"; }).length; + var pct = total ? Math.round(completed / total * 100) : 0; + + var rows = eventsData.map(function (e, i) { + var stLabel = { completed: "Выполнено", in_progress: "В работе", pending: "Ожидает", overdue: "Просрочено" }; + return '' + (i + 1) + '' + esc(e.title) + '' + esc(e.responsible || "") + '' + esc(e.section || "") + '' + esc(e.branch || "") + '' + esc(e.deadline || "") + '' + (stLabel[e.status] || e.status) + ''; + }).join(""); + + return 'Отчёт по ПБ — АО «Самрук-Казына»' + + '

Сводный отчёт по производственной безопасности

' + + '

АО «Самрук-Казына»

' + + '
Сформирован: ' + now + '
' + + '
' + + '
' + total + '
Всего
' + + '
' + completed + '
Выполнено
' + + '
' + in_progress + '
В работе
' + + '
' + pending + '
Ожидает
' + + '
' + overdue + '
Просрочено
' + + '
' + pct + '%
Выполнение
' + + '
' + + '' + + rows + '
МероприятиеОтветственныйРазделФилиалСрокСтатус
' + + ''; +} + +function downloadWord() { + var html = generateReportHTML(); + var blob = new Blob(["\ufeff" + html], { type: "application/msword" }); + var url = URL.createObjectURL(blob); + var a = document.createElement("a"); + a.href = url; + a.download = "Otchyot_PB.doc"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function downloadPdf() { + var html = generateReportHTML(); + var w = window.open("", "_blank"); + w.document.write(html); + w.document.close(); + w.focus(); + w.print(); +} + +// ─── HSE Integration ────────────────────────────────────────────────────── +function sendHseReport() { + var result = document.getElementById("hseResult"); + result.innerHTML = 'ⓘ Это статическая демо-версия. Интеграция с HSE.sk.kz недоступна в офлайн-режиме. Отчёт за месяц можно скачать на вкладке «Отчёты».'; +} + +// ─── AI Chat ────────────────────────────────────────────────────────────── +function sendChat() { + var input = document.getElementById("chatInput"); + var q = input.value.trim(); + if (!q) return; + input.value = ""; + var msgs = document.getElementById("chatMsgs"); + msgs.innerHTML += '
' + esc(q) + '
'; + var answer = getAIAnswer(q); + msgs.innerHTML += '
🤖 ИИ-агент
' + answer + '
'; + msgs.scrollTop = msgs.scrollHeight; +} + +function getAIAnswer(q) { + var ev = eventsData; + var total = ev.length; + var completed = ev.filter(function (e) { return e.status === "completed"; }).length; + var in_progress = ev.filter(function (e) { return e.status === "in_progress"; }).length; + var pending = ev.filter(function (e) { return e.status === "pending"; }).length; + var overdue = ev.filter(function (e) { + return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); + }).length; + var ql = q.toLowerCase(); + + if (ql.indexOf("просрочен") !== -1 || ql.indexOf("overdue") !== -1) { + var overdueList = ev.filter(function (e) { + return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); + }); + if (overdueList.length === 0) return "Просроченных мероприятий нет."; + return "Просроченные мероприятия (" + overdueList.length + "):
" + + overdueList.map(function (e) { + return "- " + esc(e.title) + " (срок: " + esc(e.deadline) + ", филиал: " + esc(e.branch || "—") + ")"; + }).join("
"); + } + + if (ql.indexOf("риск") !== -1) { + var risky = ev.filter(function (e) { + return e.status !== "completed" && daysUntil(e.deadline) <= 14 && daysUntil(e.deadline) > 0; + }); + if (risky.length === 0) return "Мероприятий с высоким риском срыва нет."; + return "Высокий риск срыва у " + risky.length + " мероприятий:
" + + risky.map(function (e) { + return "- " + esc(e.title) + " (осталось " + daysUntil(e.deadline) + " дн., ответственный: " + esc(e.responsible || "—") + ")"; + }).join("
"); + } + + if (ql.indexOf("сводк") !== -1 || ql.indexOf("итог") !== -1) { + return "Сводка по ПБ: всего " + total + " мероприятий. Выполнено: " + completed + + ", в работе: " + in_progress + ", ожидает: " + pending + ", просрочено: " + overdue + + ". Процент выполнения: " + (total ? Math.round(completed / total * 100) : 0) + "%."; + } + + if (ql.indexOf("филиал") !== -1 || ql.indexOf("рейтинг") !== -1 || ql.indexOf("branch") !== -1) { + var br = {}; + ev.forEach(function (e) { + var b = e.branch || "Неизвестно"; + if (!br[b]) br[b] = { total: 0, done: 0 }; + br[b].total++; + if (e.status === "completed") br[b].done++; + }); + var sorted = Object.keys(br).sort(function (a, b) { + return (br[b].done / br[b].total) - (br[a].done / br[a].total); + }); + return "Рейтинг филиалов по выполнению:
" + + sorted.map(function (k, i) { + return (i + 1) + ". " + esc(k) + ": " + br[k].done + "/" + br[k].total + + " (" + Math.round(br[k].done / br[k].total * 100) + "%)"; + }).join("
"); + } + + if ((ql.indexOf("статус") !== -1 || ql.indexOf("номер") !== -1) && ql.match(/\d+/)) { + var num = parseInt(ql.match(/\d+/)[0], 10); + var found = ev[num - 1]; + if (found) { + return "№" + num + ": " + esc(found.title) + "
" + + "Статус: " + esc(found.status) + " | Срок: " + esc(found.deadline) + + " | Ответственный: " + esc(found.responsible) + + " | Филиал: " + esc(found.branch) + + " | Описание: " + esc(found.description || "—"); + } + return "Мероприятие №" + num + " не найдено."; + } + + if (ql.indexOf("советник") !== -1 || ql.indexOf("рекомендац") !== -1) { + var rlist = ev.filter(function (e) { + return e.status !== "completed" && daysUntil(e.deadline) <= 14 && daysUntil(e.deadline) > 0; + }); + var olist = ev.filter(function (e) { return e.status === "overdue"; }); + var adv = "Рекомендации:
"; + if (olist.length) adv += "- Срочно принять меры по " + olist.length + " просроченным мероприятиям.
"; + if (rlist.length) adv += "- Усилить контроль за " + rlist.length + " мероприятиями с приближающимся сроком.
"; + if (pending > 5) adv += "- " + pending + " мероприятий ещё не начаты — требуется активизация.
"; + if (adv === "Рекомендации:
") adv += "- Всё в порядке, продолжайте в том же духе."; + return adv; + } + + if (ql.indexOf("аудит") !== -1 || ql.indexOf("360") !== -1) { + return "Аудит 360°:
- Всего: " + total + + "
- Выполнено: " + completed + + "
- В работе: " + in_progress + + "
- Просрочено: " + overdue + + "
- Ожидает: " + pending + + "
- Выполнение: " + (total ? Math.round(completed / total * 100) : 0) + "%"; + } + + if (ql.indexOf("прогноз") !== -1) { + var willComplete = ev.filter(function (e) { + return e.status === "in_progress" && daysUntil(e.deadline) <= 30 && daysUntil(e.deadline) > 0; + }).length; + var atRisk = ev.filter(function (e) { + return (e.status === "pending" && daysUntil(e.deadline) <= 30) || + (e.status !== "completed" && daysUntil(e.deadline) <= 7 && daysUntil(e.deadline) >= 0); + }).length; + return "Прогноз на 30 дней:
" + + "- Ожидается завершение ~" + willComplete + " мероприятий в работе.
" + + "- " + atRisk + " мероприятий под угрозой срыва.
" + + "- Текущий процент выполнения: " + (total ? Math.round(completed / total * 100) : 0) + "%.
" + + "- При сохранении темпа к концу месяца выполнение достигнет ~" + Math.min(100, Math.round((completed + willComplete) / total * 100)) + "%."; + } + + return "Я анализирую данные по мероприятиям ПБ. Можете спросить:
" + + "- «просроченные» — список просрочек
" + + "- «риски» — мероприятия с высоким риском
" + + "- «сводка» — общая статистика
" + + "- «рейтинг филиалов» — кто в лидерах
" + + "- «статус N» — информация по конкретному мероприятию
" + + "- «советник» — рекомендации
" + + "- «аудит 360» — полный срез
" + + "- «прогноз» — что будет через месяц"; +} + +// ─── Init ───────────────────────────────────────────────────────────────── +function init() { + loadEvents(); + + // Sidebar navigation + document.querySelectorAll(".sidebar a[data-page]").forEach(function (a) { + a.addEventListener("click", function (e) { + e.preventDefault(); + document.querySelectorAll(".sidebar a").forEach(function (x) { x.classList.remove("active"); }); + this.classList.add("active"); + document.querySelectorAll(".page").forEach(function (p) { p.classList.remove("active"); }); + document.getElementById("page-" + this.dataset.page).classList.add("active"); + if (this.dataset.page === "analytics") renderAnalytics(); + }); + }); + + // Event listeners + document.getElementById("btnRefresh").addEventListener("click", loadEvents); + document.getElementById("filterSearch").addEventListener("input", renderTable); + document.getElementById("filterStatus").addEventListener("change", renderTable); + document.getElementById("filterBranch").addEventListener("change", renderTable); + document.getElementById("filterSort").addEventListener("change", renderTable); + document.getElementById("dlWord").addEventListener("click", downloadWord); + document.getElementById("dlPdf").addEventListener("click", downloadPdf); + document.getElementById("hseSendBtn").addEventListener("click", sendHseReport); + document.getElementById("chatSend").addEventListener("click", sendChat); + document.getElementById("chatInput").addEventListener("keydown", function (e) { if (e.key === "Enter") sendChat(); }); + document.getElementById("modalCancel").addEventListener("click", closeModal); + document.getElementById("modalSave").addEventListener("click", saveEdit); + document.getElementById("logoutBtn").addEventListener("click", logout); + document.getElementById("notifBell").addEventListener("click", toggleNotif); + document.getElementById("notifClose").addEventListener("click", function () { + document.getElementById("notifPanel").classList.remove("open"); + }); + document.getElementById("editModal").addEventListener("click", function (e) { + if (e.target === this) closeModal(); + }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") closeModal(); + }); + + // Set current month for HSE + document.getElementById("hseMonth").value = new Date().toISOString().slice(0, 7); +} + +// ─── Auto-login check ───────────────────────────────────────────────────── +(function () { + var isLoggedIn = localStorage.getItem("sh_token"); + var savedUser = {}; + try { savedUser = JSON.parse(localStorage.getItem("sh_user") || "{}"); } catch (e) { /* ignore */ } + + // Login button always needs listener + var loginBtn = document.getElementById("loginBtn"); + if (loginBtn) { + loginBtn.addEventListener("click", login); + } + var loginPass = document.getElementById("loginPass"); + if (loginPass) { + loginPass.addEventListener("keydown", function (e) { if (e.key === "Enter") login(); }); + } + + if (isLoggedIn && savedUser.email) { + user = savedUser; + document.getElementById("loginScreen").style.display = "none"; + document.getElementById("app").style.display = "block"; + document.getElementById("userName").textContent = user.name || ""; + document.getElementById("userEmail").textContent = user.email || ""; + init(); + } +})(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..210a23b --- /dev/null +++ b/style.css @@ -0,0 +1,2147 @@ +:root { + --ink: #0F1218; + --cyan: #00E5FF; + --cyan-dark: #00B8D4; + --white: #FFFFFF; + --gray-100: #F2F4F7; + --gray-500: #5B6573; + --card-bg: #162240; + --border-color: rgba(255, 255, 255, 0.1); + --danger: #EF4444; + --warning: #F59E0B; + --success: #10B981; + --sidebar-width: 260px; + --header-height: 64px; + --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; + --radius: 12px; + --radius-sm: 8px; + --radius-xs: 6px; + --transition: 0.2s ease; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-stack); + background: var(--ink); + color: var(--white); + line-height: 1.6; + min-height: 100vh; +} + +a { + color: var(--cyan); + text-decoration: none; + transition: color var(--transition); +} + +a:hover { + color: var(--cyan-dark); +} + +ul, ol { + list-style: none; +} + +img { + max-width: 100%; + display: block; +} + +button { + font-family: var(--font-stack); + cursor: pointer; + border: none; + outline: none; +} + +input, textarea, select { + font-family: var(--font-stack); + outline: none; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.3; +} + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); +} + +/* ===== LOGIN SCREEN ===== */ + +.login-screen { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--ink); + padding: 20px; +} + +.login-container { + width: 100%; + max-width: 420px; +} + +.login-logo { + text-align: center; + margin-bottom: 32px; +} + +.login-logo-icon { + width: 56px; + height: 56px; + background: var(--cyan); + border-radius: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 28px; + color: var(--ink); + margin-bottom: 16px; +} + +.login-logo h1 { + font-size: 22px; + font-weight: 700; + color: var(--white); +} + +.login-logo p { + font-size: 14px; + color: var(--gray-500); + margin-top: 4px; +} + +.login-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 36px 32px; +} + +.login-card h2 { + font-size: 20px; + margin-bottom: 8px; +} + +.login-card .login-subtitle { + font-size: 14px; + color: var(--gray-500); + margin-bottom: 24px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--gray-100); + margin-bottom: 6px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px 14px; + background: var(--ink); + border: 1px solid var(--border-color); + border-radius: var(--radius-xs); + color: var(--white); + font-size: 14px; + transition: border-color var(--transition); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + border-color: var(--cyan); +} + +.form-group input::placeholder { + color: var(--gray-500); +} + +.form-hint { + font-size: 12px; + color: var(--gray-500); + margin-top: 4px; +} + +.form-row { + display: flex; + gap: 12px; +} + +.form-row .form-group { + flex: 1; +} + +.login-footer { + text-align: center; + margin-top: 24px; + font-size: 13px; + color: var(--gray-500); +} + +.login-footer a { + font-weight: 500; +} + +/* ===== APP LAYOUT ===== */ + +.app-layout { + display: flex; + min-height: 100vh; +} + +/* ===== SIDEBAR ===== */ + +.sidebar { + width: var(--sidebar-width); + min-width: var(--sidebar-width); + background: var(--ink); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 100; + overflow-y: auto; +} + +.sidebar-header { + padding: 20px 20px 16px; + border-bottom: 1px solid var(--border-color); +} + +.sidebar-logo { + display: flex; + align-items: center; + gap: 10px; +} + +.sidebar-logo-icon { + width: 36px; + height: 36px; + background: var(--cyan); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: var(--ink); + flex-shrink: 0; +} + +.sidebar-logo-text { + font-size: 16px; + font-weight: 700; + color: var(--white); + white-space: nowrap; +} + +.sidebar-nav { + padding: 12px 12px 0; + flex: 1; +} + +.sidebar-section-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--gray-500); + padding: 8px 12px 6px; +} + +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: var(--radius-sm); + font-size: 14px; + color: var(--gray-500); + transition: all var(--transition); + cursor: pointer; +} + +.nav-item:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--white); +} + +.nav-item.active { + background: rgba(0, 229, 255, 0.1); + color: var(--cyan); +} + +.nav-item .nav-icon { + width: 20px; + text-align: center; + font-size: 16px; + flex-shrink: 0; +} + +.nav-badge { + margin-left: auto; + background: var(--danger); + color: var(--white); + font-size: 11px; + font-weight: 600; + padding: 2px 7px; + border-radius: 10px; + min-width: 20px; + text-align: center; +} + +.sidebar-footer { + padding: 12px; + border-top: 1px solid var(--border-color); +} + +.sidebar-user { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-radius: var(--radius-sm); + transition: background var(--transition); + cursor: pointer; +} + +.sidebar-user:hover { + background: rgba(255, 255, 255, 0.05); +} + +.sidebar-user-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + background: var(--cyan); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 14px; + color: var(--ink); + flex-shrink: 0; +} + +.sidebar-user-info { + overflow: hidden; +} + +.sidebar-user-name { + font-size: 13px; + font-weight: 600; + color: var(--white); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-user-role { + font-size: 11px; + color: var(--gray-500); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== MAIN CONTENT AREA ===== */ + +.main-content { + flex: 1; + margin-left: var(--sidebar-width); + min-width: 0; +} + +/* ===== TOP HEADER BAR ===== */ + +.top-header { + height: var(--header-height); + background: var(--ink); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + position: sticky; + top: 0; + z-index: 90; +} + +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.page-title { + font-size: 18px; + font-weight: 700; +} + +.breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--gray-500); +} + +.breadcrumb a { + color: var(--gray-500); +} + +.breadcrumb a:hover { + color: var(--cyan); +} + +.breadcrumb .separator { + color: var(--gray-500); +} + +.header-right { + display: flex; + align-items: center; + gap: 12px; +} + +.header-search { + position: relative; +} + +.header-search input { + padding: 8px 14px 8px 36px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--white); + font-size: 13px; + width: 220px; + transition: border-color var(--transition), width 0.3s ease; +} + +.header-search input:focus { + border-color: var(--cyan); + width: 280px; +} + +.header-search .search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + color: var(--gray-500); + pointer-events: none; +} + +.header-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-sm); + background: var(--card-bg); + border: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: var(--gray-500); + transition: all var(--transition); + position: relative; +} + +.header-btn:hover { + color: var(--white); + border-color: var(--cyan); +} + +.header-btn.has-notification::after { + content: ''; + position: absolute; + top: 6px; + right: 6px; + width: 8px; + height: 8px; + background: var(--danger); + border-radius: 50%; + border: 2px solid var(--card-bg); +} + +/* ===== PAGE CONTENT ===== */ + +.page-content { + padding: 24px; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + flex-wrap: wrap; + gap: 12px; +} + +.section-header h2 { + font-size: 20px; +} + +.section-header-actions { + display: flex; + gap: 8px; + align-items: center; +} + +/* ===== BUTTONS ===== */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 9px 18px; + font-size: 14px; + font-weight: 500; + border-radius: var(--radius-sm); + transition: all var(--transition); + white-space: nowrap; + line-height: 1.4; +} + +.btn-primary { + background: var(--cyan); + color: var(--ink); +} + +.btn-primary:hover { + background: var(--cyan-dark); +} + +.btn-outline { + background: transparent; + color: var(--cyan); + border: 1px solid var(--cyan); +} + +.btn-outline:hover { + background: rgba(0, 229, 255, 0.1); +} + +.btn-success { + background: var(--success); + color: var(--white); +} + +.btn-success:hover { + opacity: 0.85; +} + +.btn-danger { + background: var(--danger); + color: var(--white); +} + +.btn-danger:hover { + opacity: 0.85; +} + +.btn-sm { + padding: 5px 12px; + font-size: 12px; + border-radius: var(--radius-xs); +} + +.btn-icon { + width: 34px; + height: 34px; + padding: 0; + border-radius: var(--radius-xs); +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +/* ===== FILTERS BAR ===== */ + +.filters-bar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.filter-group { + display: flex; + align-items: center; + gap: 8px; +} + +.filter-group label { + font-size: 13px; + color: var(--gray-500); + white-space: nowrap; +} + +.filter-group select, +.filter-group input { + padding: 8px 12px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-xs); + color: var(--white); + font-size: 13px; + min-width: 140px; +} + +.filter-group select:focus, +.filter-group input:focus { + border-color: var(--cyan); +} + +.filter-active-indicator { + display: inline-block; + width: 8px; + height: 8px; + background: var(--cyan); + border-radius: 50%; +} + +/* ===== STATS GRID ===== */ + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 20px; + transition: border-color var(--transition), transform var(--transition); +} + +.stat-card:hover { + border-color: var(--cyan); + transform: translateY(-2px); +} + +.stat-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.stat-card-icon { + width: 42px; + height: 42px; + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + +.stat-card-icon.cyan { + background: rgba(0, 229, 255, 0.15); + color: var(--cyan); +} + +.stat-card-icon.success { + background: rgba(16, 185, 129, 0.15); + color: var(--success); +} + +.stat-card-icon.warning { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.stat-card-icon.danger { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.stat-card-change { + font-size: 12px; + font-weight: 600; + padding: 2px 8px; + border-radius: 20px; +} + +.stat-card-change.up { + color: var(--success); + background: rgba(16, 185, 129, 0.1); +} + +.stat-card-change.down { + color: var(--danger); + background: rgba(239, 68, 68, 0.1); +} + +.stat-card-value { + font-size: 28px; + font-weight: 700; + line-height: 1.2; +} + +.stat-card-label { + font-size: 13px; + color: var(--gray-500); + margin-top: 4px; +} + +/* ===== TABLES ===== */ + +.table-container { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + overflow: hidden; +} + +.table-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); +} + +.table-header h3 { + font-size: 16px; +} + +.table-header-actions { + display: flex; + gap: 8px; +} + +.table-wrapper { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +table thead { + background: rgba(255, 255, 255, 0.03); +} + +table th { + padding: 12px 16px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--gray-500); + text-align: left; + white-space: nowrap; +} + +table th.sortable { + cursor: pointer; + user-select: none; +} + +table th.sortable:hover { + color: var(--white); +} + +table th.sortable .sort-arrow { + font-size: 10px; + margin-left: 4px; +} + +table td { + padding: 12px 16px; + font-size: 14px; + border-bottom: 1px solid var(--border-color); + vertical-align: middle; +} + +table tbody tr { + transition: background var(--transition); +} + +table tbody tr:hover { + background: rgba(255, 255, 255, 0.03); +} + +table tbody tr.status-completed { + border-left: 3px solid var(--success); +} + +table tbody tr.status-in_progress { + border-left: 3px solid var(--cyan); +} + +table tbody tr.status-pending { + border-left: 3px solid var(--warning); +} + +table tbody tr.status-overdue { + border-left: 3px solid var(--danger); +} + +.table-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-top: 1px solid var(--border-color); + font-size: 13px; + color: var(--gray-500); +} + +.pagination { + display: flex; + align-items: center; + gap: 4px; +} + +.pagination button { + width: 32px; + height: 32px; + border-radius: var(--radius-xs); + background: transparent; + color: var(--gray-500); + font-size: 13px; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition); +} + +.pagination button:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--white); +} + +.pagination button.active { + background: var(--cyan); + color: var(--ink); + font-weight: 600; +} + +.pagination button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* ===== STATUS BADGES ===== */ + +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + white-space: nowrap; + line-height: 1.5; +} + +.badge-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.badge-completed { + background: rgba(16, 185, 129, 0.15); + color: var(--success); +} + +.badge-completed .badge-dot { + background: var(--success); +} + +.badge-in_progress { + background: rgba(0, 229, 255, 0.15); + color: var(--cyan); +} + +.badge-in_progress .badge-dot { + background: var(--cyan); +} + +.badge-pending { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.badge-pending .badge-dot { + background: var(--warning); +} + +.badge-overdue { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-overdue .badge-dot { + background: var(--danger); +} + +/* ===== ANALYTICS CHART BARS ===== */ + +.chart-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 24px; +} + +.chart-card h3 { + font-size: 16px; + margin-bottom: 20px; +} + +.chart-bars { + display: flex; + align-items: flex-end; + gap: 12px; + height: 200px; + padding-top: 20px; +} + +.chart-bar-group { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + justify-content: flex-end; +} + +.chart-bar { + width: 100%; + max-width: 48px; + border-radius: 6px 6px 0 0; + background: var(--cyan); + transition: height 0.4s ease, background var(--transition); + position: relative; + min-height: 4px; +} + +.chart-bar:hover { + background: var(--cyan-dark); +} + +.chart-bar-value { + font-size: 11px; + font-weight: 600; + margin-bottom: 4px; + color: var(--gray-100); +} + +.chart-bar-label { + font-size: 11px; + color: var(--gray-500); + margin-top: 8px; + text-align: center; +} + +.chart-bar.warning-bar { + background: var(--warning); +} + +.chart-bar.danger-bar { + background: var(--danger); +} + +.chart-bar.success-bar { + background: var(--success); +} + +/* ===== MODAL ===== */ + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal-box { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + width: 100%; + max-width: 560px; + max-height: 90vh; + display: flex; + flex-direction: column; + animation: modalIn 0.2s ease; +} + +@keyframes modalIn { + from { + opacity: 0; + transform: translateY(-16px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + font-size: 18px; +} + +.modal-close { + width: 32px; + height: 32px; + border-radius: var(--radius-xs); + background: transparent; + color: var(--gray-500); + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition); +} + +.modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--white); +} + +.modal-body { + padding: 24px; + overflow-y: auto; + flex: 1; +} + +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 16px 24px; + border-top: 1px solid var(--border-color); +} + +.modal-lg { + max-width: 720px; +} + +.modal-sm { + max-width: 400px; +} + +/* ===== NOTIFICATIONS PANEL ===== */ + +.notifications-toggle { + position: relative; +} + +.notifications-panel { + position: absolute; + top: calc(100% + 12px); + right: 0; + width: 360px; + max-height: 480px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + overflow: hidden; + z-index: 200; + animation: modalIn 0.15s ease; +} + +.notifications-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--border-color); +} + +.notifications-header h4 { + font-size: 15px; +} + +.notifications-mark-read { + font-size: 12px; + color: var(--cyan); + cursor: pointer; + background: none; +} + +.notifications-mark-read:hover { + color: var(--cyan-dark); +} + +.notifications-list { + max-height: 380px; + overflow-y: auto; +} + +.notification-item { + display: flex; + gap: 12px; + padding: 14px 16px; + border-bottom: 1px solid var(--border-color); + transition: background var(--transition); + cursor: pointer; +} + +.notification-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.notification-item.unread { + background: rgba(0, 229, 255, 0.05); +} + +.notification-item.unread::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 4px; + background: var(--cyan); + border-radius: 50%; +} + +.notification-icon { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + flex-shrink: 0; +} + +.notification-icon.info { + background: rgba(0, 229, 255, 0.15); + color: var(--cyan); +} + +.notification-icon.warning { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.notification-icon.danger { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.notification-icon.success { + background: rgba(16, 185, 129, 0.15); + color: var(--success); +} + +.notification-content { + flex: 1; + min-width: 0; +} + +.notification-text { + font-size: 13px; + line-height: 1.4; +} + +.notification-time { + font-size: 11px; + color: var(--gray-500); + margin-top: 2px; +} + +.notifications-footer { + padding: 12px 16px; + border-top: 1px solid var(--border-color); + text-align: center; +} + +.notifications-footer a { + font-size: 13px; + font-weight: 500; +} + +/* ===== AI CHAT ===== */ + +.chat-container { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + display: flex; + flex-direction: column; + height: 500px; + overflow: hidden; +} + +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-bottom: 1px solid var(--border-color); +} + +.chat-header-info { + display: flex; + align-items: center; + gap: 10px; +} + +.chat-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--cyan); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: var(--ink); +} + +.chat-header-text h4 { + font-size: 14px; +} + +.chat-header-text span { + font-size: 11px; + color: var(--success); + display: flex; + align-items: center; + gap: 4px; +} + +.chat-header-text span::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--success); +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.chat-message { + display: flex; + gap: 10px; + max-width: 85%; +} + +.chat-message.user { + align-self: flex-end; + flex-direction: row-reverse; +} + +.chat-message-avatar { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + flex-shrink: 0; +} + +.chat-message.bot .chat-message-avatar { + background: var(--cyan); + color: var(--ink); +} + +.chat-message.user .chat-message-avatar { + background: var(--gray-500); + color: var(--white); +} + +.chat-bubble { + padding: 10px 14px; + border-radius: var(--radius-sm); + font-size: 14px; + line-height: 1.5; +} + +.chat-message.bot .chat-bubble { + background: rgba(255, 255, 255, 0.05); + color: var(--white); + border-top-left-radius: 2px; +} + +.chat-message.user .chat-bubble { + background: rgba(0, 229, 255, 0.15); + color: var(--white); + border-top-right-radius: 2px; +} + +.chat-bubble-time { + font-size: 10px; + color: var(--gray-500); + margin-top: 4px; +} + +.chat-input-area { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + border-top: 1px solid var(--border-color); +} + +.chat-input-area input { + flex: 1; + padding: 10px 14px; + background: var(--ink); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--white); + font-size: 14px; +} + +.chat-input-area input:focus { + border-color: var(--cyan); +} + +.chat-input-area input::placeholder { + color: var(--gray-500); +} + +.chat-send-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-sm); + background: var(--cyan); + color: var(--ink); + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + transition: background var(--transition); +} + +.chat-send-btn:hover { + background: var(--cyan-dark); +} + +.chat-typing-indicator { + display: flex; + align-items: center; + gap: 4px; + padding: 10px 14px; +} + +.chat-typing-indicator span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--gray-500); + animation: typingBounce 1.4s infinite ease-in-out both; +} + +.chat-typing-indicator span:nth-child(1) { animation-delay: -0.32s; } +.chat-typing-indicator span:nth-child(2) { animation-delay: -0.16s; } + +@keyframes typingBounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.4; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +/* ===== HSE CONFIG SECTION ===== */ + +.config-section { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + margin-bottom: 24px; + overflow: hidden; +} + +.config-section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); + cursor: pointer; + user-select: none; +} + +.config-section-header:hover { + background: rgba(255, 255, 255, 0.02); +} + +.config-section-header h3 { + font-size: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.config-section-arrow { + font-size: 14px; + transition: transform var(--transition); + color: var(--gray-500); +} + +.config-section.open .config-section-arrow { + transform: rotate(90deg); +} + +.config-section-body { + padding: 20px; +} + +.config-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +.config-field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.config-field label { + font-size: 13px; + font-weight: 500; + color: var(--gray-100); +} + +.config-field input, +.config-field select, +.config-field textarea { + padding: 9px 12px; + background: var(--ink); + border: 1px solid var(--border-color); + border-radius: var(--radius-xs); + color: var(--white); + font-size: 14px; +} + +.config-field input:focus, +.config-field select:focus, +.config-field textarea:focus { + border-color: var(--cyan); +} + +.config-field textarea { + resize: vertical; + min-height: 80px; +} + +.config-field .hint { + font-size: 12px; + color: var(--gray-500); +} + +.config-actions { + display: flex; + gap: 8px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--border-color); +} + +.config-toggle { + position: relative; + display: inline-flex; + align-items: center; + gap: 10px; + cursor: pointer; +} + +.config-toggle input { + display: none; +} + +.config-toggle-slider { + width: 44px; + height: 24px; + background: var(--ink); + border: 1px solid var(--border-color); + border-radius: 12px; + position: relative; + transition: background var(--transition), border-color var(--transition); +} + +.config-toggle-slider::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--gray-500); + transition: transform var(--transition), background var(--transition); +} + +.config-toggle input:checked + .config-toggle-slider { + background: rgba(0, 229, 255, 0.2); + border-color: var(--cyan); +} + +.config-toggle input:checked + .config-toggle-slider::after { + transform: translateX(20px); + background: var(--cyan); +} + +.config-toggle-label { + font-size: 13px; + color: var(--white); +} + +/* ===== FILE LIST ===== */ + +.file-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.file-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border-radius: var(--radius-xs); + transition: background var(--transition); + cursor: pointer; +} + +.file-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.file-icon { + width: 38px; + height: 38px; + border-radius: var(--radius-xs); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; +} + +.file-icon.pdf { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.file-icon.image { + background: rgba(0, 229, 255, 0.15); + color: var(--cyan); +} + +.file-icon.doc { + background: rgba(16, 185, 129, 0.15); + color: var(--success); +} + +.file-icon.sheet { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.file-info { + flex: 1; + min-width: 0; +} + +.file-name { + font-size: 14px; + font-weight: 500; +} + +.file-meta { + font-size: 12px; + color: var(--gray-500); + display: flex; + gap: 8px; +} + +.file-actions { + display: flex; + gap: 4px; +} + +.file-actions button { + width: 30px; + height: 30px; + border-radius: var(--radius-xs); + background: transparent; + color: var(--gray-500); + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition); +} + +.file-actions button:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--white); +} + +.file-upload-zone { + border: 2px dashed var(--border-color); + border-radius: var(--radius); + padding: 40px 20px; + text-align: center; + transition: border-color var(--transition); + cursor: pointer; +} + +.file-upload-zone:hover { + border-color: var(--cyan); +} + +.file-upload-zone .upload-icon { + font-size: 40px; + margin-bottom: 12px; + color: var(--gray-500); +} + +.file-upload-zone p { + font-size: 14px; + color: var(--gray-500); +} + +.file-upload-zone .upload-link { + color: var(--cyan); + font-weight: 500; +} + +/* ===== EMPTY STATE ===== */ + +.empty-state { + text-align: center; + padding: 48px 20px; + color: var(--gray-500); +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.4; +} + +.empty-state h4 { + font-size: 16px; + color: var(--white); + margin-bottom: 4px; +} + +.empty-state p { + font-size: 14px; + max-width: 320px; + margin: 0 auto 16px; +} + +/* ===== LOADING SPINNER ===== */ + +.spinner { + width: 20px; + height: 20px; + border: 2px solid rgba(255, 255, 255, 0.2); + border-top-color: var(--cyan); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +.spinner-lg { + width: 36px; + height: 36px; + border-width: 3px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-screen { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; +} + +/* ===== TABS ===== */ + +.tabs { + display: flex; + gap: 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; +} + +.tab { + padding: 10px 20px; + font-size: 14px; + font-weight: 500; + color: var(--gray-500); + border-bottom: 2px solid transparent; + transition: all var(--transition); + cursor: pointer; + background: none; +} + +.tab:hover { + color: var(--white); +} + +.tab.active { + color: var(--cyan); + border-bottom-color: var(--cyan); +} + +/* ===== TOOLTIP ===== */ + +.tooltip { + position: relative; +} + +.tooltip::after { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + padding: 6px 10px; + background: var(--ink); + border: 1px solid var(--border-color); + border-radius: var(--radius-xs); + font-size: 12px; + color: var(--white); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition); +} + +.tooltip:hover::after { + opacity: 1; +} + +/* ===== PROGRESS BAR ===== */ + +.progress-bar { + height: 8px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.4s ease; +} + +.progress-bar-fill.cyan { + background: var(--cyan); +} + +.progress-bar-fill.success { + background: var(--success); +} + +.progress-bar-fill.warning { + background: var(--warning); +} + +.progress-bar-fill.danger { + background: var(--danger); +} + +/* ===== ALERT BANNERS ===== */ + +.alert { + padding: 12px 16px; + border-radius: var(--radius-xs); + font-size: 14px; + display: flex; + align-items: flex-start; + gap: 10px; + margin-bottom: 16px; +} + +.alert-icon { + font-size: 16px; + flex-shrink: 0; + margin-top: 1px; +} + +.alert-info { + background: rgba(0, 229, 255, 0.1); + border: 1px solid rgba(0, 229, 255, 0.2); + color: var(--cyan); +} + +.alert-success { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + color: var(--success); +} + +.alert-warning { + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.2); + color: var(--warning); +} + +.alert-danger { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + color: var(--danger); +} + +/* ===== INFO ROW / DETAIL ===== */ + +.info-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 24px; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 2px; +} + +.info-label { + font-size: 12px; + color: var(--gray-500); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.info-value { + font-size: 14px; + color: var(--white); +} + +/* ===== SEVERITY INDICATORS ===== */ + +.severity-critical { + color: var(--danger); + font-weight: 600; +} + +.severity-high { + color: var(--warning); + font-weight: 600; +} + +.severity-medium { + color: var(--cyan); + font-weight: 600; +} + +.severity-low { + color: var(--success); + font-weight: 600; +} + +/* ===== MOBILE MENU ===== */ + +.mobile-menu-btn { + display: none; + width: 38px; + height: 38px; + border-radius: var(--radius-xs); + background: transparent; + color: var(--white); + font-size: 20px; + align-items: center; + justify-content: center; +} + +/* ===== RESPONSIVE ===== */ + +@media (max-width: 768px) { + :root { + --sidebar-width: 0px; + } + + .sidebar { + transform: translateX(-100%); + transition: transform 0.3s ease; + width: 260px; + min-width: 260px; + left: 0; + } + + .sidebar.mobile-open { + transform: translateX(0); + box-shadow: 4px 0 24px rgba(0, 0, 0, 0.5); + } + + .sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 99; + } + + .sidebar-overlay.visible { + display: block; + } + + .main-content { + margin-left: 0; + } + + .mobile-menu-btn { + display: flex; + } + + .top-header { + padding: 0 16px; + } + + .page-content { + padding: 16px; + } + + .header-search input { + width: 140px; + } + + .header-search input:focus { + width: 180px; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + + .stat-card { + padding: 14px; + } + + .stat-card-value { + font-size: 22px; + } + + .section-header { + flex-direction: column; + align-items: flex-start; + } + + .filters-bar { + flex-direction: column; + align-items: flex-start; + } + + .filter-group select, + .filter-group input { + width: 100%; + min-width: 0; + } + + .chat-container { + height: 400px; + } + + .chat-message { + max-width: 90%; + } + + .modal-box { + max-width: 100%; + margin: 10px; + max-height: 85vh; + } + + .notifications-panel { + width: 300px; + right: -60px; + } + + .config-grid { + grid-template-columns: 1fr; + } + + .info-grid { + grid-template-columns: 1fr; + } + + .table-wrapper { + font-size: 12px; + } + + table th, + table td { + padding: 8px 10px; + font-size: 12px; + } + + .form-row { + flex-direction: column; + gap: 0; + } + + .login-card { + padding: 24px 20px; + } +} + +/* ===== UTILITY ===== */ + +.text-cyan { color: var(--cyan); } +.text-success { color: var(--success); } +.text-warning { color: var(--warning); } +.text-danger { color: var(--danger); } +.text-gray { color: var(--gray-500); } +.text-sm { font-size: 13px; } +.text-xs { font-size: 11px; } +.text-center { text-align: center; } +.text-right { text-align: right; } +.font-medium { font-weight: 500; } +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } +.mt-4 { margin-top: 4px; } +.mt-8 { margin-top: 8px; } +.mt-12 { margin-top: 12px; } +.mt-16 { margin-top: 16px; } +.mt-24 { margin-top: 24px; } +.mb-8 { margin-bottom: 8px; } +.mb-16 { margin-bottom: 16px; } +.mb-24 { margin-bottom: 24px; } +.gap-8 { gap: 8px; } +.gap-12 { gap: 12px; } +.gap-16 { gap: 16px; } +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.justify-center { justify-content: center; } +.flex-1 { flex: 1; } +.w-full { width: 100%; } +.hidden { display: none; } +.truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.animate-fade-in { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.animate-slide-up { + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +}