/* ============================================================ 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: "contacts", ico: "📞", title: "Мемлекеттік органдар" }, { id: "insider", ico: "🔐", title: "Инсайдерлік ақпарат" }, { id: "video", ico: "🎬", title: "Видео сабақтар" }, { id: "laws", ico: "⚖️", title: "Заң нормалары" }, { id: "complaints", ico: "✉️", title: "Шағым жәшігі" }, { id: "tests", ico: "📝", title: "Тесттер" }, { id: "users", ico: "👥", title: "Қызметкерлер", admin: true }, { 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: "Ерлан Қасымов", email: "e.kasymov@qonaev.kz", phone: "+7 (727) 300-10-01" }, { level: 2, name: "Басқарма төрағасы", person: "Гүлнара Әбенова", email: "g.abenova@qonaev.kz", phone: "+7 (727) 300-10-02" }, { level: 3, name: "Қаржы дирекциясы", person: "Серік Жұмабаев", email: "s.zhumabaev@qonaev.kz", phone: "+7 (727) 300-10-03" }, { level: 3, name: "Эфир дирекциясы", person: "Айжан Төлегенова", email: "a.tolegenova@qonaev.kz", phone: "+7 (727) 300-10-04" }, { level: 3, name: "Жаңалықтар дирекциясы", person: "Марат Бекенов", email: "m.bekenov@qonaev.kz", phone: "+7 (727) 300-10-05" }, { level: 3, name: "Техникалық дирекция", person: "Нұрлан Сыдықов", email: "n.sydykov@qonaev.kz", phone: "+7 (727) 300-10-06" }, { level: 3, name: "HR және әкімшілік", person: "Ләззат Қалиева", email: "l.kalieva@qonaev.kz", phone: "+7 (727) 300-10-07" }, { level: 4, name: "Комплаенс қызметі", person: "Асет (сіз)", email: "compliance@qonaev.kz", phone: "+7 (727) 300-10-08" }, ], contacts: [ { org: "Антикор (ҚР Антикоррупциялық қызметі) Алматы қаласы бойынша департаменті", type: "Ресми телефон", phone: "8 (727) 278-73-24", email: "almaty@nab.gov.kz", note: "Мекенжай: Жібек Жолы даңғылы, 15" }, { org: "Антикор бірыңғай Call-орталығы", type: "Сенім телефоны", phone: "1424", email: "", note: "Қазақстан бойынша қоңырау тегін" }, { org: "Антикор WhatsApp-чат-бот", type: "WhatsApp", phone: "8 777 000 1424", email: "", note: "Хабарлама жіберуге болады" }, { org: "Алматы қаласы прокуратурасы", type: "Кеңсе", phone: "8 (727) 225-40-86", email: "", note: "Мекенжай: Желтоқсан көшесі, 189" }, { org: "Алматы қаласы прокуратурасы", type: "Кезекші прокурор", phone: "8 (727) 330-80-51", email: "", note: "Тәулік бойы" }, { org: "ҚР Бас прокуратурасы Call-орталығы", type: "Сенім телефоны", phone: "115", email: "", note: "Дс-жм 09:00–18:30, қоңырау тегін" }, { org: "Алматы қаласы Еңбек инспекциясы басқармасы", type: "Ресми телефон", phone: "+7 (727) 338-22-42", email: "upr_gitm@mail.ru", note: "Мекенжай: Достық даңғылы, 85" }, { org: "Алматы қаласы Еңбек инспекциясы (канцелярия)", type: "Телефон", phone: "8 (727) 374-94-47", email: "kgu2016@mail.ru", note: "" }, { org: "Алматы қаласы бойынша Мемлекеттік кірістер департаменті", type: "Ресми телефон", phone: "8 (7272) 267-15-54", email: "", note: "Мекенжай: Бөгенбай батыр көшесі, 132" }, ], 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 }, ], }, { id: 3, title: "Инсайдерлік ақпарат және құпиялылық", dur: "10 сұрақ", pass: 70, questions: [ { q: "Инсайдерлік ақпарат дегеніміз не?", options: ["Әріптестер туралы қауесет", "Компания туралы әлі жарияланбаған қызметтік деректер", "Әлеуметтік желідегі ақпарат", "Жалпыға белгілі жаңалықтар"], correct: 1 }, { q: "Жаңа сюжет эфирге шыққанға дейін оны танысқа айтуға бола ма?", options: ["Иә, егер ол жақын таныс болса", "Жоқ, инсайдерлік ақпаратты таратуға тыйым салынады", "Иә, бірақ тек SMS арқылы", "Болады, себебі бұл жай ғана жаңалық"], correct: 1 }, { q: "Дереккөздің атын басшылыққа ашуға бола ма?", options: ["Иә, міндетті түрде", "Жоқ, журналист дереккөзді құпия ұстайды", "Тек түскі үзілісте айтуға болады", "Тек жазбаша түрде"], correct: 1 }, { q: "Инсайдерлік ақпаратты таратқаны үшін не болады?", options: ["Ештеңе болмайды", "Тәртіптік жауапкершілік, ауыр жағдайда заң бойынша жауапкершілік", "Тек сөгіс беріледі", "Бонус өседі"], correct: 1 }, { q: "Қызметтік ақпаратты қай жерде талқылауға БОЛМАЙДЫ?", options: ["Кеңседе әріптестермен", "Көпшілік орындарда және әлеуметтік желілерде", "Ресми жиналыста", "Комплаенс офицермен кеңесте"], correct: 1 }, ], }, ], insider: [ { title: "Инсайдерлік ақпарат дегеніміз не?", text: "Бұл компания туралы әлі жарияланбаған, қызметкерге өз жұмысы барысында белгілі болған деректер: қаржылық көрсеткіштер, келісімшарттар, жаңалық сюжеттері, кадрлық шешімдер." }, { title: "Таратуға тыйым салынады", text: "Инсайдерлік ақпаратты эфирге шыққанға дейін немесе ресми жарияланғанға дейін үшінші тұлғаларға, әлеуметтік желілерде немесе таныстарға таратуға тыйым салынады." }, { title: "Дереккөздерді құпия ұстау", text: "Журналистер өз дереккөздерін құпия ұстауға міндетті. Дереккөздің атын ашу — еңбек тәртібін өрескел бұзу." }, { title: "Не істеу керек?", text: "Инсайдерлік ақпаратқа қол жеткізгенде және оны пайдалану қажет болғанда — комплаенс офицермен кеңесіңіз. Ақпарат тек қызметтік қажеттілік үшін пайдаланылады." }, { title: "Жауапкершілік", text: "Инсайдерлік ақпаратты тарату — тәртіптік жауапкершілікке әкеледі, ал ауыр жағдайларда ҚР заңнамасы бойынша қылмыстық жауапкершілікке де тартылуы мүмкін." }, ], giftRules: { allowed: [ "Құны 3 АЕК-тен аспайтын еске салу сыйлығы (күнтізбе, қалам, кесе)", "Ресми іс-шарада берілген естелік сыйлықтар", "Мерекелік корпоративтік сыйлықтар (компания атынан)", "Іскерлік келіссөздердегі қонақжайлылық (кешкі ас, кофе)", ], forbidden: [ "Ақша және оған балама (карта, купон, сертификат)", "Құны 3 АЕК-тен асатын сыйлықтар", "Қызметтік шешімге әсер ететін сыйлықтар (мүдделер қақтығысы)", "Кез келген уақытта қолма-қол ақша немесе қарыз беру", ], }, }; /* ---------------- 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("forgotBtn").addEventListener("click", function () { document.getElementById("recoverBox").classList.toggle("hidden"); }); document.getElementById("recoverGo").addEventListener("click", async function () { var login = document.getElementById("recoverLogin").value.trim(); var msg = document.getElementById("recoverMsg"); if (!login) { msg.textContent = "Логиніңізді жазыңыз"; return; } var res = await apiPost("/api/recover-password", { login: login }); if (res.ok) { msg.innerHTML = "" + esc(res.name) + ", жаңа құпия сөзіңіз: " + esc(res.password) + ""; } else if (res.offline) { var mu = MOCK_USERS.find(function (u) { return u.login === login; }); if (!mu) { msg.textContent = "Мұндай логин табылмады"; return; } var np = "Qonaev" + Math.floor(1000 + Math.random() * 9000); mu.password = np; msg.innerHTML = "" + esc(mu.name) + ", жаңа құпия сөзіңіз: " + np + " (демо-режим)"; } else { msg.textContent = res.error || "Қате"; } }); 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", []); state.directMessages = lsGet("directMessages", []); 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; var dm = await apiGet("/api/direct-messages"); if (dm.ok) state.directMessages = dm.data; if (state.user && state.user.role === "admin") { var us = await apiGet("/api/users"); if (us.ok) state.users = us.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 = ""; var newCount = newItemsCount(); 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 + ""; if (state.user.role === "admin" && (item.id === "complaints" || item.id === "stats") && newCount > 0) { b.innerHTML += '' + newCount + ""; } b.addEventListener("click", function () { go(item.id); }); nav.appendChild(b); }); } function newItemsCount() { var n = 0; (state.complaints || []).forEach(function (c) { if (c.status === "new") n++; }); (state.directMessages || []).forEach(function (m) { if (m.status === "new") n++; }); return n; } 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 === "contacts") renderContacts(c); else if (page === "insider") renderInsider(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 === "users") renderUsers(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 += "
"; if (state.user.role === "admin") { var newCount = newItemsCount(); if (newCount > 0) { html += '
'; html += '

🔔 Жаңа хабарламалар: ' + newCount + "

"; html += '

Шағым жәшігіне және жеке хаттарға жаңа өтініштер келді. Жауап беру үшін «Шағым жәшігі» немесе «Дашборд» бөліміне өтіңіз.

'; html += ''; 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 += '
'; html += '
' + esc(o.name) + '
' + esc(o.person) + "
"; if (o.phone || o.email) { html += '
'; if (o.phone) html += '"; if (o.email) html += '"; html += "
"; } html += "
"; }); html += "
"; html += '
▼
'; }); html = html.replace(/
▼<\/div>\s*$/, ""); el.innerHTML = html; } /* ---------------- МЕМЛЕКЕТТІК ОРГАНДАР ---------------- */ function renderContacts(el) { var list = state.content && state.content.contacts ? state.content.contacts : []; var html = pageTitle("📞 Мемлекеттік органдар — Алматы қаласы"); html += '

Комплаенс мәселелері бойынша хабарласуға болатын мемлекеттік органдардың ресми байланыс деректері.

'; html += '
'; list.forEach(function (c) { html += '

' + esc(c.org) + '

' + esc(c.type) + "
"; html += '
'; if (c.phone) html += '"; if (c.email) html += '"; if (c.note) html += '
📍 ' + esc(c.note) + "
"; html += "
"; }); html += "
"; 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("⚖️ Заң нормалары мен комплаенс заңдар"); var gr = state.content && state.content.giftRules ? state.content.giftRules : null; if (gr) { html += '
'; html += '

🎁 Сыйлықтар ережесі

'; html += '
'; html += '
✅ Рұқсат етілген
'; gr.allowed.forEach(function (t) { html += '
' + esc(t) + "
"; }); html += "
"; html += '
🚫 Тыйым салынған
'; gr.forbidden.forEach(function (t) { html += '
' + esc(t) + "
"; }); html += "
"; html += "
"; } 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 renderInsider(el) { var list = state.content && state.content.insider ? state.content.insider : []; var html = pageTitle("🔐 Инсайдерлік ақпарат және құпиялылық"); html += '

Медиа саласында инсайдерлік ақпаратпен жұмыс — ең маңызды талаптардың бірі. Төмендегі ережелерді оқып, тест тапсыру ұсынылады.

'; list.forEach(function (item, i) { html += '
'; html += '

' + (i + 1) + ". " + esc(item.title) + "

"; html += "

" + esc(item.text) + "

"; html += "
"; }); html += '
'; html += '

📝 Материалды оқыдыңыз ба?

'; html += '

«Инсайдерлік ақпарат және құпиялылық» тестін тапсырып, біліміңізді тексеріңіз.

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

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

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

🔒 Комплаенс офицерге жеке хат

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

" + esc(m.text) + "

"; if (m.reply) { html += '
📨 Комплаенс офицердің жауабы (' + (m.replyDate || "") + '):

' + esc(m.reply) + "

"; } if (state.user.role === "admin") { html += '
'; html += ''; html += ''; if (m.status !== "done") { if (m.status !== "work") html += ''; html += ''; } html += "
"; } html += "
"; }); document.getElementById("dmList").innerHTML = html || '
Хаттар жоқ — комплаенс офицерге жаза аласыз
'; } 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 (c.reply) { html += '
📨 Жауап (' + c.replyDate + '):

' + esc(c.reply) + "

"; } if (state.user.role === "admin") { html += '
'; html += ''; html += ''; if (c.status !== "done") { if (c.status !== "work") html += ''; html += ''; } html += "
"; } html += "
"; }); document.getElementById("compList").innerHTML = html || '
Әзірге шағым жоқ
'; } async function replyComplaint(id) { var reply = document.getElementById("replyComp_" + id).value.trim(); if (!reply) { alert("Жауап мәтінін жазыңыз"); return; } if (state.offline) { var c = state.complaints.find(function (x) { return x.id === id; }); if (c) { c.reply = reply; c.replyDate = new Date().toISOString().slice(0, 10); if (c.status === "new") c.status = "work"; lsSet("complaints", state.complaints); } renderComplaintList(); return; } var res = await apiPost("/api/complaints/" + id + "/reply", { reply: reply }); if (res.ok) { await loadServerData(); renderComplaintList(); } else alert(res.error || "Қате"); } 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 || "Қате"); } async function sendDirectMsg() { var category = document.getElementById("dmCat").value; var text = document.getElementById("dmText").value.trim(); var anonymous = document.getElementById("dmAnon").checked; if (!text) { document.getElementById("dmMsg").innerHTML = '
Хат мәтінін жазыңыз
'; return; } if (state.offline) { var list = lsGet("directMessages", []); list.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, }); state.directMessages = list; lsSet("directMessages", list); document.getElementById("dmMsg").innerHTML = '
✅ Хат жіберілді (демо-режимде жергілікті сақталды)
'; document.getElementById("dmText").value = ""; renderMyDmList(); return; } var res = await apiPost("/api/direct-messages", { category: category, text: text, anonymous: anonymous }); if (res.ok) { document.getElementById("dmMsg").innerHTML = '
✅ Хатыңыз комплаенс офицерге жіберілді
'; document.getElementById("dmText").value = ""; await loadServerData(); renderMyDmList(); } else document.getElementById("dmMsg").innerHTML = '
' + esc(res.error || "Қате") + "
"; } /* ---------------- ҚЫЗМЕТКЕРЛЕР (АККАУНТТАР, админ) ---------------- */ async function renderUsers(el) { var html = pageTitle("👥 Қызметкерлер және аккаунттар"); html += '

Логиндер мен құпия сөздер users.json файлында сақталады. Мұнда жаңа қызметкер қосуға, құпия сөз ауыстыруға және аккаунтты өшіруге болады.

'; html += '

➕ Жаңа қызметкер қосу

'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''; html += '
'; html += '
'; el.innerHTML = html; loadUsersList(); } async function loadUsersList() { var users; if (state.offline) { users = MOCK_USERS; } else { var r = await apiGet("/api/users"); if (!r.ok) return; users = r.data; } var html = '
'; users.forEach(function (u) { html += ""; html += '"; html += ''; html += '"; }); html += "
ЛогинАты-жөніРөлҚұпия сөз
" + esc(u.login) + "" + esc(u.name) + "' + (u.role === "admin" ? "Админ" : "Қызметкер") + "' + esc(u.password) + ''; html += '' + (u.role !== "admin" ? '' : "") + "
"; document.getElementById("usersList").innerHTML = html; } async function addUser() { var login = document.getElementById("uLogin").value.trim(); var name = document.getElementById("uName").value.trim(); var pass = document.getElementById("uPass").value.trim(); var role = document.getElementById("uRole").value; var msg = document.getElementById("uMsg"); if (!login || !name || !pass) { msg.innerHTML = '
Барлық өрістерді толтырыңыз
'; return; } if (state.offline) { MOCK_USERS.push({ login: login, name: name, password: pass, role: role }); msg.innerHTML = '
✅ Қосылды (демо)
'; loadUsersList(); return; } var res = await apiPost("/api/users", { login: login, name: name, password: pass, role: role }); if (res.ok) { msg.innerHTML = '
✅ Қызметкер қосылды
'; document.getElementById("uLogin").value = ""; document.getElementById("uName").value = ""; document.getElementById("uPass").value = ""; loadUsersList(); } else msg.innerHTML = '
' + esc(res.error || "Қате") + "
"; } async function delUser(login) { if (!confirm("«" + login + "» аккаунтын өшіру керек пе?")) return; if (state.offline) { var i = MOCK_USERS.findIndex(function (u) { return u.login === login; }); if (i >= 0) MOCK_USERS.splice(i, 1); loadUsersList(); return; } var res = await fetch(apiUrl() + "/api/users/" + encodeURIComponent(login), { method: "DELETE", headers: { "Authorization": state.token ? "Bearer " + state.token : "" }, }); if (res.ok) loadUsersList(); else alert("Қате"); } async function resetUserPass(login) { var np = prompt("«" + login + "» үшін жаңа құпия сөз:"); if (!np) return; if (state.offline) { var u = MOCK_USERS.find(function (x) { return x.login === login; }); if (u) u.password = np; loadUsersList(); return; } var res = await apiPost("/api/users/" + encodeURIComponent(login) + "/password", { password: np }); if (res.ok) loadUsersList(); 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 answers = t.questions.map(function (q, qi) { return state.curTestAnswers[qi]; }); if (state.offline) { var correct = 0; t.questions.forEach(function (q, qi) { if (state.curTestAnswers[qi] === q.correct) correct++; }); var pct = Math.round(correct / total * 100); state.testResults.push({ testId: t.id, login: state.user.login, name: state.user.name, score: pct, date: new Date().toISOString().slice(0, 10) }); lsSet("testResults", state.testResults); showTestResult(pct, correct, total); return; } var res = await apiPost("/api/test-results", { testId: t.id, answers: answers }); if (res.ok) { await loadServerData(); showTestResult(res.score, res.correct, res.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 += "
"; if (state.user.role === "admin") { html += '
'; html += '
' + (data.dmTotal || 0) + "
Жеке хаттар (барлығы)
"; html += '
' + (data.dmOpen || 0) + "
Жауап күтіп тұрған хаттар
"; html += '
' + (data.dmDone || 0) + "
Шешілген хаттар
"; html += "
"; } 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) + "

"; if (c.reply) { chtml += '
📨 Жауап:

' + esc(c.reply) + "

"; } chtml += "
"; }); document.getElementById("statsComplaints").innerHTML = chtml || '
Шағым жоқ
'; if (state.user.role === "admin") { var dms = state.directMessages || []; dms.sort(function (a, b) { return b.id - a.id; }); var dhtml = ""; dms.forEach(function (m) { var st = { "new": "Жаңа", "work": "Қаралуда", "done": "Шешілді" }[m.status] || m.status; dhtml += '
'; dhtml += '
' + esc(m.category) + "" + (m.anonymous ? "Аноним" : esc(m.authorName)) + ' · ' + st + "
"; dhtml += '

' + esc(m.text) + "

"; if (m.reply) { dhtml += '
📨 Жауап:

' + esc(m.reply) + "

"; } dhtml += '
'; dhtml += ''; dhtml += ''; if (m.status !== "done") { if (m.status !== "work") dhtml += ''; dhtml += ''; } dhtml += "
"; }); document.getElementById("statsDm").innerHTML = dhtml || '
Жеке хаттар жоқ
'; } } async function replyDm(id) { var reply = document.getElementById("replyDm_" + id).value.trim(); if (!reply) { alert("Жауап мәтінін жазыңыз"); return; } if (state.offline) { var list = lsGet("directMessages", []); var m = list.find(function (x) { return x.id === id; }); if (m) { m.reply = reply; m.replyDate = new Date().toISOString().slice(0, 10); if (m.status === "new") m.status = "work"; lsSet("directMessages", list); } renderStats(document.getElementById("content")); return; } var res = await apiPost("/api/direct-messages/" + id + "/reply", { reply: reply }); if (res.ok) { await loadServerData(); renderStats(document.getElementById("content")); } else alert(res.error || "Қате"); } async function setDmStatus(id, status) { if (state.offline) { var list = lsGet("directMessages", []); var m = list.find(function (x) { return x.id === id; }); if (m) { m.status = status; lsSet("directMessages", list); } renderStats(document.getElementById("content")); return; } var res = await apiPost("/api/direct-messages/" + id + "/status", { status: status }); if (res.ok) { await loadServerData(); renderStats(document.getElementById("content")); } else alert(res.error || "Қате"); } 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 }; }); var dms = state.directMessages || []; var dmOpen = dms.filter(function (m) { return m.status !== "done"; }).length; return { users: state.offline ? MOCK_USERS.length : 0, complaintsTotal: complaints.length, complaintsOpen: byStatus.new + byStatus.work, testsTotal: results.length, dmTotal: dms.length, dmOpen: dmOpen, dmDone: dms.length - dmOpen, 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, """); }