From 0af5fc4d2dddacc5bca2acf7ab4db3c302cfa0d2 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Tue, 11 Aug 2026 07:22:31 +0000 Subject: [PATCH] =?UTF-8?q?v1:=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=B5=D0=BD=D1=81=20=D0=BF=D0=BE=D1=80=D1=82=D0=B0=D0=BB=D1=8B?= =?UTF-8?q?=20=E2=80=94=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BD,=20=D0=B1=D0=B0?= =?UTF-8?q?=D1=81=D1=88=D1=8B=D0=BB=D1=8B=D2=9B,=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=B5=D0=BE,=20=D0=B7=D0=B0=D2=A3=D0=B4=D0=B0=D1=80,=20=D1=88?= =?UTF-8?q?=D0=B0=D2=93=D1=8B=D0=BC=D0=B4=D0=B0=D1=80,=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=82=D0=B5=D1=80,=20=D0=B4=D0=B0=D1=88=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + data.json | 59 +++++ index.html | 64 +++++ package-lock.json | 10 + package.json | 6 + script.js | 623 ++++++++++++++++++++++++++++++++++++++++++++++ server.js | 183 ++++++++++++++ style.css | 242 ++++++++++++++++++ 8 files changed, 1190 insertions(+) create mode 100644 .gitignore create mode 100644 data.json create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 script.js create mode 100644 server.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25006ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vibe42-run.log +.vibe42-run.pid diff --git a/data.json b/data.json new file mode 100644 index 0000000..0a28bc7 --- /dev/null +++ b/data.json @@ -0,0 +1,59 @@ +{ + "users": [ + { "login": "1001", "password": "Admin123", "name": "Асет (Комплаенс офицер)", "role": "admin" }, + { "login": "2001", "password": "Worker123", "name": "Айгүл Серікқызы", "role": "user" }, + { "login": "2002", "password": "Worker123", "name": "Бауыржан Нұрланұлы", "role": "user" }, + { "login": "2003", "password": "Worker123", "name": "Дана Қайратқызы", "role": "user" } + ], + "announcements": [ + { "id": 1, "title": "Жылдық комплаенс оқуы басталды", "text": "Барлық қызметкерлер 1 қарашаға дейін «Сыбайлас жемқорлыққа қарсы» тестін тапсыруы қажет. Видео сабақтар мен заңдар бөлімін қарап шығыңыз.", "date": "2026-08-11" }, + { "id": 2, "title": "Мүдделер қақтығысын декларациялау", "text": "Қызметкерлер жыл сайын мүдделер қақтығысы туралы декларация толтырады. Нысан шағым жәшігі бөлімінен алуға болады.", "date": "2026-08-05" } + ], + "org": [ + { "level": 1, "name": "Бас директор", "person": "Ерлан Қасымов" }, + { "level": 2, "name": "Басқарма төрағасы", "person": "Гүлнара Әбенова" }, + { "level": 3, "name": "Қаржы дирекциясы", "person": "Серік Жұмабаев" }, + { "level": 3, "name": "Эфир дирекциясы", "person": "Айжан Төлегенова" }, + { "level": 3, "name": "Жаңалықтар дирекциясы", "person": "Марат Бекенов" }, + { "level": 3, "name": "Техникалық дирекция", "person": "Нұрлан Сыдықов" }, + { "level": 3, "name": "HR және әкімшілік", "person": "Ләззат Қалиева" }, + { "level": 4, "name": "Комплаенс қызметі", "person": "Асет (сіз)" } + ], + "videos": [ + { "id": 1, "title": "Комплаенс деген не?", "desc": "Кіріспе: комплаенс жүйесінің негіздері, мақсаттары және рөлі.", "dur": "12 мин", "thumb": "https://picsum.photos/seed/compl1/480/270" }, + { "id": 2, "title": "Сыбайлас жемқорлыққа қарсы заңнама", "desc": "ҚР заңдары, тыйымдар мен жауапкершілік туралы негізгі ережелер.", "dur": "18 мин", "thumb": "https://picsum.photos/seed/compl2/480/270" }, + { "id": 3, "title": "Мүдделер қақтығысы", "desc": "Мүдделер қақтығысын қалай тану және декларациялау керек.", "dur": "15 мин", "thumb": "https://picsum.photos/seed/compl3/480/270" }, + { "id": 4, "title": "Сыйлықтар мен белгілер ережелері", "desc": "Қызметкерлерге қандай сыйлықтар қабылдауға рұқсат етіледі.", "dur": "10 мин", "thumb": "https://picsum.photos/seed/compl4/480/270" } + ], + "laws": [ + { "id": 1, "cat": "Антикор", "title": "«Сыбайлас жемқорлыққа қарсы іс-қимыл туралы» ҚР Заңы", "text": "Қызметкерлердің жемқорлық құқық бұзушылықтар жасауына тыйым салады, кірістер декларациясын міндеттейді." }, + { "id": 2, "cat": "Еңбек", "title": "ҚР Еңбек кодексі", "text": "Қызметкерлердің құқықтары мен міндеттері, еңбек тәртібі, жауапкершілік нормалары." }, + { "id": 3, "cat": "Антикор", "title": "Мүдделер қақтығысы туралы ереже", "text": "Компания ішіндегі мүдделер қақтығысын анықтау, жариялау және шешу тәртібі." }, + { "id": 4, "cat": "Ақпарат", "title": "«Ақпаратқа қол жеткізу туралы» ҚР Заңы", "text": "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары." }, + { "id": 5, "cat": "Этика", "title": "Компанияның Іскерлік этика кодексі", "text": "Қызметкерлердің мінез-құлық стандарттары, қарым-қатынас ережелері." } + ], + "tests": [ + { + "id": 1, "title": "Сыбайлас жемқорлыққа қарсы негіздер", "pass": 70, + "questions": [ + { "q": "Сыбайлас жемқорлық құқық бұзушылық деген не?", "options": ["Қызметкердің өз өкілеттігін жеке байыту үшін пайдалануы", "Жұмысқа уақытында келу", "Каникулға шығу", "Басшымен сөйлесу"], "correct": 0 }, + { "q": "Қызметкерге қандай сыйлық қабылдауға рұқсат?", "options": ["Кез келген уақытта кез келген сыйлық", "Құны 3 АЕК-тен аспайтын және есепке алынған сыйлық", "Тек ақшалай", "Барлық сыйлыққа тыйым салынған"], "correct": 1 }, + { "q": "Мүдделер қақтығысы дегеніміз?", "options": ["Жұмыстағы дау", "Жеке мүдде мен қызметтік міндеттердің қайшылығы", "Екі қызметкердің таласы", "Жұмыс орнын ауыстыру"], "correct": 1 }, + { "q": "Жемқорлық фактісін қайда хабарлау керек?", "options": ["Барлығына айту", "Комплаенс қызметіне немесе шағым жәшігіне", "Әлеуметтік желіде жазу", "Үндемеу"], "correct": 1 }, + { "q": "Кірістер мен мүлік туралы декларация кімге міндетті?", "options": ["Тек басшыларға", "Заңда көрсетілген қызметкерлерге", "Ешкімге", "Тек шетелге шығатындарға"], "correct": 1 } + ] + }, + { + "id": 2, "title": "Іскерлік этика кодексі", "pass": 70, + "questions": [ + { "q": "Іскерлік этика кодексінің мақсаты?", "options": ["Қызметкерлерді жазалау", "Мінез-құлық стандарттарын белгілеу", "Жалақыны азайту", "Каникулды қысқарту"], "correct": 1 }, + { "q": "Алқаптас әріптеске қалай қарау керек?", "options": ["Құрметпен және теңдікте", "Менсінбеу", "Елемеу", "Тек басшыларға ғана құрмет"], "correct": 0 }, + { "q": "Конфиденциалды ақпаратты не істеуге болмайды?", "options": ["Қауіпсіз жерде сақтау", "Үшінші тұлғаларға тарату", "Пароль қою", "Құлыптап қою"], "correct": 1 }, + { "q": "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?", "options": ["Келісу", "Бас тартып, комплаенс қызметіне хабарлау", "Ойланып жауап беру", "Ешкімге айтпау"], "correct": 1 }, + { "q": "Тест нәтижелері мен оқу материалдары кімге қажет?", "options": ["Тек HR-ға", "Барлық қызметкерлерге", "Тек админге", "Тек басшыларға"], "correct": 1 } + ] + } + ], + "complaints": [], + "testResults": [] +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..bf12e4c --- /dev/null +++ b/index.html @@ -0,0 +1,64 @@ + + + + + +Qonaev TV — Комплаенс порталы + + + + + + +
+ +
+ + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a3a4906 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "qonaev-compliance", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qonaev-compliance" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3850311 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "qonaev-compliance", + "private": true, + "type": "commonjs", + "scripts": { "start": "node server.js" } +} diff --git a/script.js b/script.js new file mode 100644 index 0000000..5350e6c --- /dev/null +++ b/script.js @@ -0,0 +1,623 @@ +/* ============================================================ + Qonaev TV — Комплаенс порталы + Режим 1: сервер арқылы (run) — нақты деректер, логин, статистика + Режим 2: Предпросмотр (офлайн) — localStorage арқылы демо + ============================================================ */ + +var state = { + user: null, + token: null, + offline: false, + content: null, + complaints: [], + testResults: [], + curPage: "home", + curTest: null, + curTestAnswers: {}, +}; + +var NAV = [ + { id: "home", ico: "🏠", title: "Басты бет" }, + { id: "org", ico: "🏛️", title: "Басшылық құрылымы" }, + { id: "video", ico: "🎬", title: "Видео сабақтар" }, + { id: "laws", ico: "⚖️", title: "Заң нормалары" }, + { id: "complaints", ico: "✉️", title: "Шағым жәшігі" }, + { id: "tests", ico: "📝", title: "Тесттер" }, + { id: "stats", ico: "📊", title: "Дашборд", admin: true }, +]; + +/* ---------------- ДЕМО ДЕРЕКТЕР (офлайн режим) ---------------- */ +var MOCK_USERS = [ + { login: "1001", password: "Admin123", name: "Асет (Комплаенс офицер)", role: "admin" }, + { login: "2001", password: "Worker123", name: "Айгүл Серікқызы", role: "user" }, + { login: "2002", password: "Worker123", name: "Бауыржан Нұрланұлы", role: "user" }, + { login: "2003", password: "Worker123", name: "Дана Қайратқызы", role: "user" }, +]; + +var MOCK_CONTENT = { + announcements: [ + { id: 1, title: "Жылдық комплаенс оқуы басталды", text: "Барлық қызметкерлер 1 қарашаға дейін «Сыбайлас жемқорлыққа қарсы» тестін тапсыруы қажет. Видео сабақтар мен заңдар бөлімін қарап шығыңыз.", date: "2026-08-11" }, + { id: 2, title: "Мүдделер қақтығысын декларациялау", text: "Қызметкерлер жыл сайын мүдделер қақтығысы туралы декларация толтырады. Нысан шағым жәшігі бөлімінен алуға болады.", date: "2026-08-05" }, + ], + org: [ + { level: 1, name: "Бас директор", person: "Ерлан Қасымов" }, + { level: 2, name: "Басқарма төрағасы", person: "Гүлнара Әбенова" }, + { level: 3, name: "Қаржы дирекциясы", person: "Серік Жұмабаев" }, + { level: 3, name: "Эфир дирекциясы", person: "Айжан Төлегенова" }, + { level: 3, name: "Жаңалықтар дирекциясы", person: "Марат Бекенов" }, + { level: 3, name: "Техникалық дирекция", person: "Нұрлан Сыдықов" }, + { level: 3, name: "HR және әкімшілік", person: "Ләззат Қалиева" }, + { level: 4, name: "Комплаенс қызметі", person: "Асет (сіз)" }, + ], + videos: [ + { id: 1, title: "Комплаенс деген не?", desc: "Кіріспе: комплаенс жүйесінің негіздері, мақсаттары және рөлі.", dur: "12 мин", thumb: "https://picsum.photos/seed/compl1/480/270" }, + { id: 2, title: "Сыбайлас жемқорлыққа қарсы заңнама", desc: "ҚР заңдары, тыйымдар мен жауапкершілік туралы негізгі ережелер.", dur: "18 мин", thumb: "https://picsum.photos/seed/compl2/480/270" }, + { id: 3, title: "Мүдделер қақтығысы", desc: "Мүдделер қақтығысын қалай тану және декларациялау керек.", dur: "15 мин", thumb: "https://picsum.photos/seed/compl3/480/270" }, + { id: 4, title: "Сыйлықтар мен белгілер ережелері", desc: "Қызметкерлерге қандай сыйлықтар қабылдауға рұқсат етіледі.", dur: "10 мин", thumb: "https://picsum.photos/seed/compl4/480/270" }, + ], + laws: [ + { id: 1, cat: "Антикор", title: "«Сыбайлас жемқорлыққа қарсы іс-қимыл туралы» ҚР Заңы", text: "Қызметкерлердің жемқорлық құқық бұзушылықтар жасауына тыйым салады, кірістер декларациясын міндеттейді." }, + { id: 2, cat: "Еңбек", title: "ҚР Еңбек кодексі", text: "Қызметкерлердің құқықтары мен міндеттері, еңбек тәртібі, жауапкершілік нормалары." }, + { id: 3, cat: "Антикор", title: "Мүдделер қақтығысы туралы ереже", text: "Компания ішіндегі мүдделер қақтығысын анықтау, жариялау және шешу тәртібі." }, + { id: 4, cat: "Ақпарат", title: "«Ақпаратқа қол жеткізу туралы» ҚР Заңы", text: "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары." }, + { id: 5, cat: "Этика", title: "Компанияның Іскерлік этика кодексі", text: "Қызметкерлердің мінез-құлық стандарттары, қарым-қатынас ережелері." }, + ], + tests: [ + { + id: 1, title: "Сыбайлас жемқорлыққа қарсы негіздер", dur: "10 сұрақ", pass: 70, + questions: [ + { q: "Сыбайлас жемқорлық құқық бұзушылық деген не?", options: ["Қызметкердің өз өкілеттігін жеке байыту үшін пайдалануы", "Жұмысқа уақытында келу", "Каникулға шығу", "Басшымен сөйлесу"], correct: 0 }, + { q: "Қызметкерге қандай сыйлық қабылдауға рұқсат?", options: ["Кез келген уақытта кез келген сыйлық", "Құны 3 АЕК-тен аспайтын және есепке алынған сыйлық", "Тек ақшалай", "Барлық сыйлыққа тыйым салынған"], correct: 1 }, + { q: "Мүдделер қақтығысы дегеніміз?", options: ["Жұмыстағы дау", "Жеке мүдде мен қызметтік міндеттердің қайшылығы", "Екі қызметкердің таласы", "Жұмыс орнын ауыстыру"], correct: 1 }, + { q: "Жемқорлық фактісін қайда хабарлау керек?", options: ["Барлығына айту", "Комплаенс қызметіне немесе шағым жәшігіне", "Әлеуметтік желіде жазу", "Үндемеу"], correct: 1 }, + { q: "Кірістер мен мүлік туралы декларация кімге міндетті?", options: ["Тек басшыларға", "Заңда көрсетілген қызметкерлерге", "Ешкімге", "Тек шетелге шығатындарға"], correct: 1 }, + ], + }, + { + id: 2, title: "Іскерлік этика кодексі", dur: "10 сұрақ", pass: 70, + questions: [ + { q: "Іскерлік этика кодексінің мақсаты?", options: ["Қызметкерлерді жазалау", "Мінез-құлық стандарттарын белгілеу", "Жалақыны азайту", "Каникулды қысқарту"], correct: 1 }, + { q: "Алқаптас әріптеске қалай қарау керек?", options: ["Құрметпен және теңдікте", "Менсінбеу", "Елемеу", "Тек басшыларға ғана құрмет"], correct: 0 }, + { q: "Конфиденциалды ақпаратты не істеуге болмайды?", options: ["Қауіпсіз жерде сақтау", "Үшінші тұлғаларға тарату", "Пароль қою", "Құлыптап қою"], correct: 1 }, + { q: "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?", options: ["Келісу", "Бас тартып, комплаенс қызметіне хабарлау", "Ойланып жауап беру", "Ешкімге айтпау"], correct: 1 }, + { q: "Тест нәтижелері мен оқу материалдары кімге қажет?", options: ["Тек HR-ға", "Барлық қызметкерлерге", "Тек админге", "Тек басшыларға"], correct: 1 }, + ], + }, + ], +}; + +/* ---------------- API ҚАБАТЫ ---------------- */ +function apiUrl() { return ""; } + +async function apiPost(path, body) { + try { + var r = await fetch(apiUrl() + path, { + method: "POST", + headers: { "Content-Type": "application/json", "Authorization": state.token ? "Bearer " + state.token : "" }, + body: JSON.stringify(body), + }); + try { + var ct = r.headers.get("content-type") || ""; + if (ct.indexOf("json") < 0) { state.offline = true; return { offline: true, ok: false }; } + return await r.json(); + } catch (e) { state.offline = true; return { offline: true, ok: false }; } + } catch (e) { + state.offline = true; + return { offline: true, ok: false }; + } +} + +async function apiGet(path) { + try { + var r = await fetch(apiUrl() + path, { + headers: { "Authorization": state.token ? "Bearer " + state.token : "" }, + }); + try { + var ct = r.headers.get("content-type") || ""; + if (ct.indexOf("json") < 0) { state.offline = true; return { offline: true, ok: false }; } + return await r.json(); + } catch (e) { state.offline = true; return { offline: true, ok: false }; } + } catch (e) { + state.offline = true; + return { offline: true, ok: false }; + } +} + +/* ---------------- ЛОКАЛДЫҚ САҚТАУ (офлайн) ---------------- */ +function lsGet(k, def) { try { var v = JSON.parse(localStorage.getItem("qcom_" + k)); return v === null ? def : v; } catch (e) { return def; } } +function lsSet(k, v) { localStorage.setItem("qcom_" + k, JSON.stringify(v)); } + +/* ---------------- КІРУ ---------------- */ +document.getElementById("loginForm").addEventListener("submit", async function (e) { + e.preventDefault(); + var login = document.getElementById("loginNum").value.trim(); + var pass = document.getElementById("loginPass").value; + var errEl = document.getElementById("loginError"); + errEl.textContent = ""; + + var res = await apiPost("/api/login", { login: login, password: pass }); + + if (res.offline) { + var mu = MOCK_USERS.find(function (u) { return u.login === login && u.password === pass; }); + if (!mu) { errEl.textContent = "Логин немесе құпия сөз қате. (Демо: 1001 / Admin123)"; return; } + state.user = { login: mu.login, name: mu.name, role: mu.role }; + state.offline = true; + state.content = MOCK_CONTENT; + state.complaints = lsGet("complaints", []); + state.testResults = lsGet("testResults", []); + enterApp(); + return; + } + + if (!res.ok) { errEl.textContent = res.error || "Кіру сәтсіз"; return; } + state.user = res.user; + state.token = res.token; + await loadServerData(); + enterApp(); +}); + +async function loadServerData() { + var c = await apiGet("/api/content"); + if (c.ok) state.content = c.data; + var cp = await apiGet("/api/complaints"); + if (cp.ok) state.complaints = cp.data; + var tr = await apiGet("/api/test-results"); + if (tr.ok) state.testResults = tr.data; +} + +function enterApp() { + document.getElementById("loginScreen").classList.add("hidden"); + document.getElementById("app").classList.remove("hidden"); + document.getElementById("userName").textContent = state.user.name; + document.getElementById("userAvatar").textContent = state.user.name.trim().charAt(0).toUpperCase(); + var rb = document.getElementById("roleBadge"); + rb.textContent = state.user.role === "admin" ? "Админ (комплаенс)" : "Қызметкер"; + rb.className = "role-badge " + state.user.role; + buildNav(); + go("home"); +} + +function buildNav() { + var nav = document.getElementById("nav"); + nav.innerHTML = ""; + NAV.forEach(function (item) { + if (item.admin && state.user.role !== "admin") return; + var b = document.createElement("button"); + b.className = "nav-item" + (item.admin ? " admin-only" : ""); + b.dataset.id = item.id; + b.innerHTML = '' + item.ico + "" + item.title + ""; + b.addEventListener("click", function () { go(item.id); }); + nav.appendChild(b); + }); +} + +function go(page) { + state.curPage = page; + document.querySelectorAll(".nav-item").forEach(function (n) { + n.classList.toggle("active", n.dataset.id === page); + }); + var t = NAV.find(function (n) { return n.id === page; }); + document.getElementById("pageTitle").textContent = t ? t.title : "Басты бет"; + var c = document.getElementById("content"); + c.innerHTML = ""; + if (page === "home") renderHome(c); + else if (page === "org") renderOrg(c); + else if (page === "video") renderVideo(c); + else if (page === "laws") renderLaws(c); + else if (page === "complaints") renderComplaints(c); + else if (page === "tests") renderTests(c); + else if (page === "stats") renderStats(c); +} + +function pageTitle(text) { + return '
' + text + "
"; +} + +/* ---------------- БАСТЫ БЕТ ---------------- */ +function renderHome(el) { + var myResults = state.testResults.filter(function (r) { return r.login === state.user.login; }); + var myComplaints = state.complaints.filter(function (c) { return c.authorLogin === state.user.login; }); + + var html = '
'; + html += '

👋 Сәлем, ' + esc(state.user.name) + "!

Комплаенс порталына қош келдіңіз. Мұнда заңдар, видео сабақтар, тесттер және шағым жәшігі бар.

"; + html += '
' + myResults.length + "
Сіз тапсырған тест
"; + html += '
' + myComplaints.length + "
Сіздің шағымдарыңыз
"; + html += '
' + (state.content ? state.content.videos.length : 0) + "
Видео сабақ
"; + html += "
"; + + html += '
📢 Хабарландырулар
'; + if (state.content && state.content.announcements.length) { + state.content.announcements.forEach(function (a) { + html += '

' + esc(a.title) + '

📅 ' + a.date + '

' + esc(a.text) + "

"; + }); + } else { + html += '
Хабарландыру жоқ
'; + } + + if (state.user.role === "admin") { + html += '

➕ Жаңа хабарландыру

'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + } + el.innerHTML = html; +} + +async function addAnnouncement() { + var title = document.getElementById("annTitle").value.trim(); + var text = document.getElementById("annText").value.trim(); + var date = document.getElementById("annDate").value || new Date().toISOString().slice(0, 10); + if (!title || !text) { alert("Тақырып пен мәтінді толтырыңыз"); return; } + if (state.offline) { + var list = state.content.announcements; + list.unshift({ id: Date.now(), title: title, text: text, date: date }); + renderHome(document.getElementById("content")); + return; + } + var res = await apiPost("/api/announcements", { title: title, text: text, date: date }); + if (res.ok) { + await loadServerData(); + renderHome(document.getElementById("content")); + } else alert(res.error || "Қате"); +} + +/* ---------------- БАСШЫЛЫҚ ҚҰРЫЛЫМЫ ---------------- */ +function renderOrg(el) { + var org = state.content ? state.content.org : []; + var html = pageTitle("🏛️ Басшылық құрылымы"); + var levels = {}; + org.forEach(function (o) { + if (!levels[o.level]) levels[o.level] = []; + levels[o.level].push(o); + }); + Object.keys(levels).sort(function (a, b) { return a - b; }).forEach(function (lv) { + html += '
'; + levels[lv].forEach(function (o) { + html += '
' + esc(o.name) + '
' + esc(o.person) + "
"; + }); + html += "
"; + html += '
▼
'; + }); + html = html.replace(/
▼<\/div>\s*$/, ""); + el.innerHTML = html; +} + +/* ---------------- ВИДЕО ---------------- */ +function renderVideo(el) { + var vids = state.content ? state.content.videos : []; + var html = pageTitle("🎬 Видео сабақтар"); + html += '
'; + vids.forEach(function (v) { + html += '
'; + html += '
▶
'; + html += "

" + esc(v.title) + '

' + esc(v.desc) + '

⏱ ' + v.dur + "
"; + html += "
"; + }); + html += "
"; + el.innerHTML = html; +} + +function openVideo(id) { + var v = state.content.videos.find(function (x) { return x.id === id; }); + if (!v) return; + var url = prompt("Видео сілтемесін қойыңыз (https://...)", v.link || "https://www.youtube.com/watch?v=dQw4w9WgXcQ"); + if (url && state.offline) { v.link = url; } + if (url) window.open(url, "_blank"); +} + +/* ---------------- ЗАҢДАР ---------------- */ +function renderLaws(el) { + var laws = state.content ? state.content.laws : []; + var cats = []; + laws.forEach(function (l) { if (cats.indexOf(l.cat) < 0) cats.push(l.cat); }); + + var html = pageTitle("⚖️ Заң нормалары мен комплаенс заңдар"); + html += '
'; + html += ''; + cats.forEach(function (c) { html += '"; }); + html += "
"; + html += '
'; + el.innerHTML = html; + filterLaws("all"); +} + +function filterLaws(cat) { + var laws = state.content ? state.content.laws : []; + document.querySelectorAll(".tab").forEach(function (t) { t.classList.toggle("active", t.dataset.cat === cat); }); + var list = cat === "all" ? laws : laws.filter(function (l) { return l.cat === cat; }); + var html = ""; + list.forEach(function (l) { + html += '

' + esc(l.title) + '

' + esc(l.cat) + "

" + esc(l.text) + "

"; + }); + document.getElementById("lawList").innerHTML = html || '
Табылмады
'; +} + +/* ---------------- ШАҒЫМ ЖӘШІГІ ---------------- */ +function renderComplaints(el) { + var html = pageTitle("✉️ Шағым жәшігі"); + html += '

Жаңа шағым / өтініш

'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += ''; + html += '
'; + + html += '
Менің шағымдарым' + (state.user.role === "admin" ? " және барлық өтініштер" : "") + "
"; + html += '
'; + el.innerHTML = html; + renderComplaintList(); +} + +function renderComplaintList() { + var list = state.complaints.slice().sort(function (a, b) { return b.id - a.id; }); + if (state.user.role !== "admin") { + list = list.filter(function (c) { return c.authorLogin === state.user.login || c.anonymous; }); + } + var html = ""; + list.forEach(function (c) { + var st = { "new": "Жаңа", "work": "Қаралуда", "done": "Шешілді" }[c.status] || c.status; + html += '
'; + html += '
' + esc(c.category) + ""; + html += '' + st + "
"; + html += '' + (c.anonymous ? "Аноним" : esc(c.authorName)) + " · " + c.date + "
"; + html += "

" + esc(c.text) + "

"; + if (state.user.role === "admin" && c.status !== "done") { + html += '
'; + if (c.status !== "work") html += ''; + html += ''; + html += "
"; + } + html += "
"; + }); + document.getElementById("compList").innerHTML = html || '
Әзірге шағым жоқ
'; +} + +async function sendComplaint() { + var category = document.getElementById("compCat").value; + var text = document.getElementById("compText").value.trim(); + var anonymous = document.getElementById("compAnon").checked; + if (!text) { document.getElementById("compMsg").innerHTML = '
Мәтінді толтырыңыз
'; return; } + + if (state.offline) { + state.complaints.unshift({ + id: Date.now(), category: category, text: text, anonymous: anonymous, + status: "new", date: new Date().toISOString().slice(0, 10), + authorLogin: anonymous ? null : state.user.login, + authorName: anonymous ? null : state.user.name, + }); + lsSet("complaints", state.complaints); + document.getElementById("compMsg").innerHTML = '
✅ Шағымыңыз жіберілді (демо-режимде жергілікті сақталды)
'; + document.getElementById("compText").value = ""; + renderComplaintList(); + return; + } + var res = await apiPost("/api/complaints", { category: category, text: text, anonymous: anonymous }); + if (res.ok) { + document.getElementById("compMsg").innerHTML = '
✅ Шағымыңыз қабылданды. Рахмет!
'; + document.getElementById("compText").value = ""; + await loadServerData(); + renderComplaintList(); + } else document.getElementById("compMsg").innerHTML = '
' + esc(res.error || "Қате") + "
"; +} + +async function setComplaintStatus(id, status) { + if (state.offline) { + var c = state.complaints.find(function (x) { return x.id === id; }); + if (c) { c.status = status; lsSet("complaints", state.complaints); } + renderComplaintList(); + return; + } + var res = await apiPost("/api/complaints/" + id + "/status", { status: status }); + if (res.ok) { await loadServerData(); renderComplaintList(); } + else alert(res.error || "Қате"); +} + +/* ---------------- ТЕСТТЕР ---------------- */ +function renderTests(el) { + if (state.curTest !== null) { renderTestRun(el); return; } + var tests = state.content ? state.content.tests : []; + var html = pageTitle("📝 Тесттер"); + html += '
'; + tests.forEach(function (t) { + var done = state.testResults.filter(function (r) { return r.testId === t.id; }); + html += '

' + esc(t.title) + "

"; + html += "

Сұрақтар саны: " + t.questions.length + " · өту үшін: " + t.pass + "%

"; + html += '
Әрекеттер: ' + done.length; + if (done.length) { + var last = done[done.length - 1]; + html += " · соңғысы: " + last.score + "%"; + } + html += "
"; + html += '
'; + }); + html += "
"; + html += '
Менің нәтижелерім
'; + html += '
'; + el.innerHTML = html; + + var mine = state.testResults.filter(function (r) { return r.login === state.user.login; }).slice().reverse(); + var th = ""; + if (mine.length) { + th = ""; + mine.forEach(function (r) { + var tn = tests.find(function (t) { return t.id === r.testId; }); + th += ""; + }); + th += "
ТестНәтижеКүні
" + esc(tn ? tn.title : "Тест") + "= (tn ? tn.pass : 70) ? "done" : "new") + "\">" + r.score + "%" + r.date + "
"; + } + document.getElementById("myTestResults").innerHTML = th || '
Әзірге тест тапсырған жоқсыз
'; +} + +function startTest(id) { + state.curTest = id; + state.curTestAnswers = {}; + renderTests(document.getElementById("content")); +} + +function renderTestRun(el) { + var t = state.content.tests.find(function (x) { return x.id === state.curTest; }); + if (!t) { state.curTest = null; renderTests(el); return; } + var html = ''; + html += '
' + esc(t.title) + "
"; + t.questions.forEach(function (q, qi) { + html += '

' + (qi + 1) + ". " + esc(q.q) + "

"; + q.options.forEach(function (op, oi) { + html += '"; + }); + html += "
"; + }); + html += ''; + el.innerHTML = html; +} + +function pickAnswer(elm, qi, oi) { + state.curTestAnswers[qi] = oi; + document.querySelectorAll('.q-option[data-q="' + qi + '"]').forEach(function (o) { + o.classList.toggle("selected", o.dataset.o === String(oi)); + }); +} + +async function submitTest() { + var t = state.content.tests.find(function (x) { return x.id === state.curTest; }); + var total = t.questions.length; + var answered = Object.keys(state.curTestAnswers).length; + if (answered < total) { alert("Барлық сұрақтарға жауап беріңіз (" + answered + "/" + total + ")"); return; } + + var score = 0; + t.questions.forEach(function (q, qi) { if (state.curTestAnswers[qi] === q.correct) score++; }); + var pct = Math.round(score / total * 100); + + var result = { testId: t.id, login: state.user.login, name: state.user.name, score: pct, date: new Date().toISOString().slice(0, 10) }; + + if (state.offline) { + state.testResults.push(result); + lsSet("testResults", state.testResults); + showTestResult(pct, score, total); + return; + } + var res = await apiPost("/api/test-results", { testId: t.id, score: pct }); + if (res.ok) { + await loadServerData(); + showTestResult(pct, score, total); + } else alert(res.error || "Қате"); +} + +function showTestResult(pct, score, total) { + state.curTest = null; + var el = document.getElementById("content"); + var html = '
'; + html += '
' + pct + "%
"; + html += "

Дұрыс жауаптар: " + score + " / " + total + "

"; + html += '
' + (pct >= 70 ? "✅ Тест сәтті өтілді!" : "Тест өтілмеді, қайта тапсыруға болады") + "
"; + html += ''; + html += "
"; + el.innerHTML = html; +} + +/* ---------------- ДАШБОРД (АДМИН) ---------------- */ +async function renderStats(el) { + var html = pageTitle("📊 Дашборд — статистика"); + + var data; + if (state.offline) { + data = computeStats(); + } else { + var r = await apiGet("/api/stats"); + if (r.ok) data = r.data; else data = computeStats(); + } + if (!data) { el.innerHTML = '
Деректер жоқ
'; return; } + + html += '
'; + html += '
' + data.users + "
Қызметкерлер
"; + html += '
' + data.complaintsTotal + "
Шағымдар (барлығы)
"; + html += '
' + data.complaintsOpen + "
Ашық шағымдар
"; + html += '
' + data.testsTotal + "
Тест тапсырулар
"; + html += "
"; + + html += '
'; + html += '

Шағымдар санаты бойынша

'; + html += '

Тест өту көрсеткіші

'; + html += "
"; + + html += '

Тест нәтижелері бойынша орташа балл

'; + + html += '
Соңғы шағымдар
'; + el.innerHTML = html; + + var catData = data.byCategory; + var catMax = Math.max.apply(null, catData.map(function (c) { return c.count; }).concat([1])); + var catHtml = ""; + catData.forEach(function (c) { + var w = Math.max(8, Math.round(c.count / catMax * 100)); + catHtml += '
' + esc(c.cat) + '
' + c.count + "
"; + }); + document.getElementById("catBars").innerHTML = catHtml || '
Жоқ
'; + + var passHtml = ""; + ["done", "work", "new"].forEach(function (s) { + var c = data.byStatus[s] || 0; + var w = data.complaintsTotal ? Math.max(8, Math.round(c / data.complaintsTotal * 100)) : 8; + var color = s === "done" ? "green" : s === "work" ? "orange" : "red"; + var name = s === "done" ? "Шешілді" : s === "work" ? "Қаралуда" : "Жаңа"; + passHtml += '
' + name + '
' + c + "
"; + }); + document.getElementById("passBars").innerHTML = passHtml; + + var avgHtml = ""; + data.byTest.forEach(function (t) { + avgHtml += '
' + esc(t.title) + '
' + t.avg + "%
"; + }); + document.getElementById("avgBars").innerHTML = avgHtml || '
Тест әлі жоқ
'; + + var cl = state.complaints.slice().sort(function (a, b) { return b.id - a.id; }).slice(0, 8); + var chtml = ""; + cl.forEach(function (c) { + var st = { "new": "Жаңа", "work": "Қаралуда", "done": "Шешілді" }[c.status] || c.status; + chtml += '
' + esc(c.category) + "" + (c.anonymous ? "Аноним" : esc(c.authorName)) + ' · ' + st + "

" + esc(c.text) + "

"; + }); + document.getElementById("statsComplaints").innerHTML = chtml || '
Шағым жоқ
'; +} + +function computeStats() { + var complaints = state.complaints || []; + var results = state.testResults || []; + var tests = state.content ? state.content.tests : []; + var byStatus = { new: 0, work: 0, done: 0 }; + var byCategory = {}; + complaints.forEach(function (c) { + byStatus[c.status] = (byStatus[c.status] || 0) + 1; + byCategory[c.category] = (byCategory[c.category] || 0) + 1; + }); + var catData = Object.keys(byCategory).map(function (k) { return { cat: k, count: byCategory[k] }; }); + var byTest = tests.map(function (t) { + var rs = results.filter(function (r) { return r.testId === t.id; }); + var avg = rs.length ? Math.round(rs.reduce(function (s, r) { return s + r.score; }, 0) / rs.length) : 0; + return { title: t.title, count: rs.length, avg: avg }; + }); + return { + users: state.offline ? MOCK_USERS.length : 0, + complaintsTotal: complaints.length, + complaintsOpen: byStatus.new + byStatus.work, + testsTotal: results.length, + byStatus: byStatus, + byCategory: catData, + byTest: byTest, + }; +} + +/* ---------------- ШЫҒУ ---------------- */ +document.getElementById("logoutBtn").addEventListener("click", function () { + state.user = null; + state.token = null; + document.getElementById("app").classList.add("hidden"); + document.getElementById("loginScreen").classList.remove("hidden"); + document.getElementById("loginPass").value = ""; +}); + +/* ---------------- УТИЛ ---------------- */ +function esc(s) { + return String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..8335405 --- /dev/null +++ b/server.js @@ -0,0 +1,183 @@ +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +const PORT = process.env.PORT || 3000; +const DATA_FILE = path.join(__dirname, "data.json"); + +function load() { + try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); } + catch (e) { return { users: [], announcements: [], org: [], videos: [], laws: [], tests: [], complaints: [], testResults: [] }; } +} +function save(d) { + fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2)); +} + +const sessions = new Map(); + +function body(req) { + return new Promise((resolve) => { + let s = ""; + req.on("data", (c) => (s += c)); + req.on("end", () => { + try { resolve(JSON.parse(s || "{}")); } catch (e) { resolve({}); } + }); + }); +} + +function send(res, code, obj) { + res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(obj)); +} + +function getUser(req) { + const h = req.headers["authorization"] || ""; + const token = h.replace(/^Bearer\s+/i, ""); + return sessions.get(token) || null; +} + +function publicUser(u) { + return { login: u.login, name: u.name, role: u.role }; +} + +http.createServer(async (req, res) => { + const url = req.url.split("?")[0]; + const db = load(); + + // ---------- ЛОГИН ---------- + if (req.method === "POST" && url === "/api/login") { + const b = await body(req); + const u = db.users.find((x) => x.login === String(b.login) && x.password === String(b.password)); + if (!u) return send(res, 401, { ok: false, error: "Логин немесе құпия сөз қате" }); + const token = crypto.randomBytes(24).toString("hex"); + sessions.set(token, u); + return send(res, 200, { ok: true, token, user: publicUser(u) }); + } + + // ---------- АУТЕНТИФИКАЦИЯ ---------- + const user = getUser(req); + const isApi = url.startsWith("/api/"); + if (isApi && !user) return send(res, 401, { ok: false, error: "Кіру қажет" }); + + // ---------- КОНТЕНТ ---------- + if (req.method === "GET" && url === "/api/content") { + return send(res, 200, { ok: true, data: { + announcements: db.announcements, + org: db.org, + videos: db.videos, + laws: db.laws, + tests: db.tests.map((t) => ({ + id: t.id, title: t.title, pass: t.pass, + questions: t.questions.map((q) => ({ q: q.q, options: q.options })), + })), + }}); + } + + // ---------- ХАБАРЛАНДЫРУ (админ) ---------- + if (req.method === "POST" && url === "/api/announcements") { + if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" }); + const b = await body(req); + if (!b.title || !b.text) return send(res, 400, { ok: false, error: "Тақырып пен мәтінді толтырыңыз" }); + db.announcements.unshift({ id: Date.now(), title: b.title, text: b.text, date: b.date || new Date().toISOString().slice(0, 10) }); + save(db); + return send(res, 200, { ok: true }); + } + + // ---------- ШАҒЫМДАР ---------- + if (req.method === "GET" && url === "/api/complaints") { + let list = db.complaints; + if (user.role !== "admin") { + list = list.filter((c) => c.authorLogin === user.login || c.anonymous); + } + return send(res, 200, { ok: true, data: list }); + } + + if (req.method === "POST" && url === "/api/complaints") { + const b = await body(req); + if (!b.text) return send(res, 400, { ok: false, error: "Мәтінді толтырыңыз" }); + const item = { + id: Date.now(), + category: b.category || "Басқа", + text: String(b.text).slice(0, 3000), + anonymous: !!b.anonymous, + status: "new", + date: new Date().toISOString().slice(0, 10), + authorLogin: b.anonymous ? null : user.login, + authorName: b.anonymous ? null : user.name, + }; + db.complaints.unshift(item); + save(db); + return send(res, 200, { ok: true }); + } + + const stMatch = url.match(/^\/api\/complaints\/(\d+)\/status$/); + if (req.method === "POST" && stMatch) { + if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" }); + const b = await body(req); + const c = db.complaints.find((x) => x.id === Number(stMatch[1])); + if (!c) return send(res, 404, { ok: false, error: "Табылмады" }); + if (["new", "work", "done"].includes(b.status)) c.status = b.status; + save(db); + return send(res, 200, { ok: true }); + } + + // ---------- ТЕСТ НӘТИЖЕЛЕРІ ---------- + if (req.method === "GET" && url === "/api/test-results") { + let list = db.testResults; + if (user.role !== "admin") list = list.filter((r) => r.login === user.login); + return send(res, 200, { ok: true, data: list }); + } + + if (req.method === "POST" && url === "/api/test-results") { + const b = await body(req); + const test = db.tests.find((t) => t.id === Number(b.testId)); + if (!test) return send(res, 400, { ok: false, error: "Тест табылмады" }); + db.testResults.push({ + id: Date.now(), + testId: test.id, + login: user.login, + name: user.name, + score: Number(b.score) || 0, + date: new Date().toISOString().slice(0, 10), + }); + save(db); + return send(res, 200, { ok: true }); + } + + // ---------- СТАТИСТИКА (админ) ---------- + if (req.method === "GET" && url === "/api/stats") { + if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" }); + const byStatus = { new: 0, work: 0, done: 0 }; + const byCategory = {}; + db.complaints.forEach((c) => { + byStatus[c.status] = (byStatus[c.status] || 0) + 1; + byCategory[c.category] = (byCategory[c.category] || 0) + 1; + }); + const byTest = db.tests.map((t) => { + const rs = db.testResults.filter((r) => r.testId === t.id); + const avg = rs.length ? Math.round(rs.reduce((s, r) => s + r.score, 0) / rs.length) : 0; + return { title: t.title, count: rs.length, avg }; + }); + return send(res, 200, { ok: true, data: { + users: db.users.length, + complaintsTotal: db.complaints.length, + complaintsOpen: byStatus.new + byStatus.work, + testsTotal: db.testResults.length, + byStatus, + byCategory: Object.keys(byCategory).map((k) => ({ cat: k, count: byCategory[k] })), + byTest, + }}); + } + + // ---------- СТАТИКА ---------- + let file = url === "/" ? "/index.html" : url; + const full = path.join(__dirname, file); + if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { + const ext = path.extname(full); + const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : ext === ".json" ? "application/json" : "text/html"; + res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); + return res.end(fs.readFileSync(full)); + } + send(res, 404, { ok: false, error: "Not found" }); +}).listen(PORT, () => console.log("Qonaev TV compliance порталы: http://localhost:" + PORT)); diff --git a/style.css b/style.css new file mode 100644 index 0000000..53daeda --- /dev/null +++ b/style.css @@ -0,0 +1,242 @@ +:root{ + --ink:#0F1218; + --cyan:#00E5FF; + --cyan-50:#E8FCFF; + --white:#fff; + --gray-500:#5B6573; + --gray-100:#F2F4F7; + --sidebar:#0F1218; + --accent:#2F80ED; + --green:#27AE60; + --red:#EB5757; + --orange:#F2994A; +} +*{box-sizing:border-box;margin:0;padding:0} +html,body{height:100%} +body{ + font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif; + color:var(--ink);background:#F5F7FA; +} +.hidden{display:none !important} +button{font-family:inherit} + +/* ===== КІРУ ЭКРАНЫ ===== */ +.login-wrap{ + min-height:100vh;display:flex;align-items:center;justify-content:center; + background:radial-gradient(1200px 600px at 20% -10%,#1B2B4A 0%,var(--ink) 55%); + padding:24px; +} +.login-card{ + width:100%;max-width:420px;background:var(--white);border-radius:20px; + padding:40px 36px;box-shadow:0 24px 60px rgba(0,0,0,.35); +} +.login-logo{ + width:64px;height:64px;border-radius:16px;background:var(--ink); + display:flex;align-items:center;justify-content:center;font-size:34px;margin:0 auto 18px; +} +.login-card h1{font-size:26px;text-align:center;font-weight:800} +.login-sub{text-align:center;color:var(--gray-500);margin-bottom:28px} +.login-card label{display:block;font-size:13px;font-weight:600;color:var(--gray-500);margin:14px 0 6px} +.login-card input{ + width:100%;padding:12px 14px;border:1.5px solid #DFE4EA;border-radius:10px; + font-size:16px;outline:none;transition:border-color .2s; +} +.login-card input:focus{border-color:var(--accent)} +.login-error{color:var(--red);font-size:13px;min-height:18px;margin-top:10px} +.login-hint{margin-top:18px;font-size:12px;color:var(--gray-500);text-align:center;background:var(--gray-100);border-radius:8px;padding:8px} + +/* ===== ҚОЛДАНБА ===== */ +.app{display:flex;min-height:100vh} +.sidebar{ + width:260px;background:var(--sidebar);color:#fff;flex-shrink:0; + display:flex;flex-direction:column;padding:22px 16px;position:sticky;top:0;height:100vh; +} +.side-brand{display:flex;align-items:center;gap:12px;padding:0 8px 20px;border-bottom:1px solid rgba(255,255,255,.08)} +.side-logo{font-size:30px} +.side-name{font-weight:800;font-size:17px} +.side-sub{color:#8A93A6;font-size:12px} +.nav{flex:1;padding:16px 0;overflow-y:auto} +.nav-item{ + display:flex;align-items:center;gap:12px;padding:11px 12px;margin-bottom:4px; + border-radius:10px;color:#B7BECC;cursor:pointer;font-size:15px;font-weight:500; + transition:background .15s,color .15s;border:none;background:none;width:100%;text-align:left; +} +.nav-item:hover{background:rgba(255,255,255,.06);color:#fff} +.nav-item.active{background:var(--cyan);color:var(--ink);font-weight:700} +.nav-item .ico{font-size:18px;width:24px;text-align:center} +.nav-item.admin-only{color:#9BE4FF} +.side-user{ + display:flex;align-items:center;gap:10px;border-top:1px solid rgba(255,255,255,.08); + padding:14px 8px 0; +} +.side-avatar{ + width:40px;height:40px;border-radius:50%;background:var(--cyan);color:var(--ink); + display:flex;align-items:center;justify-content:center;font-weight:800;font-size:17px;flex-shrink:0; +} +.side-username{font-size:14px;font-weight:600;word-break:break-word} +.side-logout{background:none;border:none;color:#8A93A6;font-size:12px;cursor:pointer;padding:0;margin-top:2px} +.side-logout:hover{color:var(--cyan)} + +.main{flex:1;min-width:0} +.topbar{ + display:flex;align-items:center;justify-content:space-between; + padding:20px 32px;background:var(--white);border-bottom:1px solid #E7EBF0; + position:sticky;top:0;z-index:10; +} +.topbar h2{font-size:21px;font-weight:800} +.role-badge{font-size:12px;font-weight:700;padding:6px 12px;border-radius:999px;background:var(--cyan-50);color:#0B6E7A} +.role-badge.admin{background:#EAF7EF;color:#1B7A3D} +.content{padding:28px 32px 60px;max-width:1100px} + +/* ===== КАРТОЧКАЛАР / ГРИД ===== */ +.grid{display:grid;gap:16px} +.grid-2{grid-template-columns:repeat(2,1fr)} +.grid-3{grid-template-columns:repeat(3,1fr)} +.grid-4{grid-template-columns:repeat(4,1fr)} +.card{background:var(--white);border:1px solid #E7EBF0;border-radius:14px;padding:22px} +.card h3{font-size:17px;font-weight:700;margin-bottom:12px} +.card p{color:var(--gray-500)} +.stat-card{border-left:4px solid var(--accent)} +.stat-card.orange{border-color:var(--orange)} +.stat-card.green{border-color:var(--green)} +.stat-card.red{border-color:var(--red)} +.stat-num{font-size:34px;font-weight:800;line-height:1.1} +.stat-label{font-size:13px;color:var(--gray-500);margin-top:4px} + +/* ===== КЕСТЕ ===== */ +table{width:100%;border-collapse:collapse;background:var(--white);border-radius:14px;overflow:hidden;border:1px solid #E7EBF0} +th,td{text-align:left;padding:12px 16px;border-bottom:1px solid #EFF2F6;font-size:14px;vertical-align:top} +th{background:var(--gray-100);font-weight:700;font-size:13px;color:var(--gray-500);white-space:nowrap} +tr:last-child td{border-bottom:none} + +/* ===== ФОРМА ===== */ +.form label{display:block;font-size:13px;font-weight:600;color:var(--gray-500);margin:14px 0 6px} +.form input,.form textarea,.form select{ + width:100%;padding:11px 14px;border:1.5px solid #DFE4EA;border-radius:10px; + font-size:15px;outline:none;font-family:inherit;resize:vertical; +} +.form input:focus,.form textarea:focus,.form select:focus{border-color:var(--accent)} +.form-row{display:flex;gap:14px} +.form-row>div{flex:1} + +/* ===== КНОПКА ===== */ +.btn{ + display:inline-block;border:none;border-radius:10px;padding:12px 22px; + font-size:15px;font-weight:700;cursor:pointer;transition:opacity .15s,transform .1s; +} +.btn:hover{opacity:.9} +.btn:active{transform:scale(.98)} +.btn-primary{background:var(--accent);color:#fff} +.btn-cyan{background:var(--cyan);color:var(--ink)} +.btn-ghost{background:var(--gray-100);color:var(--ink)} +.btn-danger{background:#FDECEC;color:var(--red)} +.btn-block{width:100%} +.btn-sm{padding:8px 14px;font-size:13px} + +/* ===== БӘДЖ ===== */ +.badge{display:inline-block;font-size:12px;font-weight:700;padding:4px 10px;border-radius:999px;white-space:nowrap} +.badge.new{background:#FDECEC;color:var(--red)} +.badge.work{background:#FEF3E2;color:var(--orange)} +.badge.done{background:#EAF7EF;color:#1B7A3D} +.badge.admin{background:var(--cyan-50);color:#0B6E7A} +.badge.user{background:#EEF1F6;color:var(--gray-500)} + +/* ===== ҚАБЫЛДАУ / ХАБАРЛАУ ===== */ +.msg{ + padding:12px 16px;border-radius:10px;font-size:14px;font-weight:600;margin:14px 0; +} +.msg.ok{background:#EAF7EF;color:#1B7A3D} +.msg.err{background:#FDECEC;color:var(--red)} + +/* ===== ҚҰРЫЛЫМ (орг) ===== */ +.org-level{display:flex;flex-wrap:wrap;gap:12px;justify-content:center} +.org-card{ + background:var(--white);border:1px solid #E7EBF0;border-radius:12px; + padding:16px 20px;min-width:200px;text-align:center; +} +.org-card.top{background:var(--ink);color:#fff;border-color:var(--ink)} +.org-name{font-weight:700;font-size:15px} +.org-pos{font-size:12px;color:var(--gray-500);margin-top:2px} +.org-card.top .org-pos{color:#9AA6BB} +.org-arrow{text-align:center;color:var(--gray-500);font-size:20px;padding:6px 0} + +/* ===== ВИДЕО ===== */ +.video-card{cursor:pointer;overflow:hidden;display:flex;flex-direction:column} +.video-thumb{ + height:150px;background:var(--ink);border-radius:10px;margin-bottom:12px; + display:flex;align-items:center;justify-content:center;color:#fff;font-size:40px; + background-size:cover;background-position:center; +} +.video-card h3{font-size:15px} +.video-card p{font-size:13px} + +/* ===== ТЕСТ ===== */ +.test-question{margin-bottom:20px} +.test-question h4{font-size:15px;font-weight:700;margin-bottom:10px} +.q-option{ + display:block;padding:11px 14px;border:1.5px solid #DFE4EA;border-radius:10px; + margin-bottom:8px;cursor:pointer;transition:all .15s; +} +.q-option:hover{border-color:var(--accent);background:var(--cyan-50)} +.q-option.selected{border-color:var(--accent);background:#E3F0FF;font-weight:600} +.q-option.correct{border-color:var(--green);background:#EAF7EF;font-weight:700} +.q-option.wrong{border-color:var(--red);background:#FDECEC} +.q-option.disabled{cursor:default;pointer-events:none} +.test-score{font-size:26px;font-weight:800;color:var(--green);text-align:center;margin:10px 0} + +/* ===== ЖАЛОБА ===== */ +.complaint-item{border-left:4px solid #DFE4EA} +.complaint-item.new{border-color:var(--red)} +.complaint-item.work{border-color:var(--orange)} +.complaint-item.done{border-color:var(--green)} +.cat-tag{font-size:12px;font-weight:700;padding:3px 10px;border-radius:999px;background:var(--gray-100);color:var(--gray-500)} + +/* ===== ДИАГРАММА ===== */ +.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px} +.bar-label{width:180px;font-size:13px;font-weight:600;flex-shrink:0} +.bar-track{flex:1;background:var(--gray-100);border-radius:8px;height:26px;overflow:hidden} +.bar-fill{height:100%;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:flex-end;padding-right:8px;color:#fff;font-size:12px;font-weight:700;min-width:26px;transition:width .5s} +.bar-fill.green{background:var(--green)} +.bar-fill.orange{background:var(--orange)} +.bar-fill.red{background:var(--red)} + +/* ===== УТИЛИТ ===== */ +.row{display:flex;gap:14px;align-items:center;flex-wrap:wrap} +.spread{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap} +.muted{color:var(--gray-500);font-size:13px} +.empty{text-align:center;color:var(--gray-500);padding:30px 0} +.section-title{font-size:19px;font-weight:800;margin-bottom:16px;display:flex;align-items:center;gap:10px} +.chip-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px} +.chip{ + border:1.5px solid #DFE4EA;background:var(--white);border-radius:999px; + padding:7px 16px;font-size:13px;font-weight:600;cursor:pointer; +} +.chip.active{background:var(--accent);border-color:var(--accent);color:#fff} +.announce{background:var(--white);border:1px solid #E7EBF0;border-radius:12px;padding:18px 20px;margin-bottom:12px;border-left:4px solid var(--cyan)} +.announce h3{font-size:16px;margin-bottom:4px} +.announce .date{font-size:12px;color:var(--gray-500);margin-bottom:8px} +.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px} +.tab{ + border:1.5px solid #DFE4EA;background:var(--white);border-radius:10px; + padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer; +} +.tab.active{background:var(--ink);border-color:var(--ink);color:#fff} +.search{width:100%;max-width:360px;padding:10px 14px;border:1.5px solid #DFE4EA;border-radius:10px;font-size:14px;outline:none} +.search:focus{border-color:var(--accent)} + +@media (max-width:900px){ + .grid-4{grid-template-columns:repeat(2,1fr)} + .grid-3{grid-template-columns:repeat(2,1fr)} + .grid-2{grid-template-columns:1fr} + .sidebar{width:76px;padding:22px 10px} + .side-brand>div,.side-sub,.side-name,.nav-item span:not(.ico),.side-username{display:none} + .nav-item{justify-content:center;padding:11px} + .side-user{justify-content:center} + .side-avatar{margin:0} + .content{padding:20px 16px} + .topbar{padding:16px} +} +@media (max-width:560px){ + .grid-3,.grid-4{grid-template-columns:1fr} + .form-row{flex-direction:column} +}