v3: инсайдерлік ақпарат бөлімі+тест, сыйлықтар инфографика, комплаенс офицерге жеке хат
This commit is contained in:
parent
56d78709f2
commit
5a5963d084
34
data.json
34
data.json
@ -43,6 +43,27 @@
|
|||||||
{ "id": 4, "cat": "Ақпарат", "title": "«Ақпаратқа қол жеткізу туралы» ҚР Заңы", "text": "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары." },
|
{ "id": 4, "cat": "Ақпарат", "title": "«Ақпаратқа қол жеткізу туралы» ҚР Заңы", "text": "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары." },
|
||||||
{ "id": 5, "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": [
|
"tests": [
|
||||||
{
|
{
|
||||||
"id": 1, "title": "Сыбайлас жемқорлыққа қарсы негіздер", "pass": 70,
|
"id": 1, "title": "Сыбайлас жемқорлыққа қарсы негіздер", "pass": 70,
|
||||||
@ -63,8 +84,19 @@
|
|||||||
{ "q": "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?", "options": ["Келісу", "Бас тартып, комплаенс қызметіне хабарлау", "Ойланып жауап беру", "Ешкімге айтпау"], "correct": 1 },
|
{ "q": "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?", "options": ["Келісу", "Бас тартып, комплаенс қызметіне хабарлау", "Ойланып жауап беру", "Ешкімге айтпау"], "correct": 1 },
|
||||||
{ "q": "Тест нәтижелері мен оқу материалдары кімге қажет?", "options": ["Тек HR-ға", "Барлық қызметкерлерге", "Тек админге", "Тек басшыларға"], "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": [],
|
"complaints": [],
|
||||||
"testResults": []
|
"testResults": [],
|
||||||
|
"directMessages": []
|
||||||
}
|
}
|
||||||
|
|||||||
176
script.js
176
script.js
@ -20,6 +20,7 @@ var NAV = [
|
|||||||
{ id: "home", ico: "🏠", title: "Басты бет" },
|
{ id: "home", ico: "🏠", title: "Басты бет" },
|
||||||
{ id: "org", ico: "🏛️", title: "Басшылық құрылымы" },
|
{ id: "org", ico: "🏛️", title: "Басшылық құрылымы" },
|
||||||
{ id: "contacts", ico: "📞", title: "Мемлекеттік органдар" },
|
{ id: "contacts", ico: "📞", title: "Мемлекеттік органдар" },
|
||||||
|
{ id: "insider", ico: "🔐", title: "Инсайдерлік ақпарат" },
|
||||||
{ id: "video", ico: "🎬", title: "Видео сабақтар" },
|
{ id: "video", ico: "🎬", title: "Видео сабақтар" },
|
||||||
{ id: "laws", ico: "⚖️", title: "Заң нормалары" },
|
{ id: "laws", ico: "⚖️", title: "Заң нормалары" },
|
||||||
{ id: "complaints", ico: "✉️", title: "Шағым жәшігі" },
|
{ id: "complaints", ico: "✉️", title: "Шағым жәшігі" },
|
||||||
@ -95,7 +96,38 @@ var MOCK_CONTENT = {
|
|||||||
{ q: "Тест нәтижелері мен оқу материалдары кімге қажет?", options: ["Тек HR-ға", "Барлық қызметкерлерге", "Тек админге", "Тек басшыларға"], 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 ҚАБАТЫ ---------------- */
|
/* ---------------- API ҚАБАТЫ ---------------- */
|
||||||
@ -157,6 +189,7 @@ document.getElementById("loginForm").addEventListener("submit", async function (
|
|||||||
state.content = MOCK_CONTENT;
|
state.content = MOCK_CONTENT;
|
||||||
state.complaints = lsGet("complaints", []);
|
state.complaints = lsGet("complaints", []);
|
||||||
state.testResults = lsGet("testResults", []);
|
state.testResults = lsGet("testResults", []);
|
||||||
|
state.directMessages = lsGet("directMessages", []);
|
||||||
enterApp();
|
enterApp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,6 +208,10 @@ async function loadServerData() {
|
|||||||
if (cp.ok) state.complaints = cp.data;
|
if (cp.ok) state.complaints = cp.data;
|
||||||
var tr = await apiGet("/api/test-results");
|
var tr = await apiGet("/api/test-results");
|
||||||
if (tr.ok) state.testResults = tr.data;
|
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() {
|
function enterApp() {
|
||||||
@ -215,6 +252,7 @@ function go(page) {
|
|||||||
if (page === "home") renderHome(c);
|
if (page === "home") renderHome(c);
|
||||||
else if (page === "org") renderOrg(c);
|
else if (page === "org") renderOrg(c);
|
||||||
else if (page === "contacts") renderContacts(c);
|
else if (page === "contacts") renderContacts(c);
|
||||||
|
else if (page === "insider") renderInsider(c);
|
||||||
else if (page === "video") renderVideo(c);
|
else if (page === "video") renderVideo(c);
|
||||||
else if (page === "laws") renderLaws(c);
|
else if (page === "laws") renderLaws(c);
|
||||||
else if (page === "complaints") renderComplaints(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); });
|
laws.forEach(function (l) { if (cats.indexOf(l.cat) < 0) cats.push(l.cat); });
|
||||||
|
|
||||||
var html = pageTitle("⚖️ Заң нормалары мен комплаенс заңдар");
|
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 += '<div class="tabs">';
|
||||||
html += '<button class="tab active" data-cat="all" onclick="filterLaws(\'all\')">Барлығы</button>';
|
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>"; });
|
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>';
|
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) {
|
function renderComplaints(el) {
|
||||||
var html = pageTitle("✉️ Шағым жәшігі");
|
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 += '<div class="form">';
|
||||||
html += '<label>Санаты</label><select id="compCat"><option>Жемқорлық фактісі</option><option>Мүдделер қақтығысы</option><option>Жұмыс ортасы</option><option>Жалақы/еңбек құқығы</option><option>Басқа</option></select>';
|
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 += '<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 += '<button class="btn btn-primary" style="margin-top:12px" onclick="sendComplaint()">Жіберу</button>';
|
||||||
html += '<div id="compMsg"></div></div></div>';
|
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 class="section-title">Менің шағымдарым' + (state.user.role === "admin" ? " және барлық өтініштер" : "") + "</div>";
|
||||||
html += '<div id="compList"></div>';
|
html += '<div id="compList"></div>';
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
@ -454,6 +537,32 @@ async function setComplaintStatus(id, status) {
|
|||||||
else alert(res.error || "Қате");
|
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) {
|
function renderTests(el) {
|
||||||
if (state.curTest !== null) { renderTestRun(el); return; }
|
if (state.curTest !== null) { renderTestRun(el); return; }
|
||||||
@ -525,22 +634,21 @@ async function submitTest() {
|
|||||||
var answered = Object.keys(state.curTestAnswers).length;
|
var answered = Object.keys(state.curTestAnswers).length;
|
||||||
if (answered < total) { alert("Барлық сұрақтарға жауап беріңіз (" + answered + "/" + total + ")"); return; }
|
if (answered < total) { alert("Барлық сұрақтарға жауап беріңіз (" + answered + "/" + total + ")"); return; }
|
||||||
|
|
||||||
var score = 0;
|
var answers = t.questions.map(function (q, qi) { return state.curTestAnswers[qi]; });
|
||||||
t.questions.forEach(function (q, qi) { if (state.curTestAnswers[qi] === q.correct) score++; });
|
|
||||||
var pct = Math.round(score / total * 100);
|
|
||||||
|
|
||||||
var result = { testId: t.id, login: state.user.login, name: state.user.name, score: pct, date: new Date().toISOString().slice(0, 10) };
|
|
||||||
|
|
||||||
if (state.offline) {
|
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);
|
lsSet("testResults", state.testResults);
|
||||||
showTestResult(pct, score, total);
|
showTestResult(pct, correct, total);
|
||||||
return;
|
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) {
|
if (res.ok) {
|
||||||
await loadServerData();
|
await loadServerData();
|
||||||
showTestResult(pct, score, total);
|
showTestResult(res.score, res.correct, res.total);
|
||||||
} else alert(res.error || "Қате");
|
} 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 class="card stat-card green"><div class="stat-num">' + data.testsTotal + "</div><div class=\"stat-label\">Тест тапсырулар</div></div>";
|
||||||
html += "</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="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="catBars"></div></div>';
|
||||||
html += '<div class="card"><h3>Тест өту көрсеткіші</h3><div id="passBars"></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="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="statsComplaints"></div>';
|
||||||
|
html += '<div class="section-title" style="margin-top:28px">🔒 Комплаенс офицерге жеке хаттар</div><div id="statsDm"></div>';
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
|
|
||||||
var catData = data.byCategory;
|
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>";
|
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>';
|
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() {
|
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;
|
var avg = rs.length ? Math.round(rs.reduce(function (s, r) { return s + r.score; }, 0) / rs.length) : 0;
|
||||||
return { title: t.title, count: rs.length, avg: avg };
|
return { title: t.title, count: rs.length, avg: avg };
|
||||||
});
|
});
|
||||||
|
var dms = state.directMessages || [];
|
||||||
|
var dmOpen = dms.filter(function (m) { return m.status !== "done"; }).length;
|
||||||
return {
|
return {
|
||||||
users: state.offline ? MOCK_USERS.length : 0,
|
users: state.offline ? MOCK_USERS.length : 0,
|
||||||
complaintsTotal: complaints.length,
|
complaintsTotal: complaints.length,
|
||||||
complaintsOpen: byStatus.new + byStatus.work,
|
complaintsOpen: byStatus.new + byStatus.work,
|
||||||
testsTotal: results.length,
|
testsTotal: results.length,
|
||||||
|
dmTotal: dms.length,
|
||||||
|
dmOpen: dmOpen,
|
||||||
|
dmDone: dms.length - dmOpen,
|
||||||
byStatus: byStatus,
|
byStatus: byStatus,
|
||||||
byCategory: catData,
|
byCategory: catData,
|
||||||
byTest: byTest,
|
byTest: byTest,
|
||||||
|
|||||||
54
server.js
54
server.js
@ -8,7 +8,7 @@ const DATA_FILE = path.join(__dirname, "data.json");
|
|||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); }
|
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) {
|
function save(d) {
|
||||||
fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2));
|
fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2));
|
||||||
@ -66,6 +66,8 @@ http.createServer(async (req, res) => {
|
|||||||
announcements: db.announcements,
|
announcements: db.announcements,
|
||||||
org: db.org,
|
org: db.org,
|
||||||
contacts: db.contacts,
|
contacts: db.contacts,
|
||||||
|
insider: db.insider,
|
||||||
|
giftRules: db.giftRules,
|
||||||
videos: db.videos,
|
videos: db.videos,
|
||||||
laws: db.laws,
|
laws: db.laws,
|
||||||
tests: db.tests.map((t) => ({
|
tests: db.tests.map((t) => ({
|
||||||
@ -134,15 +136,58 @@ http.createServer(async (req, res) => {
|
|||||||
const b = await body(req);
|
const b = await body(req);
|
||||||
const test = db.tests.find((t) => t.id === Number(b.testId));
|
const test = db.tests.find((t) => t.id === Number(b.testId));
|
||||||
if (!test) return send(res, 400, { ok: false, error: "Тест табылмады" });
|
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({
|
db.testResults.push({
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
testId: test.id,
|
testId: test.id,
|
||||||
login: user.login,
|
login: user.login,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
score: Number(b.score) || 0,
|
score,
|
||||||
date: new Date().toISOString().slice(0, 10),
|
date: new Date().toISOString().slice(0, 10),
|
||||||
});
|
});
|
||||||
save(db);
|
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 });
|
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;
|
const avg = rs.length ? Math.round(rs.reduce((s, r) => s + r.score, 0) / rs.length) : 0;
|
||||||
return { title: t.title, count: rs.length, avg };
|
return { 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: {
|
return send(res, 200, { ok: true, data: {
|
||||||
users: db.users.length,
|
users: db.users.length,
|
||||||
complaintsTotal: db.complaints.length,
|
complaintsTotal: db.complaints.length,
|
||||||
complaintsOpen: byStatus.new + byStatus.work,
|
complaintsOpen: byStatus.new + byStatus.work,
|
||||||
testsTotal: db.testResults.length,
|
testsTotal: db.testResults.length,
|
||||||
|
dmTotal: dms.length,
|
||||||
|
dmOpen,
|
||||||
|
dmDone: dms.length - dmOpen,
|
||||||
byStatus,
|
byStatus,
|
||||||
byCategory: Object.keys(byCategory).map((k) => ({ cat: k, count: byCategory[k] })),
|
byCategory: Object.keys(byCategory).map((k) => ({ cat: k, count: byCategory[k] })),
|
||||||
byTest,
|
byTest,
|
||||||
|
|||||||
11
style.css
11
style.css
@ -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{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)}
|
.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){
|
@media (max-width:900px){
|
||||||
.grid-4{grid-template-columns:repeat(2,1fr)}
|
.grid-4{grid-template-columns:repeat(2,1fr)}
|
||||||
.grid-3{grid-template-columns:repeat(2,1fr)}
|
.grid-3{grid-template-columns:repeat(2,1fr)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user