v4: аккаунттарды басқару, құпия сөзді қалпына келтіру, жеке хаттарға жауап

This commit is contained in:
Dauren777 2026-08-11 09:27:27 +00:00
parent 5a5963d084
commit bcc0954b2a
6 changed files with 759 additions and 77 deletions

425
data.json
View File

@ -1,54 +1,224 @@
{ {
"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" }
],
"announcements": [ "announcements": [
{ "id": 1, "title": "Жылдық комплаенс оқуы басталды", "text": "Барлық қызметкерлер 1 қарашаға дейін «Сыбайлас жемқорлыққа қарсы» тестін тапсыруы қажет. Видео сабақтар мен заңдар бөлімін қарап шығыңыз.", "date": "2026-08-11" }, {
{ "id": 2, "title": "Мүдделер қақтығысын декларациялау", "text": "Қызметкерлер жыл сайын мүдделер қақтығысы туралы декларация толтырады. Нысан шағым жәшігі бөлімінен алуға болады.", "date": "2026-08-05" } "id": 1,
"title": "Жылдық комплаенс оқуы басталды",
"text": "Барлық қызметкерлер 1 қарашаға дейін «Сыбайлас жемқорлыққа қарсы» тестін тапсыруы қажет. Видео сабақтар мен заңдар бөлімін қарап шығыңыз.",
"date": "2026-08-11"
},
{
"id": 2,
"title": "Мүдделер қақтығысын декларациялау",
"text": "Қызметкерлер жыл сайын мүдделер қақтығысы туралы декларация толтырады. Нысан шағым жәшігі бөлімінен алуға болады.",
"date": "2026-08-05"
}
], ],
"org": [ "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": 1,
{ "level": 3, "name": "Қаржы дирекциясы", "person": "Серік Жұмабаев", "email": "s.zhumabaev@qonaev.kz", "phone": "+7 (727) 300-10-03" }, "name": "Бас директор",
{ "level": 3, "name": "Эфир дирекциясы", "person": "Айжан Төлегенова", "email": "a.tolegenova@qonaev.kz", "phone": "+7 (727) 300-10-04" }, "person": "Ерлан Қасымов",
{ "level": 3, "name": "Жаңалықтар дирекциясы", "person": "Марат Бекенов", "email": "m.bekenov@qonaev.kz", "phone": "+7 (727) 300-10-05" }, "email": "e.kasymov@qonaev.kz",
{ "level": 3, "name": "Техникалық дирекция", "person": "Нұрлан Сыдықов", "email": "n.sydykov@qonaev.kz", "phone": "+7 (727) 300-10-06" }, "phone": "+7 (727) 300-10-01"
{ "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" } {
"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": [ "contacts": [
{ "org": "Антикор (ҚР Антикоррупциялық қызметі) Алматы қаласы бойынша департаменті", "type": "Ресми телефон", "phone": "8 (727) 278-73-24", "email": "almaty@nab.gov.kz", "note": "Мекенжай: Жібек Жолы даңғылы, 15" }, {
{ "org": "Антикор бірыңғай Call-орталығы", "type": "Сенім телефоны", "phone": "1424", "email": "", "note": "Қазақстан бойынша қоңырау тегін" }, "org": "Антикор (ҚР Антикоррупциялық қызметі) Алматы қаласы бойынша департаменті",
{ "org": "Антикор WhatsApp-чат-бот", "type": "WhatsApp", "phone": "8 777 000 1424", "email": "", "note": "Хабарлама жіберуге болады" }, "type": "Ресми телефон",
{ "org": "Алматы қаласы прокуратурасы", "type": "Кеңсе", "phone": "8 (727) 225-40-86", "email": "", "note": "Мекенжай: Желтоқсан көшесі, 189" }, "phone": "8 (727) 278-73-24",
{ "org": "Алматы қаласы прокуратурасы", "type": "Кезекші прокурор", "phone": "8 (727) 330-80-51", "email": "", "note": "Тәулік бойы" }, "email": "almaty@nab.gov.kz",
{ "org": "ҚР Бас прокуратурасы Call-орталығы", "type": "Сенім телефоны", "phone": "115", "email": "", "note": "Дс-жм 09:00–18:30, қоңырау тегін" }, "note": "Мекенжай: Жібек Жолы даңғылы, 15"
{ "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" } "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": [ "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": 1,
{ "id": 3, "title": "Мүдделер қақтығысы", "desc": "Мүдделер қақтығысын қалай тану және декларациялау керек.", "dur": "15 мин", "thumb": "https://picsum.photos/seed/compl3/480/270" }, "title": "Комплаенс деген не?",
{ "id": 4, "title": "Сыйлықтар мен белгілер ережелері", "desc": "Қызметкерлерге қандай сыйлықтар қабылдауға рұқсат етіледі.", "dur": "10 мин", "thumb": "https://picsum.photos/seed/compl4/480/270" } "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": [ "laws": [
{ "id": 1, "cat": "Антикор", "title": "«Сыбайлас жемқорлыққа қарсы іс-қимыл туралы» ҚР Заңы", "text": "Қызметкерлердің жемқорлық құқық бұзушылықтар жасауына тыйым салады, кірістер декларациясын міндеттейді." }, {
{ "id": 2, "cat": "Еңбек", "title": "ҚР Еңбек кодексі", "text": "Қызметкерлердің құқықтары мен міндеттері, еңбек тәртібі, жауапкершілік нормалары." }, "id": 1,
{ "id": 3, "cat": "Антикор", "title": "Мүдделер қақтығысы туралы ереже", "text": "Компания ішіндегі мүдделер қақтығысын анықтау, жариялау және шешу тәртібі." }, "cat": "Антикор",
{ "id": 4, "cat": "Ақпарат", "title": "«Ақпаратқа қол жеткізу туралы» ҚР Заңы", "text": "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары." }, "title": "«Сыбайлас жемқорлыққа қарсы іс-қимыл туралы» ҚР Заңы",
{ "id": 5, "cat": "Этика", "title": "Компанияның Іскерлік этика кодексі", "text": "Қызметкерлердің мінез-құлық стандарттары, қарым-қатынас ережелері." } "text": "Қызметкерлердің жемқорлық құқық бұзушылықтар жасауына тыйым салады, кірістер декларациясын міндеттейді."
},
{
"id": 2,
"cat": "Еңбек",
"title": "ҚР Еңбек кодексі",
"text": "Қызметкерлердің құқықтары мен міндеттері, еңбек тәртібі, жауапкершілік нормалары."
},
{
"id": 3,
"cat": "Антикор",
"title": "Мүдделер қақтығысы туралы ереже",
"text": "Компания ішіндегі мүдделер қақтығысын анықтау, жариялау және шешу тәртібі."
},
{
"id": 4,
"cat": "Ақпарат",
"title": "«Ақпаратқа қол жеткізу туралы» ҚР Заңы",
"text": "Қоғамдық ақпаратқа қол жеткізу, құпия ақпаратты қорғау талаптары."
},
{
"id": 5,
"cat": "Этика",
"title": "Компанияның Іскерлік этика кодексі",
"text": "Қызметкерлердің мінез-құлық стандарттары, қарым-қатынас ережелері."
}
], ],
"insider": [ "insider": [
{ "title": "Инсайдерлік ақпарат дегеніміз не?", "text": "Бұл компания туралы әлі жарияланбаған, қызметкерге өз жұмысы барысында белгілі болған деректер: қаржылық көрсеткіштер, келісімшарттар, жаңалық сюжеттері, кадрлық шешімдер." }, {
{ "title": "Таратуға тыйым салынады", "text": "Инсайдерлік ақпаратты эфирге шыққанға дейін немесе ресми жарияланғанға дейін үшінші тұлғаларға, әлеуметтік желілерде немесе таныстарға таратуға тыйым салынады." }, "title": "Инсайдерлік ақпарат дегеніміз не?",
{ "title": "Дереккөздерді құпия ұстау", "text": "Журналистер өз дереккөздерін құпия ұстауға міндетті. Дереккөздің атын ашу — еңбек тәртібін өрескел бұзу." }, "text": "Бұл компания туралы әлі жарияланбаған, қызметкерге өз жұмысы барысында белгілі болған деректер: қаржылық көрсеткіштер, келісімшарттар, жаңалық сюжеттері, кадрлық шешімдер."
{ "title": "Не істеу керек?", "text": "Инсайдерлік ақпаратқа қол жеткізгенде және оны пайдалану қажет болғанда — комплаенс офицермен кеңесіңіз. Ақпарат тек қызметтік қажеттілік үшін пайдаланылады." }, },
{ "title": "Жауапкершілік", "text": "Инсайдерлік ақпаратты тарату — тәртіптік жауапкершілікке әкеледі, ал ауыр жағдайларда ҚР заңнамасы бойынша қылмыстық жауапкершілікке де тартылуы мүмкін." } {
"title": "Таратуға тыйым салынады",
"text": "Инсайдерлік ақпаратты эфирге шыққанға дейін немесе ресми жарияланғанға дейін үшінші тұлғаларға, әлеуметтік желілерде немесе таныстарға таратуға тыйым салынады."
},
{
"title": "Дереккөздерді құпия ұстау",
"text": "Журналистер өз дереккөздерін құпия ұстауға міндетті. Дереккөздің атын ашу — еңбек тәртібін өрескел бұзу."
},
{
"title": "Не істеу керек?",
"text": "Инсайдерлік ақпаратқа қол жеткізгенде және оны пайдалану қажет болғанда — комплаенс офицермен кеңесіңіз. Ақпарат тек қызметтік қажеттілік үшін пайдаланылады."
},
{
"title": "Жауапкершілік",
"text": "Инсайдерлік ақпаратты тарату — тәртіптік жауапкершілікке әкеледі, ал ауыр жағдайларда ҚР заңнамасы бойынша қылмыстық жауапкершілікке де тартылуы мүмкін."
}
], ],
"giftRules": { "giftRules": {
"allowed": [ "allowed": [
@ -66,33 +236,174 @@
}, },
"tests": [ "tests": [
{ {
"id": 1, "title": "Сыбайлас жемқорлыққа қарсы негіздер", "pass": 70, "id": 1,
"title": "Сыбайлас жемқорлыққа қарсы негіздер",
"pass": 70,
"questions": [ "questions": [
{ "q": "Сыбайлас жемқорлық құқық бұзушылық деген не?", "options": ["Қызметкердің өз өкілеттігін жеке байыту үшін пайдалануы", "Жұмысқа уақытында келу", "Каникулға шығу", "Басшымен сөйлесу"], "correct": 0 }, {
{ "q": "Қызметкерге қандай сыйлық қабылдауға рұқсат?", "options": ["Кез келген уақытта кез келген сыйлық", "Құны 3 АЕК-тен аспайтын және есепке алынған сыйлық", "Тек ақшалай", "Барлық сыйлыққа тыйым салынған"], "correct": 1 }, "q": "Сыбайлас жемқорлық құқық бұзушылық деген не?",
{ "q": "Мүдделер қақтығысы дегеніміз?", "options": ["Жұмыстағы дау", "Жеке мүдде мен қызметтік міндеттердің қайшылығы", "Екі қызметкердің таласы", "Жұмыс орнын ауыстыру"], "correct": 1 }, "options": [
{ "q": "Жемқорлық фактісін қайда хабарлау керек?", "options": ["Барлығына айту", "Комплаенс қызметіне немесе шағым жәшігіне", "Әлеуметтік желіде жазу", "Үндемеу"], "correct": 1 }, "Қызметкердің өз өкілеттігін жеке байыту үшін пайдалануы",
{ "q": "Кірістер мен мүлік туралы декларация кімге міндетті?", "options": ["Тек басшыларға", "Заңда көрсетілген қызметкерлерге", "Ешкімге", "Тек шетелге шығатындарға"], "correct": 1 } "Жұмысқа уақытында келу",
"Каникулға шығу",
"Басшымен сөйлесу"
],
"correct": 0
},
{
"q": "Қызметкерге қандай сыйлық қабылдауға рұқсат?",
"options": [
"Кез келген уақытта кез келген сыйлық",
"Құны 3 АЕК-тен аспайтын және есепке алынған сыйлық",
"Тек ақшалай",
"Барлық сыйлыққа тыйым салынған"
],
"correct": 1
},
{
"q": "Мүдделер қақтығысы дегеніміз?",
"options": [
"Жұмыстағы дау",
"Жеке мүдде мен қызметтік міндеттердің қайшылығы",
"Екі қызметкердің таласы",
"Жұмыс орнын ауыстыру"
],
"correct": 1
},
{
"q": "Жемқорлық фактісін қайда хабарлау керек?",
"options": [
"Барлығына айту",
"Комплаенс қызметіне немесе шағым жәшігіне",
"Әлеуметтік желіде жазу",
"Үндемеу"
],
"correct": 1
},
{
"q": "Кірістер мен мүлік туралы декларация кімге міндетті?",
"options": [
"Тек басшыларға",
"Заңда көрсетілген қызметкерлерге",
"Ешкімге",
"Тек шетелге шығатындарға"
],
"correct": 1
}
] ]
}, },
{ {
"id": 2, "title": "Іскерлік этика кодексі", "pass": 70, "id": 2,
"title": "Іскерлік этика кодексі",
"pass": 70,
"questions": [ "questions": [
{ "q": "Іскерлік этика кодексінің мақсаты?", "options": ["Қызметкерлерді жазалау", "Мінез-құлық стандарттарын белгілеу", "Жалақыны азайту", "Каникулды қысқарту"], "correct": 1 }, {
{ "q": "Алқаптас әріптеске қалай қарау керек?", "options": ["Құрметпен және теңдікте", "Менсінбеу", "Елемеу", "Тек басшыларға ғана құрмет"], "correct": 0 }, "q": "Іскерлік этика кодексінің мақсаты?",
{ "q": "Конфиденциалды ақпаратты не істеуге болмайды?", "options": ["Қауіпсіз жерде сақтау", "Үшінші тұлғаларға тарату", "Пароль қою", "Құлыптап қою"], "correct": 1 }, "options": [
{ "q": "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?", "options": ["Келісу", "Бас тартып, комплаенс қызметіне хабарлау", "Ойланып жауап беру", "Ешкімге айтпау"], "correct": 1 }, "Қызметкерлерді жазалау",
{ "q": "Тест нәтижелері мен оқу материалдары кімге қажет?", "options": ["Тек HR-ға", "Барлық қызметкерлерге", "Тек админге", "Тек басшыларға"], "correct": 1 } "Мінез-құлық стандарттарын белгілеу",
"Жалақыны азайту",
"Каникулды қысқарту"
],
"correct": 1
},
{
"q": "Алқаптас әріптеске қалай қарау керек?",
"options": [
"Құрметпен және теңдікте",
"Менсінбеу",
"Елемеу",
"Тек басшыларға ғана құрмет"
],
"correct": 0
},
{
"q": "Конфиденциалды ақпаратты не істеуге болмайды?",
"options": [
"Қауіпсіз жерде сақтау",
"Үшінші тұлғаларға тарату",
"Пароль қою",
"Құлыптап қою"
],
"correct": 1
},
{
"q": "Жемқорлық әрекетке қатысуды сұраса, не істеу керек?",
"options": [
"Келісу",
"Бас тартып, комплаенс қызметіне хабарлау",
"Ойланып жауап беру",
"Ешкімге айтпау"
],
"correct": 1
},
{
"q": "Тест нәтижелері мен оқу материалдары кімге қажет?",
"options": [
"Тек HR-ға",
"Барлық қызметкерлерге",
"Тек админге",
"Тек басшыларға"
],
"correct": 1
}
] ]
}, },
{ {
"id": 3, "title": "Инсайдерлік ақпарат және құпиялылық", "pass": 70, "id": 3,
"title": "Инсайдерлік ақпарат және құпиялылық",
"pass": 70,
"questions": [ "questions": [
{ "q": "Инсайдерлік ақпарат дегеніміз не?", "options": ["Әріптестер туралы қауесет", "Компания туралы әлі жарияланбаған қызметтік деректер", "Әлеуметтік желідегі ақпарат", "Жалпыға белгілі жаңалықтар"], "correct": 1 }, {
{ "q": "Жаңа сюжет эфирге шыққанға дейін оны танысқа айтуға бола ма?", "options": ["Иә, егер ол жақын таныс болса", "Жоқ, инсайдерлік ақпаратты таратуға тыйым салынады", "Иә, бірақ тек SMS арқылы", "Болады, себебі бұл жай ғана жаңалық"], "correct": 1 }, "q": "Инсайдерлік ақпарат дегеніміз не?",
{ "q": "Дереккөздің атын басшылыққа ашуға бола ма?", "options": ["Иә, міндетті түрде", "Жоқ, журналист дереккөзді құпия ұстайды", "Тек түскі үзілісте айтуға болады", "Тек жазбаша түрде"], "correct": 1 }, "options": [
{ "q": "Инсайдерлік ақпаратты таратқаны үшін не болады?", "options": ["Ештеңе болмайды", "Тәртіптік жауапкершілік, ауыр жағдайда заң бойынша жауапкершілік", "Тек сөгіс беріледі", "Бонус өседі"], "correct": 1 }, "Әріптестер туралы қауесет",
{ "q": "Қызметтік ақпаратты қай жерде талқылауға БОЛМАЙДЫ?", "options": ["Кеңседе әріптестермен", "Көпшілік орындарда және әлеуметтік желілерде", "Ресми жиналыста", "Комплаенс офицермен кеңесте"], "correct": 1 } "Компания туралы әлі жарияланбаған қызметтік деректер",
"Әлеуметтік желідегі ақпарат",
"Жалпыға белгілі жаңалықтар"
],
"correct": 1
},
{
"q": "Жаңа сюжет эфирге шыққанға дейін оны танысқа айтуға бола ма?",
"options": [
"Иә, егер ол жақын таныс болса",
"Жоқ, инсайдерлік ақпаратты таратуға тыйым салынады",
"Иә, бірақ тек SMS арқылы",
"Болады, себебі бұл жай ғана жаңалық"
],
"correct": 1
},
{
"q": "Дереккөздің атын басшылыққа ашуға бола ма?",
"options": [
"Иә, міндетті түрде",
"Жоқ, журналист дереккөзді құпия ұстайды",
"Тек түскі үзілісте айтуға болады",
"Тек жазбаша түрде"
],
"correct": 1
},
{
"q": "Инсайдерлік ақпаратты таратқаны үшін не болады?",
"options": [
"Ештеңе болмайды",
"Тәртіптік жауапкершілік, ауыр жағдайда заң бойынша жауапкершілік",
"Тек сөгіс беріледі",
"Бонус өседі"
],
"correct": 1
},
{
"q": "Қызметтік ақпаратты қай жерде талқылауға БОЛМАЙДЫ?",
"options": [
"Кеңседе әріптестермен",
"Көпшілік орындарда және әлеуметтік желілерде",
"Ресми жиналыста",
"Комплаенс офицермен кеңесте"
],
"correct": 1
}
] ]
} }
], ],

View File

@ -23,6 +23,13 @@
<div id="loginError" class="login-error"></div> <div id="loginError" class="login-error"></div>
<button type="submit" class="btn btn-primary btn-block">Кіру</button> <button type="submit" class="btn btn-primary btn-block">Кіру</button>
</form> </form>
<button type="button" class="btn btn-ghost btn-block" id="forgotBtn" style="margin-top:10px">Құпия сөзді ұмыттыңыз ба?</button>
<div id="recoverBox" class="hidden" style="margin-top:14px">
<div class="login-sub">Логиніңізді жазыңыз — жаңа құпия сөз беріледі</div>
<input type="text" id="recoverLogin" placeholder="Логин (жеке нөміріңіз)">
<button type="button" class="btn btn-primary btn-block" id="recoverGo" style="margin-top:10px">Жаңа құпия сөз алу</button>
<div id="recoverMsg" class="login-error"></div>
</div>
<p class="login-hint">Демо: админ — 1001 / Admin123 · қызметкер — 2001 / Worker123</p> <p class="login-hint">Демо: админ — 1001 / Admin123 · қызметкер — 2001 / Worker123</p>
</div> </div>
</div> </div>

263
script.js
View File

@ -25,6 +25,7 @@ var NAV = [
{ id: "laws", ico: "⚖️", title: "Заң нормалары" }, { id: "laws", ico: "⚖️", title: "Заң нормалары" },
{ id: "complaints", ico: "✉️", title: "Шағым жәшігі" }, { id: "complaints", ico: "✉️", title: "Шағым жәшігі" },
{ id: "tests", ico: "📝", title: "Тесттер" }, { id: "tests", ico: "📝", title: "Тесттер" },
{ id: "users", ico: "👥", title: "Қызметкерлер", admin: true },
{ id: "stats", ico: "📊", title: "Дашборд", admin: true }, { id: "stats", ico: "📊", title: "Дашборд", admin: true },
]; ];
@ -172,6 +173,28 @@ function lsGet(k, def) { try { var v = JSON.parse(localStorage.getItem("qcom_" +
function lsSet(k, v) { localStorage.setItem("qcom_" + k, JSON.stringify(v)); } function lsSet(k, v) { localStorage.setItem("qcom_" + k, JSON.stringify(v)); }
/* ---------------- КІРУ ---------------- */ /* ---------------- КІРУ ---------------- */
document.getElementById("forgotBtn").addEventListener("click", function () {
document.getElementById("recoverBox").classList.toggle("hidden");
});
document.getElementById("recoverGo").addEventListener("click", async function () {
var login = document.getElementById("recoverLogin").value.trim();
var msg = document.getElementById("recoverMsg");
if (!login) { msg.textContent = "Логиніңізді жазыңыз"; return; }
var res = await apiPost("/api/recover-password", { login: login });
if (res.ok) {
msg.innerHTML = "<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) { document.getElementById("loginForm").addEventListener("submit", async function (e) {
e.preventDefault(); e.preventDefault();
var login = document.getElementById("loginNum").value.trim(); var login = document.getElementById("loginNum").value.trim();
@ -208,9 +231,11 @@ 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;
var dm = await apiGet("/api/direct-messages");
if (dm.ok) state.directMessages = dm.data;
if (state.user && state.user.role === "admin") { if (state.user && state.user.role === "admin") {
var dm = await apiGet("/api/direct-messages"); var us = await apiGet("/api/users");
if (dm.ok) state.directMessages = dm.data; if (us.ok) state.users = us.data;
} }
} }
@ -229,17 +254,28 @@ function enterApp() {
function buildNav() { function buildNav() {
var nav = document.getElementById("nav"); var nav = document.getElementById("nav");
nav.innerHTML = ""; nav.innerHTML = "";
var newCount = newItemsCount();
NAV.forEach(function (item) { NAV.forEach(function (item) {
if (item.admin && state.user.role !== "admin") return; if (item.admin && state.user.role !== "admin") return;
var b = document.createElement("button"); var b = document.createElement("button");
b.className = "nav-item" + (item.admin ? " admin-only" : ""); b.className = "nav-item" + (item.admin ? " admin-only" : "");
b.dataset.id = item.id; b.dataset.id = item.id;
b.innerHTML = '<span class="ico">' + item.ico + "</span><span>" + item.title + "</span>"; 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); }); b.addEventListener("click", function () { go(item.id); });
nav.appendChild(b); 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) { function go(page) {
state.curPage = page; state.curPage = page;
document.querySelectorAll(".nav-item").forEach(function (n) { document.querySelectorAll(".nav-item").forEach(function (n) {
@ -257,6 +293,7 @@ function go(page) {
else if (page === "laws") renderLaws(c); else if (page === "laws") renderLaws(c);
else if (page === "complaints") renderComplaints(c); else if (page === "complaints") renderComplaints(c);
else if (page === "tests") renderTests(c); else if (page === "tests") renderTests(c);
else if (page === "users") renderUsers(c);
else if (page === "stats") renderStats(c); else if (page === "stats") renderStats(c);
} }
@ -276,8 +313,18 @@ function renderHome(el) {
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 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>"; html += "</div>";
html += '<div class="section-title" style="margin-top:28px">📢 Хабарландырулар</div>'; if (state.user.role === "admin") {
if (state.content && state.content.announcements.length) { 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) { 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>"; html += '<div class="announce"><h3>' + esc(a.title) + '</h3><div class="date">📅 ' + a.date + '</div><p>' + esc(a.text) + "</p></div>";
}); });
@ -469,8 +516,42 @@ function renderComplaints(el) {
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>';
html += '<div class="section-title" style="margin-top:24px">Менің жеке хаттарым (комплаенс офицерге)</div>';
html += '<div id="dmList"></div>';
el.innerHTML = html; el.innerHTML = html;
renderComplaintList(); 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() { function renderComplaintList() {
@ -486,10 +567,19 @@ function renderComplaintList() {
html += '<span class="badge ' + c.status + '">' + st + "</span></div>"; html += '<span class="badge ' + c.status + '">' + st + "</span></div>";
html += '<span class="muted">' + (c.anonymous ? "Аноним" : esc(c.authorName)) + " · " + c.date + "</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>"; html += "<p style=\"margin-top:10px\">" + esc(c.text) + "</p>";
if (state.user.role === "admin" && c.status !== "done") {
html += '<div class="row" style="margin-top:12px">'; if (c.reply) {
if (c.status !== "work") html += '<button class="btn btn-sm btn-ghost" onclick="setComplaintStatus(' + c.id + ',\'work\')">Қаралуда</button>'; html += '<div class="reply-box"><b>📨 Жауап (' + c.replyDate + '):</b><p>' + esc(c.reply) + "</p></div>";
html += '<button class="btn btn-sm btn-primary" onclick="setComplaintStatus(' + c.id + ',\'done\')">Шешілді</button>'; }
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>";
} }
html += "</div>"; html += "</div>";
@ -497,6 +587,25 @@ function renderComplaintList() {
document.getElementById("compList").innerHTML = html || '<div class="empty">Әзірге шағым жоқ</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() { async function sendComplaint() {
var category = document.getElementById("compCat").value; var category = document.getElementById("compCat").value;
var text = document.getElementById("compText").value.trim(); var text = document.getElementById("compText").value.trim();
@ -551,18 +660,113 @@ async function sendDirectMsg() {
authorLogin: anonymous ? null : state.user.login, authorLogin: anonymous ? null : state.user.login,
authorName: anonymous ? null : state.user.name, authorName: anonymous ? null : state.user.name,
}); });
state.directMessages = list;
lsSet("directMessages", list); lsSet("directMessages", list);
document.getElementById("dmMsg").innerHTML = '<div class="msg ok">✅ Хат жіберілді (демо-режимде жергілікті сақталды)</div>'; document.getElementById("dmMsg").innerHTML = '<div class="msg ok">✅ Хат жіберілді (демо-режимде жергілікті сақталды)</div>';
document.getElementById("dmText").value = ""; document.getElementById("dmText").value = "";
renderMyDmList();
return; return;
} }
var res = await apiPost("/api/direct-messages", { category: category, text: text, anonymous: anonymous }); var res = await apiPost("/api/direct-messages", { category: category, text: text, anonymous: anonymous });
if (res.ok) { if (res.ok) {
document.getElementById("dmMsg").innerHTML = '<div class="msg ok">✅ Хатыңыз комплаенс офицерге жіберілді</div>'; document.getElementById("dmMsg").innerHTML = '<div class="msg ok">✅ Хатыңыз комплаенс офицерге жіберілді</div>';
document.getElementById("dmText").value = ""; document.getElementById("dmText").value = "";
await loadServerData();
renderMyDmList();
} else document.getElementById("dmMsg").innerHTML = '<div class="msg err">' + esc(res.error || "Қате") + "</div>"; } 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="uPass" placeholder="Ағылшын әріптерімен"></div>';
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></tr></thead><tbody>';
users.forEach(function (u) {
html += "<tr><td><b>" + esc(u.login) + "</b></td><td>" + esc(u.name) + "</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 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, password: pass, role: role });
msg.innerHTML = '<div class="msg ok">✅ Қосылды (демо)</div>';
loadUsersList();
return;
}
var res = await apiPost("/api/users", { login: login, name: name, password: pass, role: role });
if (res.ok) {
msg.innerHTML = '<div class="msg ok">✅ Қызметкер қосылды</div>';
document.getElementById("uLogin").value = "";
document.getElementById("uName").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) { function renderTests(el) {
if (state.curTest !== null) { renderTestRun(el); return; } if (state.curTest !== null) { renderTestRun(el); return; }
@ -732,7 +936,11 @@ async function renderStats(el) {
var chtml = ""; var chtml = "";
cl.forEach(function (c) { cl.forEach(function (c) {
var st = { "new": "Жаңа", "work": "Қаралуда", "done": "Шешілді" }[c.status] || c.status; 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></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>";
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>'; document.getElementById("statsComplaints").innerHTML = chtml || '<div class="empty">Шағым жоқ</div>';
@ -745,17 +953,44 @@ async function renderStats(el) {
dhtml += '<div class="card complaint-item ' + m.status + '" style="margin-bottom:10px">'; 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 += '<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>"; dhtml += '<p style="margin-top:8px">' + esc(m.text) + "</p>";
if (m.status !== "done") { if (m.reply) {
dhtml += '<div class="row" style="margin-top:10px">'; dhtml += '<div class="reply-box"><b>📨 Жауап:</b><p>' + esc(m.reply) + "</p></div>";
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>"; 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>'; 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) { async function setDmStatus(id, status) {
if (state.offline) { if (state.offline) {
var list = lsGet("directMessages", []); var list = lsGet("directMessages", []);

102
server.js
View File

@ -5,14 +5,22 @@ const crypto = require("crypto");
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, "data.json"); const DATA_FILE = path.join(__dirname, "data.json");
const USERS_FILE = path.join(__dirname, "users.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: [], contacts: [], insider: [], giftRules: {}, videos: [], laws: [], tests: [], complaints: [], testResults: [], directMessages: [] }; } catch (e) { return { 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));
} }
function loadUsers() {
try { return JSON.parse(fs.readFileSync(USERS_FILE, "utf8")); }
catch (e) { return []; }
}
function saveUsers(users) {
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2));
}
const sessions = new Map(); const sessions = new Map();
@ -48,13 +56,68 @@ http.createServer(async (req, res) => {
// ---------- ЛОГИН ---------- // ---------- ЛОГИН ----------
if (req.method === "POST" && url === "/api/login") { if (req.method === "POST" && url === "/api/login") {
const b = await body(req); const b = await body(req);
const u = db.users.find((x) => x.login === String(b.login) && x.password === String(b.password)); const u = loadUsers().find((x) => x.login === String(b.login) && x.password === String(b.password));
if (!u) return send(res, 401, { ok: false, error: "Логин немесе құпия сөз қате" }); if (!u) return send(res, 401, { ok: false, error: "Логин немесе құпия сөз қате" });
const token = crypto.randomBytes(24).toString("hex"); const token = crypto.randomBytes(24).toString("hex");
sessions.set(token, u); sessions.set(token, u);
return send(res, 200, { ok: true, token, user: publicUser(u) }); return send(res, 200, { ok: true, token, user: publicUser(u) });
} }
// ---------- ПАРОЛЬДЫ ҚАЛПЫНА КЕЛТІРУ ----------
if (req.method === "POST" && url === "/api/recover-password") {
const b = await body(req);
const users = loadUsers();
const u = users.find((x) => x.login === String(b.login));
if (!u) return send(res, 404, { ok: false, error: "Мұндай логин табылмады" });
const newPass = "Qonaev" + Math.floor(1000 + Math.random() * 9000);
u.password = newPass;
saveUsers(users);
return send(res, 200, { ok: true, password: newPass, name: u.name });
}
// ---------- АККАУНТТАРДЫ БАСҚАРУ (админ) ----------
if (req.method === "GET" && url === "/api/users") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
return send(res, 200, { ok: true, data: loadUsers().map((x) => ({
login: x.login, name: x.name, role: x.role, password: x.password,
})) });
}
if (req.method === "POST" && url === "/api/users") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
if (!b.login || !b.name || !b.password) return send(res, 400, { ok: false, error: "Логин, аты-жөні және құпия сөз міндетті" });
const users = loadUsers();
if (users.find((x) => x.login === String(b.login))) return send(res, 400, { ok: false, error: "Мұндай логин бар" });
users.push({ login: String(b.login), password: String(b.password), name: String(b.name), role: b.role === "admin" ? "admin" : "user" });
saveUsers(users);
return send(res, 200, { ok: true });
}
const uDelMatch = url.match(/^\/api\/users\/([^/]+)$/);
if (req.method === "DELETE" && uDelMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const users = loadUsers();
const i = users.findIndex((x) => x.login === uDelMatch[1]);
if (i < 0) return send(res, 404, { ok: false, error: "Табылмады" });
users.splice(i, 1);
saveUsers(users);
return send(res, 200, { ok: true });
}
const uPassMatch = url.match(/^\/api\/users\/([^/]+)\/password$/);
if (req.method === "POST" && uPassMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
const users = loadUsers();
const u = users.find((x) => x.login === uPassMatch[1]);
if (!u) return send(res, 404, { ok: false, error: "Табылмады" });
if (!b.password) return send(res, 400, { ok: false, error: "Жаңа құпия сөзді жазыңыз" });
u.password = String(b.password);
saveUsers(users);
return send(res, 200, { ok: true });
}
// ---------- АУТЕНТИФИКАЦИЯ ---------- // ---------- АУТЕНТИФИКАЦИЯ ----------
const user = getUser(req); const user = getUser(req);
const isApi = url.startsWith("/api/"); const isApi = url.startsWith("/api/");
@ -114,6 +177,20 @@ http.createServer(async (req, res) => {
return send(res, 200, { ok: true }); return send(res, 200, { ok: true });
} }
const compReplyMatch = url.match(/^\/api\/complaints\/(\d+)\/reply$/);
if (req.method === "POST" && compReplyMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
const b = await body(req);
const c = db.complaints.find((x) => x.id === Number(compReplyMatch[1]));
if (!c) return send(res, 404, { ok: false, error: "Табылмады" });
if (!b.reply) return send(res, 400, { ok: false, error: "Жауап мәтінін жазыңыз" });
c.reply = String(b.reply).slice(0, 3000);
c.replyDate = new Date().toISOString().slice(0, 10);
if (c.status === "new") c.status = "work";
save(db);
return send(res, 200, { ok: true });
}
const stMatch = url.match(/^\/api\/complaints\/(\d+)\/status$/); const stMatch = url.match(/^\/api\/complaints\/(\d+)\/status$/);
if (req.method === "POST" && stMatch) { if (req.method === "POST" && stMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" }); if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
@ -157,8 +234,9 @@ http.createServer(async (req, res) => {
// ---------- КОМПЛАЕНС ОФИЦЕРГЕ ЖЕКЕ ХАТ ---------- // ---------- КОМПЛАЕНС ОФИЦЕРГЕ ЖЕКЕ ХАТ ----------
if (req.method === "GET" && url === "/api/direct-messages") { if (req.method === "GET" && url === "/api/direct-messages") {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" }); let list = db.directMessages || [];
return send(res, 200, { ok: true, data: db.directMessages || [] }); if (user.role !== "admin") list = list.filter((m) => m.authorLogin === user.login);
return send(res, 200, { ok: true, data: list });
} }
if (req.method === "POST" && url === "/api/direct-messages") { if (req.method === "POST" && url === "/api/direct-messages") {
@ -180,6 +258,20 @@ http.createServer(async (req, res) => {
return send(res, 200, { ok: true }); return send(res, 200, { ok: true });
} }
const dmReplyMatch = url.match(/^\/api\/direct-messages\/(\d+)\/reply$/);
if (req.method === "POST" && dmReplyMatch) {
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(dmReplyMatch[1]));
if (!m) return send(res, 404, { ok: false, error: "Табылмады" });
if (!b.reply) return send(res, 400, { ok: false, error: "Жауап мәтінін жазыңыз" });
m.reply = String(b.reply).slice(0, 3000);
m.replyDate = new Date().toISOString().slice(0, 10);
if (m.status === "new") m.status = "work";
save(db);
return send(res, 200, { ok: true });
}
const dmMatch = url.match(/^\/api\/direct-messages\/(\d+)\/status$/); const dmMatch = url.match(/^\/api\/direct-messages\/(\d+)\/status$/);
if (req.method === "POST" && dmMatch) { if (req.method === "POST" && dmMatch) {
if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" }); if (user.role !== "admin") return send(res, 403, { ok: false, error: "Құқық жоқ" });
@ -208,7 +300,7 @@ http.createServer(async (req, res) => {
const dms = db.directMessages || []; const dms = db.directMessages || [];
const dmOpen = dms.filter((m) => m.status !== "done").length; 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: loadUsers().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,

View File

@ -241,6 +241,37 @@ tr:last-child td{border-bottom:none}
.gift-item:last-child{border-bottom:none} .gift-item:last-child{border-bottom:none}
@media (max-width:700px){.gift-grid{grid-template-columns:1fr}} @media (max-width:700px){.gift-grid{grid-template-columns:1fr}}
/* ===== БЕЙДЖ В НАВИГАЦИИ ===== */
.nav-badge{
margin-left:auto;background:var(--red);color:#fff;font-size:11px;font-weight:800;
min-width:20px;height:20px;border-radius:999px;display:inline-flex;align-items:center;
justify-content:center;padding:0 6px;
}
@media (max-width:900px){
.nav-badge{position:absolute;top:4px;right:6px;margin:0}
.nav-item{position:relative}
}
/* ===== ОТВЕТ / ПЕРЕПИСКА ===== */
.reply-box{
background:var(--cyan-50);border-left:3px solid var(--cyan);border-radius:8px;
padding:10px 14px;margin-top:10px;font-size:14px;
}
.reply-box p{margin:4px 0 0;color:var(--gray-500)}
.reply-input{
flex:1;min-width:180px;padding:8px 12px;border:1.5px solid #DFE4EA;border-radius:8px;
font-size:13px;outline:none;font-family:inherit;
}
.reply-input:focus{border-color:var(--accent)}
/* ===== АККАУНТТАР (ҚЫЗМЕТКЕРЛЕР) ===== */
.pass-row{display:flex;align-items:center;gap:8px}
.pass-text{font-family:monospace;background:var(--gray-100);padding:3px 8px;border-radius:6px;font-size:13px}
.login-sub{font-size:13px;color:var(--gray-500);margin-bottom:8px}
#recoverBox{background:var(--gray-50);border:1px dashed #DFE4EA;border-radius:10px;padding:12px}
#recoverBox input{width:100%;padding:10px 12px;border:1.5px solid #DFE4EA;border-radius:10px;font-size:14px;outline:none}
#recoverBox input:focus{border-color:var(--accent)}
@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)}

6
users.json Normal file
View File

@ -0,0 +1,6 @@
[
{ "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" }
]