From 5a5963d0840183bf5feb182eb9825e0c6af8ca56 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Tue, 11 Aug 2026 07:58:12 +0000 Subject: [PATCH] =?UTF-8?q?v3:=20=D0=B8=D0=BD=D1=81=D0=B0=D0=B9=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=BB=D1=96=D0=BA=20=D0=B0=D2=9B=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D1=82=20=D0=B1=D3=A9=D0=BB=D1=96=D0=BC=D1=96+=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82,=20=D1=81=D1=8B=D0=B9=D0=BB=D1=8B=D2=9B?= =?UTF-8?q?=D1=82=D0=B0=D1=80=20=D0=B8=D0=BD=D1=84=D0=BE=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=B0,=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D0=B5=D0=BD=D1=81=20=D0=BE=D1=84=D0=B8=D1=86=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=20=D0=B6=D0=B5=D0=BA=D0=B5=20=D1=85=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data.json | 34 ++++++++++- script.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++---- server.js | 54 ++++++++++++++++- style.css | 11 ++++ 4 files changed, 262 insertions(+), 13 deletions(-) diff --git a/data.json b/data.json index 980c281..58289ee 100644 --- a/data.json +++ b/data.json @@ -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": [] } diff --git a/script.js b/script.js index 32666b9..48b022a 100644 --- a/script.js +++ b/script.js @@ -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 += '
'; + html += '

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

'; + html += '
'; + html += '
✅ Рұқсат етілген
'; + gr.allowed.forEach(function (t) { html += '
' + esc(t) + "
"; }); + html += "
"; + html += '
🚫 Тыйым салынған
'; + gr.forbidden.forEach(function (t) { html += '
' + esc(t) + "
"; }); + html += "
"; + html += "
"; + } + html += '
'; html += ''; cats.forEach(function (c) { html += '"; }); @@ -373,10 +426,31 @@ function filterLaws(cat) { document.getElementById("lawList").innerHTML = html || '
Табылмады
'; } +/* ---------------- ИНСАЙДЕРЛІК АҚПАРАТ ---------------- */ +function renderInsider(el) { + var list = state.content && state.content.insider ? state.content.insider : []; + var html = pageTitle("🔐 Инсайдерлік ақпарат және құпиялылық"); + html += '

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

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

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

"; + html += "

" + esc(item.text) + "

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

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

'; + html += '

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

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

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

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

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

'; html += '
'; html += ''; html += ''; @@ -384,6 +458,15 @@ function renderComplaints(el) { html += ''; html += '
'; + html += '

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

'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += ''; + html += '
'; + html += "
"; + html += '
Менің шағымдарым' + (state.user.role === "admin" ? " және барлық өтініштер" : "") + "
"; html += '
'; 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 = '
Хат мәтінін жазыңыз
'; 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 = '
✅ Хат жіберілді (демо-режимде жергілікті сақталды)
'; + 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 = '
✅ Хатыңыз комплаенс офицерге жіберілді
'; + document.getElementById("dmText").value = ""; + } else document.getElementById("dmMsg").innerHTML = '
' + esc(res.error || "Қате") + "
"; +} + /* ---------------- ТЕСТТЕР ---------------- */ 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 += '
' + data.testsTotal + "
Тест тапсырулар
"; html += "
"; + if (state.user.role === "admin") { + html += '
'; + html += '
' + (data.dmTotal || 0) + "
Жеке хаттар (барлығы)
"; + html += '
' + (data.dmOpen || 0) + "
Жауап күтіп тұрған хаттар
"; + html += '
' + (data.dmDone || 0) + "
Шешілген хаттар
"; + html += "
"; + } + html += '
'; html += '

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

'; html += '

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

'; @@ -584,6 +700,7 @@ async function renderStats(el) { html += '

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

'; html += '
Соңғы шағымдар
'; + html += '
🔒 Комплаенс офицерге жеке хаттар
'; el.innerHTML = html; var catData = data.byCategory; @@ -618,6 +735,40 @@ async function renderStats(el) { chtml += '
' + esc(c.category) + "" + (c.anonymous ? "Аноним" : esc(c.authorName)) + ' · ' + st + "

" + esc(c.text) + "

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

' + esc(m.text) + "

"; + if (m.status !== "done") { + dhtml += '
'; + if (m.status !== "work") dhtml += ''; + dhtml += '
'; + } + dhtml += "
"; + }); + document.getElementById("statsDm").innerHTML = dhtml || '
Жеке хаттар жоқ
'; + } +} + +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, diff --git a/server.js b/server.js index 342297c..b196b88 100644 --- a/server.js +++ b/server.js @@ -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, diff --git a/style.css b/style.css index abeb854..968e422 100644 --- a/style.css +++ b/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: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)}