v4: аккаунттарды басқару, құпия сөзді қалпына келтіру, жеке хаттарға жауап
This commit is contained in:
parent
5a5963d084
commit
bcc0954b2a
425
data.json
425
data.json
@ -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
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
257
script.js
257
script.js
@ -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;
|
||||||
if (state.user && state.user.role === "admin") {
|
|
||||||
var dm = await apiGet("/api/direct-messages");
|
var dm = await apiGet("/api/direct-messages");
|
||||||
if (dm.ok) state.directMessages = dm.data;
|
if (dm.ok) state.directMessages = dm.data;
|
||||||
|
if (state.user && state.user.role === "admin") {
|
||||||
|
var us = await apiGet("/api/users");
|
||||||
|
if (us.ok) state.users = us.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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) {
|
||||||
|
html += '<div class="reply-box"><b>📨 Жауап (' + c.replyDate + '):</b><p>' + esc(c.reply) + "</p></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.user.role === "admin") {
|
||||||
|
html += '<div class="row" style="margin-top:12px;gap:8px;align-items:flex-start;flex-wrap:wrap">';
|
||||||
|
html += '<input id="replyComp_' + c.id + '" class="reply-input" placeholder="Жауап жазыңыз және «Жіберу» басыңыз...">';
|
||||||
|
html += '<button class="btn btn-sm btn-cyan" onclick="replyComplaint(' + c.id + ')">Жіберу</button>';
|
||||||
|
if (c.status !== "done") {
|
||||||
if (c.status !== "work") html += '<button class="btn btn-sm btn-ghost" onclick="setComplaintStatus(' + c.id + ',\'work\')">Қаралуда</button>';
|
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 += '<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
102
server.js
@ -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,
|
||||||
|
|||||||
31
style.css
31
style.css
@ -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
6
users.json
Normal 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" }
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue
Block a user