v3: инсайдерлік ақпарат бөлімі+тест, сыйлықтар инфографика, комплаенс офицерге жеке хат

This commit is contained in:
Dauren777 2026-08-11 07:58:12 +00:00
parent 56d78709f2
commit 5a5963d084
4 changed files with 262 additions and 13 deletions

View File

@ -43,6 +43,27 @@
{ "id": 4, "cat": "Ақпарат", "title": "«Ақпаратқа қол жеткізу туралы» ҚР Заңы", "text": "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары." },
{ "id": 5, "cat": "Этика", "title": "Компанияның Іскерлік этика кодексі", "text": "Қызметкерлердің мінез-құлық стандарттары, қарым-қатынас ережелері." }
],
"insider": [
{ "title": "Инсайдерлік ақпарат дегеніміз не?", "text": "Бұл компания туралы әлі жарияланбаған, қызметкерге өз жұмысы барысында белгілі болған деректер: қаржылық көрсеткіштер, келісімшарттар, жаңалық сюжеттері, кадрлық шешімдер." },
{ "title": "Таратуға тыйым салынады", "text": "Инсайдерлік ақпаратты эфирге шыққанға дейін немесе ресми жарияланғанға дейін үшінші тұлғаларға, әлеуметтік желілерде немесе таныстарға таратуға тыйым салынады." },
{ "title": "Дереккөздерді құпия ұстау", "text": "Журналистер өз дереккөздерін құпия ұстауға міндетті. Дереккөздің атын ашу — еңбек тәртібін өрескел бұзу." },
{ "title": "Не істеу керек?", "text": "Инсайдерлік ақпаратқа қол жеткізгенде және оны пайдалану қажет болғанда — комплаенс офицермен кеңесіңіз. Ақпарат тек қызметтік қажеттілік үшін пайдаланылады." },
{ "title": "Жауапкершілік", "text": "Инсайдерлік ақпаратты тарату — тәртіптік жауапкершілікке әкеледі, ал ауыр жағдайларда ҚР заңнамасы бойынша қылмыстық жауапкершілікке де тартылуы мүмкін." }
],
"giftRules": {
"allowed": [
"Құны 3 АЕК-тен аспайтын еске салу сыйлығы (күнтізбе, қалам, кесе)",
"Ресми іс-шарада берілген естелік сыйлықтар",
"Мерекелік корпоративтік сыйлықтар (компания атынан)",
"Іскерлік келіссөздердегі қонақжайлылық (кешкі ас, кофе)"
],
"forbidden": [
"Ақша және оған балама (карта, купон, сертификат)",
"Құны 3 АЕК-тен асатын сыйлықтар",
"Қызметтік шешімге әсер ететін сыйлықтар (мүдделер қақтығысы)",
"Кез келген уақытта қолма-қол ақша немесе қарыз беру"
]
},
"tests": [
{
"id": 1, "title": "Сыбайлас жемқорлыққа қарсы негіздер", "pass": 70,
@ -63,8 +84,19 @@
{ "q": "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?", "options": ["Келісу", "Бас тартып, комплаенс қызметіне хабарлау", "Ойланып жауап беру", "Ешкімге айтпау"], "correct": 1 },
{ "q": "Тест нәтижелері мен оқу материалдары кімге қажет?", "options": ["Тек HR-ға", "Барлық қызметкерлерге", "Тек админге", "Тек басшыларға"], "correct": 1 }
]
},
{
"id": 3, "title": "Инсайдерлік ақпарат және құпиялылық", "pass": 70,
"questions": [
{ "q": "Инсайдерлік ақпарат дегеніміз не?", "options": ["Әріптестер туралы қауесет", "Компания туралы әлі жарияланбаған қызметтік деректер", "Әлеуметтік желідегі ақпарат", "Жалпыға белгілі жаңалықтар"], "correct": 1 },
{ "q": "Жаңа сюжет эфирге шыққанға дейін оны танысқа айтуға бола ма?", "options": ["Иә, егер ол жақын таныс болса", "Жоқ, инсайдерлік ақпаратты таратуға тыйым салынады", "Иә, бірақ тек SMS арқылы", "Болады, себебі бұл жай ғана жаңалық"], "correct": 1 },
{ "q": "Дереккөздің атын басшылыққа ашуға бола ма?", "options": ["Иә, міндетті түрде", "Жоқ, журналист дереккөзді құпия ұстайды", "Тек түскі үзілісте айтуға болады", "Тек жазбаша түрде"], "correct": 1 },
{ "q": "Инсайдерлік ақпаратты таратқаны үшін не болады?", "options": ["Ештеңе болмайды", "Тәртіптік жауапкершілік, ауыр жағдайда заң бойынша жауапкершілік", "Тек сөгіс беріледі", "Бонус өседі"], "correct": 1 },
{ "q": "Қызметтік ақпаратты қай жерде талқылауға БОЛМАЙДЫ?", "options": ["Кеңседе әріптестермен", "Көпшілік орындарда және әлеуметтік желілерде", "Ресми жиналыста", "Комплаенс офицермен кеңесте"], "correct": 1 }
]
}
],
"complaints": [],
"testResults": []
"testResults": [],
"directMessages": []
}

176
script.js
View File

@ -20,6 +20,7 @@ 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: "Шағым жәшігі" },
@ -95,7 +96,38 @@ var MOCK_CONTENT = {
{ 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 ҚАБАТЫ ---------------- */
@ -157,6 +189,7 @@ document.getElementById("loginForm").addEventListener("submit", async function (
state.content = MOCK_CONTENT;
state.complaints = lsGet("complaints", []);
state.testResults = lsGet("testResults", []);
state.directMessages = lsGet("directMessages", []);
enterApp();
return;
}
@ -175,6 +208,10 @@ async function loadServerData() {
if (cp.ok) state.complaints = cp.data;
var tr = await apiGet("/api/test-results");
if (tr.ok) state.testResults = tr.data;
if (state.user && state.user.role === "admin") {
var dm = await apiGet("/api/direct-messages");
if (dm.ok) state.directMessages = dm.data;
}
}
function enterApp() {
@ -215,6 +252,7 @@ function go(page) {
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);
@ -353,6 +391,21 @@ function renderLaws(el) {
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>"; });
@ -373,10 +426,31 @@ function filterLaws(cat) {
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="card" style="margin-bottom:20px"><h3>Жаңа шағым / өтініш</h3>';
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>';
@ -384,6 +458,15 @@ function renderComplaints(el) {
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>';
el.innerHTML = html;
@ -454,6 +537,32 @@ async function setComplaintStatus(id, status) {
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,
});
lsSet("directMessages", list);
document.getElementById("dmMsg").innerHTML = '<div class="msg ok">✅ Хат жіберілді (демо-режимде жергілікті сақталды)</div>';
document.getElementById("dmText").value = "";
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 = "";
} else document.getElementById("dmMsg").innerHTML = '<div class="msg err">' + esc(res.error || "Қате") + "</div>";
}
/* ---------------- ТЕСТТЕР ---------------- */
function renderTests(el) {
if (state.curTest !== null) { renderTestRun(el); return; }
@ -525,22 +634,21 @@ async function submitTest() {
var answered = Object.keys(state.curTestAnswers).length;
if (answered < total) { alert("Барлық сұрақтарға жауап беріңіз (" + answered + "/" + total + ")"); return; }
var score = 0;
t.questions.forEach(function (q, qi) { if (state.curTestAnswers[qi] === q.correct) score++; });
var pct = Math.round(score / total * 100);
var result = { testId: t.id, login: state.user.login, name: state.user.name, score: pct, date: new Date().toISOString().slice(0, 10) };
var answers = t.questions.map(function (q, qi) { return state.curTestAnswers[qi]; });
if (state.offline) {
state.testResults.push(result);
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, score, total);
showTestResult(pct, correct, total);
return;
}
var res = await apiPost("/api/test-results", { testId: t.id, score: pct });
var res = await apiPost("/api/test-results", { testId: t.id, answers: answers });
if (res.ok) {
await loadServerData();
showTestResult(pct, score, total);
showTestResult(res.score, res.correct, res.total);
} else alert(res.error || "Қате");
}
@ -576,6 +684,14 @@ async function renderStats(el) {
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>';
@ -584,6 +700,7 @@ async function renderStats(el) {
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;
@ -618,6 +735,40 @@ async function renderStats(el) {
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></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.status !== "done") {
dhtml += '<div class="row" style="margin-top:10px">';
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></div>';
}
dhtml += "</div>";
});
document.getElementById("statsDm").innerHTML = dhtml || '<div class="empty">Жеке хаттар жоқ</div>';
}
}
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() {
@ -636,11 +787,16 @@ function computeStats() {
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,

View File

@ -8,7 +8,7 @@ const DATA_FILE = path.join(__dirname, "data.json");
function load() {
try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); }
catch (e) { return { users: [], announcements: [], org: [], videos: [], laws: [], tests: [], complaints: [], testResults: [] }; }
catch (e) { return { users: [], announcements: [], org: [], contacts: [], insider: [], giftRules: {}, videos: [], laws: [], tests: [], complaints: [], testResults: [], directMessages: [] }; }
}
function save(d) {
fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2));
@ -66,6 +66,8 @@ http.createServer(async (req, res) => {
announcements: db.announcements,
org: db.org,
contacts: db.contacts,
insider: db.insider,
giftRules: db.giftRules,
videos: db.videos,
laws: db.laws,
tests: db.tests.map((t) => ({
@ -134,15 +136,58 @@ http.createServer(async (req, res) => {
const b = await body(req);
const test = db.tests.find((t) => t.id === Number(b.testId));
if (!test) return send(res, 400, { ok: false, error: "Тест табылмады" });
const answers = Array.isArray(b.answers) ? b.answers : [];
let correct = 0;
test.questions.forEach((q, i) => {
if (answers[i] === q.correct) correct++;
});
const total = test.questions.length;
const score = total ? Math.round((correct / total) * 100) : 0;
db.testResults.push({
id: Date.now(),
testId: test.id,
login: user.login,
name: user.name,
score: Number(b.score) || 0,
score,
date: new Date().toISOString().slice(0, 10),
});
save(db);
return send(res, 200, { ok: true, score, correct, total });
}
// ---------- КОМПЛАЕНС ОФИЦЕРГЕ ЖЕКЕ ХАТ ----------
if (req.method === "GET" && url === "/api/direct-messages") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
return send(res, 200, { ok: true, data: db.directMessages || [] });
}
if (req.method === "POST" && url === "/api/direct-messages") {
const b = await body(req);
if (!b.text) return send(res, 400, { ok: false, error: "Мәтінді толтырыңыз" });
const item = {
id: Date.now(),
text: String(b.text).slice(0, 3000),
category: b.category || "Жалпы",
status: "new",
date: new Date().toISOString().slice(0, 10),
authorLogin: b.anonymous ? null : user.login,
authorName: b.anonymous ? null : user.name,
anonymous: !!b.anonymous,
};
if (!db.directMessages) db.directMessages = [];
db.directMessages.unshift(item);
save(db);
return send(res, 200, { ok: true });
}
const dmMatch = url.match(/^\/api\/direct-messages\/(\d+)\/status$/);
if (req.method === "POST" && dmMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
const m = (db.directMessages || []).find((x) => x.id === Number(dmMatch[1]));
if (!m) return send(res, 404, { ok: false, error: "Табылмады" });
if (["new", "work", "done"].includes(b.status)) m.status = b.status;
save(db);
return send(res, 200, { ok: true });
}
@ -160,11 +205,16 @@ http.createServer(async (req, res) => {
const avg = rs.length ? Math.round(rs.reduce((s, r) => s + r.score, 0) / rs.length) : 0;
return { title: t.title, count: rs.length, avg };
});
const dms = db.directMessages || [];
const dmOpen = dms.filter((m) => m.status !== "done").length;
return send(res, 200, { ok: true, data: {
users: db.users.length,
complaintsTotal: db.complaints.length,
complaintsOpen: byStatus.new + byStatus.work,
testsTotal: db.testResults.length,
dmTotal: dms.length,
dmOpen,
dmDone: dms.length - dmOpen,
byStatus,
byCategory: Object.keys(byCategory).map((k) => ({ cat: k, count: byCategory[k] })),
byTest,

View File

@ -230,6 +230,17 @@ tr:last-child td{border-bottom:none}
.search{width:100%;max-width:360px;padding:10px 14px;border:1.5px solid #DFE4EA;border-radius:10px;font-size:14px;outline:none}
.search:focus{border-color:var(--accent)}
.gift-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:14px}
.gift-col{border-radius:12px;padding:16px 18px}
.gift-ok{background:#EAF7EF;border:1.5px solid #B7E3C8}
.gift-no{background:#FDECEC;border:1.5px solid #F5C2C2}
.gift-head{font-weight:800;font-size:15px;margin-bottom:10px}
.gift-ok .gift-head{color:#1B7A3D}
.gift-no .gift-head{color:var(--red)}
.gift-item{font-size:14px;padding:7px 0;border-bottom:1px dashed rgba(0,0,0,.08)}
.gift-item:last-child{border-bottom:none}
@media (max-width:700px){.gift-grid{grid-template-columns:1fr}}
@media (max-width:900px){
.grid-4{grid-template-columns:repeat(2,1fr)}
.grid-3{grid-template-columns:repeat(2,1fr)}