1098 lines
65 KiB
JavaScript
1098 lines
65 KiB
JavaScript
/* ============================================================
|
||
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("registerBox").classList.add("hidden");
|
||
});
|
||
|
||
document.getElementById("registerBtn").addEventListener("click", function () {
|
||
document.getElementById("registerBox").classList.toggle("hidden");
|
||
document.getElementById("recoverBox").classList.add("hidden");
|
||
});
|
||
|
||
document.getElementById("regGo").addEventListener("click", async function () {
|
||
var name = document.getElementById("regName").value.trim();
|
||
var phone = document.getElementById("regPhone").value.trim();
|
||
var login = document.getElementById("regLogin").value.trim();
|
||
var pass = document.getElementById("regPass").value.trim();
|
||
var msg = document.getElementById("regMsg");
|
||
if (!name || !login || !pass) { msg.textContent = "Аты-жөнін, логинді және құпия сөзді толтырыңыз"; return; }
|
||
var res = await apiPost("/api/register", { name: name, login: login, password: pass, phone: phone });
|
||
if (res.ok) {
|
||
state.user = res.user;
|
||
state.token = res.token;
|
||
await loadServerData();
|
||
enterApp();
|
||
return;
|
||
}
|
||
if (res.offline) {
|
||
if (MOCK_USERS.find(function (u) { return u.login === login; })) {
|
||
msg.textContent = "Мұндай логин бар (демо-режим)";
|
||
return;
|
||
}
|
||
MOCK_USERS.push({ login: login, password: pass, name: name, phone: phone, role: "user" });
|
||
state.user = { login: login, name: name, role: "user" };
|
||
state.offline = true;
|
||
state.content = MOCK_CONTENT;
|
||
state.complaints = lsGet("complaints", []);
|
||
state.testResults = lsGet("testResults", []);
|
||
state.directMessages = lsGet("directMessages", []);
|
||
enterApp();
|
||
return;
|
||
}
|
||
msg.textContent = res.error || "Қате";
|
||
});
|
||
|
||
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 = "<b>" + esc(res.name) + "</b>, жаңа құпия сөзіңіз: <b>" + esc(res.password) + "</b>";
|
||
} 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 = "<b>" + esc(mu.name) + "</b>, жаңа құпия сөзіңіз: <b>" + np + "</b> (демо-режим)";
|
||
} 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 = '<span class="ico">' + item.ico + "</span><span>" + item.title + "</span>";
|
||
if (state.user.role === "admin" && (item.id === "complaints" || item.id === "stats") && newCount > 0) {
|
||
b.innerHTML += '<span class="nav-badge">' + newCount + "</span>";
|
||
}
|
||
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 '<div class="section-title">' + text + "</div>";
|
||
}
|
||
|
||
/* ---------------- БАСТЫ БЕТ ---------------- */
|
||
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 = '<div class="grid grid-2">';
|
||
html += '<div class="card"><h3>👋 Сәлем, ' + esc(state.user.name) + "!</h3><p>Комплаенс порталына қош келдіңіз. Мұнда заңдар, видео сабақтар, тесттер және шағым жәшігі бар.</p></div>";
|
||
html += '<div class="card stat-card"><div class="stat-num">' + myResults.length + "</div><div class=\"stat-label\">Сіз тапсырған тест</div></div>";
|
||
html += '<div class="card stat-card orange"><div class="stat-num">' + myComplaints.length + "</div><div class=\"stat-label\">Сіздің шағымдарыңыз</div></div>";
|
||
html += '<div class="card stat-card green"><div class="stat-num">' + (state.content ? state.content.videos.length : 0) + "</div><div class=\"stat-label\">Видео сабақ</div></div>";
|
||
html += "</div>";
|
||
|
||
if (state.user.role === "admin") {
|
||
var newCount = newItemsCount();
|
||
if (newCount > 0) {
|
||
html += '<div class="card" style="margin-top:20px;border:2px solid var(--red);background:var(--red-50)">';
|
||
html += '<h3>🔔 Жаңа хабарламалар: <b>' + newCount + "</b></h3>";
|
||
html += '<p style="margin:8px 0">Шағым жәшігіне және жеке хаттарға жаңа өтініштер келді. Жауап беру үшін «Шағым жәшігі» немесе «Дашборд» бөліміне өтіңіз.</p>';
|
||
html += '<button class="btn btn-primary btn-sm" onclick="go(\'complaints\')">Ашып қарау</button>';
|
||
html += "</div>";
|
||
}
|
||
}
|
||
|
||
html += '<div class="section-title" style="margin-top:28px">📢 Хабарландырулар</div>'; if (state.content && state.content.announcements.length) {
|
||
state.content.announcements.forEach(function (a) {
|
||
html += '<div class="announce"><h3>' + esc(a.title) + '</h3><div class="date">📅 ' + a.date + '</div><p>' + esc(a.text) + "</p></div>";
|
||
});
|
||
} else {
|
||
html += '<div class="empty">Хабарландыру жоқ</div>';
|
||
}
|
||
|
||
if (state.user.role === "admin") {
|
||
html += '<div class="card" style="margin-top:24px"><h3>➕ Жаңа хабарландыру</h3>';
|
||
html += '<div class="form"><div class="form-row">';
|
||
html += '<div><label>Тақырып</label><input id="annTitle" placeholder="Тақырыбы"></div>';
|
||
html += '<div><label>Күні</label><input id="annDate" type="date"></div>';
|
||
html += '</div><label>Мәтін</label><textarea id="annText" rows="3" placeholder="Хабарландыру мәтіні"></textarea>';
|
||
html += '<button class="btn btn-primary" style="margin-top:14px" onclick="addAnnouncement()">Жариялау</button></div></div>';
|
||
}
|
||
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 += '<div class="org-level">';
|
||
levels[lv].forEach(function (o) {
|
||
html += '<div class="org-card' + (o.level === 1 ? " top" : "") + '">';
|
||
html += '<div class="org-name">' + esc(o.name) + '</div><div class="org-pos">' + esc(o.person) + "</div>";
|
||
if (o.phone || o.email) {
|
||
html += '<div class="org-contacts">';
|
||
if (o.phone) html += '<div class="org-c">📞 <a href="tel:' + esc(o.phone.replace(/\s|\(|\)|-/g, "")) + '">' + esc(o.phone) + "</a></div>";
|
||
if (o.email) html += '<div class="org-c">✉️ <a href="mailto:' + esc(o.email) + '">' + esc(o.email) + "</a></div>";
|
||
html += "</div>";
|
||
}
|
||
html += "</div>";
|
||
});
|
||
html += "</div>";
|
||
html += '<div class="org-arrow">▼</div>';
|
||
});
|
||
html = html.replace(/<div class="org-arrow">▼<\/div>\s*$/, "");
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
/* ---------------- МЕМЛЕКЕТТІК ОРГАНДАР ---------------- */
|
||
function renderContacts(el) {
|
||
var list = state.content && state.content.contacts ? state.content.contacts : [];
|
||
var html = pageTitle("📞 Мемлекеттік органдар — Алматы қаласы");
|
||
html += '<p class="muted" style="margin-bottom:16px">Комплаенс мәселелері бойынша хабарласуға болатын мемлекеттік органдардың ресми байланыс деректері.</p>';
|
||
html += '<div class="grid grid-2">';
|
||
list.forEach(function (c) {
|
||
html += '<div class="card"><div class="spread"><h3>' + esc(c.org) + '</h3><span class="cat-tag">' + esc(c.type) + "</span></div>";
|
||
html += '<div style="margin-top:12px">';
|
||
if (c.phone) html += '<div class="org-c">📞 <a href="tel:' + esc(c.phone.replace(/\s|\(|\)|-/g, "")) + '"><b>' + esc(c.phone) + "</b></a></div>";
|
||
if (c.email) html += '<div class="org-c">✉️ <a href="mailto:' + esc(c.email) + '">' + esc(c.email) + "</a></div>";
|
||
if (c.note) html += '<div class="org-c muted">📍 ' + esc(c.note) + "</div>";
|
||
html += "</div></div>";
|
||
});
|
||
html += "</div>";
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
/* ---------------- ВИДЕО ---------------- */
|
||
function renderVideo(el) {
|
||
var vids = state.content ? state.content.videos : [];
|
||
var html = pageTitle("🎬 Видео сабақтар");
|
||
html += '<div class="grid grid-2">';
|
||
vids.forEach(function (v) {
|
||
html += '<div class="card video-card" onclick="openVideo(' + v.id + ')">';
|
||
html += '<div class="video-thumb" style="background-image:url(\'' + v.thumb + '\')">▶</div>';
|
||
html += "<h3>" + esc(v.title) + '</h3><p>' + esc(v.desc) + '</p><div class="muted" style="margin-top:8px">⏱ ' + v.dur + "</div>";
|
||
html += "</div>";
|
||
});
|
||
html += "</div>";
|
||
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 += '<div class="card" style="margin-bottom:24px;border:1.5px solid #DFE4EA">';
|
||
html += '<h3 style="font-size:19px">🎁 Сыйлықтар ережесі</h3>';
|
||
html += '<div class="gift-grid">';
|
||
html += '<div class="gift-col gift-ok"><div class="gift-head">✅ Рұқсат етілген</div>';
|
||
gr.allowed.forEach(function (t) { html += '<div class="gift-item">' + esc(t) + "</div>"; });
|
||
html += "</div>";
|
||
html += '<div class="gift-col gift-no"><div class="gift-head">🚫 Тыйым салынған</div>';
|
||
gr.forbidden.forEach(function (t) { html += '<div class="gift-item">' + esc(t) + "</div>"; });
|
||
html += "</div>";
|
||
html += "</div></div>";
|
||
}
|
||
|
||
html += '<div class="tabs">';
|
||
html += '<button class="tab active" data-cat="all" onclick="filterLaws(\'all\')">Барлығы</button>';
|
||
cats.forEach(function (c) { html += '<button class="tab" data-cat="' + esc(c) + '" onclick="filterLaws(\'' + esc(c) + '\')">' + esc(c) + "</button>"; });
|
||
html += "</div>";
|
||
html += '<div id="lawList"></div>';
|
||
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 += '<div class="card" style="margin-bottom:12px"><div class="spread"><h3>' + esc(l.title) + '</h3><span class="cat-tag">' + esc(l.cat) + "</span></div><p>" + esc(l.text) + "</p></div>";
|
||
});
|
||
document.getElementById("lawList").innerHTML = html || '<div class="empty">Табылмады</div>';
|
||
}
|
||
|
||
/* ---------------- ИНСАЙДЕРЛІК АҚПАРАТ ---------------- */
|
||
function renderInsider(el) {
|
||
var list = state.content && state.content.insider ? state.content.insider : [];
|
||
var html = pageTitle("🔐 Инсайдерлік ақпарат және құпиялылық");
|
||
html += '<p class="muted" style="margin-bottom:16px">Медиа саласында инсайдерлік ақпаратпен жұмыс — ең маңызды талаптардың бірі. Төмендегі ережелерді оқып, тест тапсыру ұсынылады.</p>';
|
||
list.forEach(function (item, i) {
|
||
html += '<div class="card" style="margin-bottom:12px;border-left:4px solid var(--accent)">';
|
||
html += '<h3>' + (i + 1) + ". " + esc(item.title) + "</h3>";
|
||
html += "<p>" + esc(item.text) + "</p>";
|
||
html += "</div>";
|
||
});
|
||
html += '<div class="card" style="margin-top:20px;background:var(--cyan-50);border:1.5px solid var(--cyan)">';
|
||
html += '<h3>📝 Материалды оқыдыңыз ба?</h3>';
|
||
html += '<p style="margin:8px 0">«Инсайдерлік ақпарат және құпиялылық» тестін тапсырып, біліміңізді тексеріңіз.</p>';
|
||
html += '<button class="btn btn-cyan" onclick="startTest(3)">Тестке өту</button>';
|
||
html += "</div>";
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
/* ---------------- ШАҒЫМ ЖӘШІГІ ---------------- */
|
||
function renderComplaints(el) {
|
||
var html = pageTitle("✉️ Шағым жәшігі");
|
||
|
||
html += '<div class="grid grid-2" style="margin-bottom:20px">';
|
||
html += '<div class="card"><h3>Жаңа шағым / өтініш</h3>';
|
||
html += '<div class="form">';
|
||
html += '<label>Санаты</label><select id="compCat"><option>Жемқорлық фактісі</option><option>Мүдделер қақтығысы</option><option>Жұмыс ортасы</option><option>Жалақы/еңбек құқығы</option><option>Басқа</option></select>';
|
||
html += '<label>Мәтін</label><textarea id="compText" rows="4" placeholder="Шағымыңызды жазыңыз..."></textarea>';
|
||
html += '<div class="row" style="margin-top:6px"><label style="display:flex;align-items:center;gap:8px;font-weight:500"><input type="checkbox" id="compAnon"> Анонимді жіберу (аты-жөніңіз көрсетілмейді)</label></div>';
|
||
html += '<button class="btn btn-primary" style="margin-top:12px" onclick="sendComplaint()">Жіберу</button>';
|
||
html += '<div id="compMsg"></div></div></div>';
|
||
|
||
html += '<div class="card"><h3>🔒 Комплаенс офицерге жеке хат</h3>';
|
||
html += '<div class="form">';
|
||
html += '<label>Тақырыбы</label><select id="dmCat"><option>Консультация</option><option>Мүдделер қақтығысы</option><option>Инсайдерлік ақпарат мәселесі</option><option>Сыйлық/қонақжайлылық</option><option>Басқа</option></select>';
|
||
html += '<label>Хат мәтіні</label><textarea id="dmText" rows="4" placeholder="Комплаенс офицерге сұрағыңызды жазыңыз..."></textarea>';
|
||
html += '<div class="row" style="margin-top:6px"><label style="display:flex;align-items:center;gap:8px;font-weight:500"><input type="checkbox" id="dmAnon"> Анонимді түрде</label></div>';
|
||
html += '<button class="btn btn-cyan" style="margin-top:12px" onclick="sendDirectMsg()">Жіберу</button>';
|
||
html += '<div id="dmMsg"></div></div></div>';
|
||
html += "</div>";
|
||
|
||
html += '<div class="section-title">Менің шағымдарым' + (state.user.role === "admin" ? " және барлық өтініштер" : "") + "</div>";
|
||
html += '<div id="compList"></div>';
|
||
|
||
html += '<div class="section-title" style="margin-top:24px">Менің жеке хаттарым (комплаенс офицерге)</div>';
|
||
html += '<div id="dmList"></div>';
|
||
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 += '<div class="card complaint-item ' + m.status + '" style="margin-bottom:12px">';
|
||
html += '<div class="spread"><span class="cat-tag">' + esc(m.category) + "</span>";
|
||
html += '<span>' + (m.anonymous ? "Аноним" : esc(m.authorName)) + ' · <span class="badge ' + m.status + '">' + st + "</span></span></div>";
|
||
html += "<p style=\"margin-top:8px\">" + esc(m.text) + "</p>";
|
||
if (m.reply) {
|
||
html += '<div class="reply-box"><b>📨 Комплаенс офицердің жауабы (' + (m.replyDate || "") + '):</b><p>' + esc(m.reply) + "</p></div>";
|
||
}
|
||
if (state.user.role === "admin") {
|
||
html += '<div class="row" style="margin-top:10px;gap:8px;align-items:flex-start;flex-wrap:wrap">';
|
||
html += '<input id="replyDm_' + m.id + '" class="reply-input" placeholder="Жауап жазыңыз және «Жіберу» басыңыз...">';
|
||
html += '<button class="btn btn-sm btn-cyan" onclick="replyDm(' + m.id + ')">Жіберу</button>';
|
||
if (m.status !== "done") {
|
||
if (m.status !== "work") html += '<button class="btn btn-sm btn-ghost" onclick="setDmStatus(' + m.id + ',\'work\')">Қаралуда</button>';
|
||
html += '<button class="btn btn-sm btn-primary" onclick="setDmStatus(' + m.id + ',\'done\')">Шешілді</button>';
|
||
}
|
||
html += "</div>";
|
||
}
|
||
html += "</div>";
|
||
});
|
||
document.getElementById("dmList").innerHTML = html || '<div class="empty">Хаттар жоқ — комплаенс офицерге жаза аласыз</div>';
|
||
}
|
||
|
||
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 += '<div class="card complaint-item ' + c.status + '" style="margin-bottom:12px">';
|
||
html += '<div class="spread"><div class="row"><span class="cat-tag">' + esc(c.category) + "</span>";
|
||
html += '<span class="badge ' + c.status + '">' + st + "</span></div>";
|
||
html += '<span class="muted">' + (c.anonymous ? "Аноним" : esc(c.authorName)) + " · " + c.date + "</span></div>";
|
||
html += "<p style=\"margin-top:10px\">" + esc(c.text) + "</p>";
|
||
|
||
if (c.reply) {
|
||
html += '<div class="reply-box"><b>📨 Жауап (' + c.replyDate + '):</b><p>' + esc(c.reply) + "</p></div>";
|
||
}
|
||
|
||
if (state.user.role === "admin") {
|
||
html += '<div class="row" style="margin-top:12px;gap:8px;align-items:flex-start;flex-wrap:wrap">';
|
||
html += '<input id="replyComp_' + c.id + '" class="reply-input" placeholder="Жауап жазыңыз және «Жіберу» басыңыз...">';
|
||
html += '<button class="btn btn-sm btn-cyan" onclick="replyComplaint(' + c.id + ')">Жіберу</button>';
|
||
if (c.status !== "done") {
|
||
if (c.status !== "work") html += '<button class="btn btn-sm btn-ghost" onclick="setComplaintStatus(' + c.id + ',\'work\')">Қаралуда</button>';
|
||
html += '<button class="btn btn-sm btn-primary" onclick="setComplaintStatus(' + c.id + ',\'done\')">Шешілді</button>';
|
||
}
|
||
html += "</div>";
|
||
}
|
||
html += "</div>";
|
||
});
|
||
document.getElementById("compList").innerHTML = html || '<div class="empty">Әзірге шағым жоқ</div>';
|
||
}
|
||
|
||
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 = '<div class="msg err">Мәтінді толтырыңыз</div>'; 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 = '<div class="msg ok">✅ Шағымыңыз жіберілді (демо-режимде жергілікті сақталды)</div>';
|
||
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 = '<div class="msg ok">✅ Шағымыңыз қабылданды. Рахмет!</div>';
|
||
document.getElementById("compText").value = "";
|
||
await loadServerData();
|
||
renderComplaintList();
|
||
} else document.getElementById("compMsg").innerHTML = '<div class="msg err">' + esc(res.error || "Қате") + "</div>";
|
||
}
|
||
|
||
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 = '<div class="msg err">Хат мәтінін жазыңыз</div>'; 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 = '<div class="msg ok">✅ Хат жіберілді (демо-режимде жергілікті сақталды)</div>';
|
||
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 = '<div class="msg ok">✅ Хатыңыз комплаенс офицерге жіберілді</div>';
|
||
document.getElementById("dmText").value = "";
|
||
await loadServerData();
|
||
renderMyDmList();
|
||
} else document.getElementById("dmMsg").innerHTML = '<div class="msg err">' + esc(res.error || "Қате") + "</div>";
|
||
}
|
||
|
||
/* ---------------- ҚЫЗМЕТКЕРЛЕР (АККАУНТТАР, админ) ---------------- */
|
||
async function renderUsers(el) {
|
||
var html = pageTitle("👥 Қызметкерлер және аккаунттар");
|
||
html += '<p class="muted" style="margin-bottom:16px">Логиндер мен құпия сөздер <code>users.json</code> файлында сақталады. Мұнда жаңа қызметкер қосуға, құпия сөз ауыстыруға және аккаунтты өшіруге болады.</p>';
|
||
|
||
html += '<div class="card" style="margin-bottom:20px"><h3>➕ Жаңа қызметкер қосу</h3><div class="form"><div class="grid grid-2">';
|
||
html += '<div><label>Логин (жеке нөмір)</label><input id="uLogin" placeholder="Мысалы: 2004"></div>';
|
||
html += '<div><label>Аты-жөні</label><input id="uName" placeholder="Аты-жөні"></div>';
|
||
html += '</div><div class="grid grid-2">';
|
||
html += '<div><label>Телефон нөмірі</label><input id="uPhone" placeholder="+7 7XX XXX XX XX"></div>';
|
||
html += '<div><label>Құпия сөз</label><input id="uPass" placeholder="Ағылшын әріптерімен"></div>';
|
||
html += '</div><div class="grid grid-2">';
|
||
html += '<div><label>Рөлі</label><select id="uRole"><option value="user">Қызметкер</option><option value="admin">Админ (комплаенс)</option></select></div>';
|
||
html += '</div>';
|
||
html += '<button class="btn btn-primary" style="margin-top:14px" onclick="addUser()">Қосу</button>';
|
||
html += '<div id="uMsg"></div></div></div>';
|
||
|
||
html += '<div id="usersList"></div>';
|
||
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 = '<div class="card"><table><thead><tr><th>Логин</th><th>Аты-жөні</th><th>Телефон</th><th>Рөл</th><th>Құпия сөз</th><th></th></tr></thead><tbody>';
|
||
users.forEach(function (u) {
|
||
html += "<tr><td><b>" + esc(u.login) + "</b></td><td>" + esc(u.name) + "</td>";
|
||
html += "<td>" + esc(u.phone || "") + "</td>";
|
||
html += '<td>' + (u.role === "admin" ? "Админ" : "Қызметкер") + "</td>";
|
||
html += '<td><span class="pass-row"><span class="pass-text">' + esc(u.password) + '</span>';
|
||
html += '<button class="btn btn-sm btn-ghost" onclick="resetUserPass(\'' + esc(u.login) + '\')">Ауыстыру</button></span></td>';
|
||
html += '<td>' + (u.role !== "admin" ? '<button class="btn btn-sm btn-danger" onclick="delUser(\'' + esc(u.login) + '\')">Өшіру</button>' : "") + "</td></tr>";
|
||
});
|
||
html += "</tbody></table></div>";
|
||
document.getElementById("usersList").innerHTML = html;
|
||
}
|
||
|
||
async function addUser() {
|
||
var login = document.getElementById("uLogin").value.trim();
|
||
var name = document.getElementById("uName").value.trim();
|
||
var phone = document.getElementById("uPhone").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 = '<div class="msg err">Барлық өрістерді толтырыңыз</div>'; return; }
|
||
if (state.offline) {
|
||
MOCK_USERS.push({ login: login, name: name, phone: phone, password: pass, role: role });
|
||
msg.innerHTML = '<div class="msg ok">✅ Қосылды (демо)</div>';
|
||
loadUsersList();
|
||
return;
|
||
}
|
||
var res = await apiPost("/api/users", { login: login, name: name, phone: phone, password: pass, role: role });
|
||
if (res.ok) {
|
||
msg.innerHTML = '<div class="msg ok">✅ Қызметкер қосылды</div>';
|
||
document.getElementById("uLogin").value = "";
|
||
document.getElementById("uName").value = "";
|
||
document.getElementById("uPhone").value = "";
|
||
document.getElementById("uPass").value = "";
|
||
loadUsersList();
|
||
} else msg.innerHTML = '<div class="msg err">' + esc(res.error || "Қате") + "</div>";
|
||
}
|
||
|
||
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 += '<div class="grid grid-2">';
|
||
tests.forEach(function (t) {
|
||
var done = state.testResults.filter(function (r) { return r.testId === t.id; });
|
||
html += '<div class="card"><h3>' + esc(t.title) + "</h3>";
|
||
html += "<p>Сұрақтар саны: " + t.questions.length + " · өту үшін: " + t.pass + "%</p>";
|
||
html += '<div class="muted" style="margin:8px 0">Әрекеттер: ' + done.length;
|
||
if (done.length) {
|
||
var last = done[done.length - 1];
|
||
html += " · соңғысы: <b>" + last.score + "%</b>";
|
||
}
|
||
html += "</div>";
|
||
html += '<button class="btn btn-primary btn-sm" onclick="startTest(' + t.id + ')">Бастау</button></div>';
|
||
});
|
||
html += "</div>";
|
||
html += '<div class="section-title" style="margin-top:28px">Менің нәтижелерім</div>';
|
||
html += '<div id="myTestResults"></div>';
|
||
el.innerHTML = html;
|
||
|
||
var mine = state.testResults.filter(function (r) { return r.login === state.user.login; }).slice().reverse();
|
||
var th = "";
|
||
if (mine.length) {
|
||
th = "<table><thead><tr><th>Тест</th><th>Нәтиже</th><th>Күні</th></tr></thead><tbody>";
|
||
mine.forEach(function (r) {
|
||
var tn = tests.find(function (t) { return t.id === r.testId; });
|
||
th += "<tr><td>" + esc(tn ? tn.title : "Тест") + "</td><td><span class=\"badge " + (r.score >= (tn ? tn.pass : 70) ? "done" : "new") + "\">" + r.score + "%</span></td><td>" + r.date + "</td></tr>";
|
||
});
|
||
th += "</tbody></table>";
|
||
}
|
||
document.getElementById("myTestResults").innerHTML = th || '<div class="empty">Әзірге тест тапсырған жоқсыз</div>';
|
||
}
|
||
|
||
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 = '<button class="btn btn-ghost btn-sm" onclick="state.curTest=null;renderTests(document.getElementById(\'content\'))">← Артқа</button>';
|
||
html += '<div class="section-title" style="margin-top:14px">' + esc(t.title) + "</div>";
|
||
t.questions.forEach(function (q, qi) {
|
||
html += '<div class="card" style="margin-bottom:14px"><div class="test-question"><h4>' + (qi + 1) + ". " + esc(q.q) + "</h4>";
|
||
q.options.forEach(function (op, oi) {
|
||
html += '<label class="q-option' + (state.curTestAnswers[qi] === oi ? " selected" : "") + '" data-q="' + qi + '" data-o="' + oi + '" onclick="pickAnswer(this,' + qi + ',' + oi + ')">' + esc(op) + "</label>";
|
||
});
|
||
html += "</div></div>";
|
||
});
|
||
html += '<button class="btn btn-primary btn-block" onclick="submitTest()">Нәтижені жіберу</button>';
|
||
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 = '<div class="card" style="max-width:520px;margin:40px auto;text-align:center">';
|
||
html += '<div class="test-score">' + pct + "%</div>";
|
||
html += "<p>Дұрыс жауаптар: " + score + " / " + total + "</p>";
|
||
html += '<div class="' + (pct >= 70 ? "msg ok" : "msg err") + '" style="text-align:center">' + (pct >= 70 ? "✅ Тест сәтті өтілді!" : "Тест өтілмеді, қайта тапсыруға болады") + "</div>";
|
||
html += '<button class="btn btn-primary" onclick="renderTests(document.getElementById(\'content\'))">Тесттерге оралу</button>';
|
||
html += "</div>";
|
||
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 = '<div class="empty">Деректер жоқ</div>'; return; }
|
||
|
||
html += '<div class="grid grid-4">';
|
||
html += '<div class="card stat-card"><div class="stat-num">' + data.users + "</div><div class=\"stat-label\">Қызметкерлер</div></div>";
|
||
html += '<div class="card stat-card red"><div class="stat-num">' + data.complaintsTotal + "</div><div class=\"stat-label\">Шағымдар (барлығы)</div></div>";
|
||
html += '<div class="card stat-card orange"><div class="stat-num">' + data.complaintsOpen + "</div><div class=\"stat-label\">Ашық шағымдар</div></div>";
|
||
html += '<div class="card stat-card green"><div class="stat-num">' + data.testsTotal + "</div><div class=\"stat-label\">Тест тапсырулар</div></div>";
|
||
html += "</div>";
|
||
|
||
if (state.user.role === "admin") {
|
||
html += '<div class="grid grid-3" style="margin-top:16px">';
|
||
html += '<div class="card stat-card"><div class="stat-num">' + (data.dmTotal || 0) + "</div><div class=\"stat-label\">Жеке хаттар (барлығы)</div></div>";
|
||
html += '<div class="card stat-card orange"><div class="stat-num">' + (data.dmOpen || 0) + "</div><div class=\"stat-label\">Жауап күтіп тұрған хаттар</div></div>";
|
||
html += '<div class="card stat-card green"><div class="stat-num">' + (data.dmDone || 0) + "</div><div class=\"stat-label\">Шешілген хаттар</div></div>";
|
||
html += "</div>";
|
||
}
|
||
|
||
html += '<div class="grid grid-2" style="margin-top:20px">';
|
||
html += '<div class="card"><h3>Шағымдар санаты бойынша</h3><div id="catBars"></div></div>';
|
||
html += '<div class="card"><h3>Тест өту көрсеткіші</h3><div id="passBars"></div></div>';
|
||
html += "</div>";
|
||
|
||
html += '<div class="card" style="margin-top:20px"><h3>Тест нәтижелері бойынша орташа балл</h3><div id="avgBars"></div></div>';
|
||
|
||
html += '<div class="section-title" style="margin-top:28px">Соңғы шағымдар</div><div id="statsComplaints"></div>';
|
||
html += '<div class="section-title" style="margin-top:28px">🔒 Комплаенс офицерге жеке хаттар</div><div id="statsDm"></div>';
|
||
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 += '<div class="bar-row"><div class="bar-label">' + esc(c.cat) + '</div><div class="bar-track"><div class="bar-fill" style="width:' + w + '%">' + c.count + "</div></div></div>";
|
||
});
|
||
document.getElementById("catBars").innerHTML = catHtml || '<div class="empty">Жоқ</div>';
|
||
|
||
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 += '<div class="bar-row"><div class="bar-label">' + name + '</div><div class="bar-track"><div class="bar-fill ' + color + '" style="width:' + w + '%">' + c + "</div></div></div>";
|
||
});
|
||
document.getElementById("passBars").innerHTML = passHtml;
|
||
|
||
var avgHtml = "";
|
||
data.byTest.forEach(function (t) {
|
||
avgHtml += '<div class="bar-row"><div class="bar-label">' + esc(t.title) + '</div><div class="bar-track"><div class="bar-fill" style="width:' + t.avg + '%">' + t.avg + "%</div></div></div>";
|
||
});
|
||
document.getElementById("avgBars").innerHTML = avgHtml || '<div class="empty">Тест әлі жоқ</div>';
|
||
|
||
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 += '<div class="card complaint-item ' + c.status + '" style="margin-bottom:10px"><div class="spread"><span class="cat-tag">' + esc(c.category) + "</span><span>" + (c.anonymous ? "Аноним" : esc(c.authorName)) + ' · <span class="badge ' + c.status + '">' + st + "</span></span></div><p style=\"margin-top:8px\">" + esc(c.text) + "</p>";
|
||
if (c.reply) {
|
||
chtml += '<div class="reply-box"><b>📨 Жауап:</b><p>' + esc(c.reply) + "</p></div>";
|
||
}
|
||
chtml += "</div>";
|
||
});
|
||
document.getElementById("statsComplaints").innerHTML = chtml || '<div class="empty">Шағым жоқ</div>';
|
||
|
||
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 += '<div class="card complaint-item ' + m.status + '" style="margin-bottom:10px">';
|
||
dhtml += '<div class="spread"><span class="cat-tag">' + esc(m.category) + "</span><span>" + (m.anonymous ? "Аноним" : esc(m.authorName)) + ' · <span class="badge ' + m.status + '">' + st + "</span></span></div>";
|
||
dhtml += '<p style="margin-top:8px">' + esc(m.text) + "</p>";
|
||
if (m.reply) {
|
||
dhtml += '<div class="reply-box"><b>📨 Жауап:</b><p>' + esc(m.reply) + "</p></div>";
|
||
}
|
||
dhtml += '<div class="row" style="margin-top:10px;gap:8px;align-items:flex-start;flex-wrap:wrap">';
|
||
dhtml += '<input id="replyDm_' + m.id + '" class="reply-input" placeholder="Жауап жазыңыз және «Жіберу» басыңыз...">';
|
||
dhtml += '<button class="btn btn-sm btn-cyan" onclick="replyDm(' + m.id + ')">Жіберу</button>';
|
||
if (m.status !== "done") {
|
||
if (m.status !== "work") dhtml += '<button class="btn btn-sm btn-ghost" onclick="setDmStatus(' + m.id + ',\'work\')">Қаралуда</button>';
|
||
dhtml += '<button class="btn btn-sm btn-primary" onclick="setDmStatus(' + m.id + ',\'done\')">Шешілді</button>';
|
||
}
|
||
dhtml += "</div></div>";
|
||
});
|
||
document.getElementById("statsDm").innerHTML = dhtml || '<div class="empty">Жеке хаттар жоқ</div>';
|
||
}
|
||
}
|
||
|
||
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, ">").replace(/"/g, """);
|
||
}
|