723 lines
30 KiB
HTML
723 lines
30 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>HR Ассистент — контроль поручений</title>
|
||
<style>
|
||
:root {
|
||
--ink: #0F1218;
|
||
--cyan: #00E5FF;
|
||
--cyan-50: #E8FCFF;
|
||
--white: #fff;
|
||
--gray-500: #5B6573;
|
||
--gray-100: #F2F4F7;
|
||
--gray-200: #E4E7EC;
|
||
--green: #10B981;
|
||
--green-bg: #ECFDF5;
|
||
--amber: #F59E0B;
|
||
--amber-bg: #FFFBEB;
|
||
--red: #EF4444;
|
||
--red-bg: #FEF2F2;
|
||
--indigo: #4F46E5;
|
||
--indigo-bg: #EEF2FF;
|
||
--radius: 12px;
|
||
--shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
||
--shadow-lg: 0 4px 24px rgba(0,0,0,.08);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0 }
|
||
|
||
body {
|
||
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
|
||
color: var(--ink);
|
||
background: var(--gray-100);
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 24px;
|
||
}
|
||
|
||
/* NAV */
|
||
.nav {
|
||
position: sticky; top: 0; z-index: 100;
|
||
background: var(--white);
|
||
border-bottom: 1px solid var(--gray-200);
|
||
backdrop-filter: blur(12px);
|
||
}
|
||
.nav .container {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
height: 60px;
|
||
}
|
||
.nav-logo {
|
||
font-size: 18px; font-weight: 700;
|
||
display: flex; align-items: center; gap: 10px;
|
||
text-decoration: none; color: var(--ink);
|
||
}
|
||
.nav-logo .dot {
|
||
width: 32px; height: 32px;
|
||
background: var(--cyan);
|
||
border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 16px;
|
||
}
|
||
.nav-links { display: flex; gap: 8px; flex-wrap: wrap }
|
||
.nav-links a {
|
||
text-decoration: none; color: var(--gray-500);
|
||
font-size: 14px; font-weight: 500;
|
||
padding: 6px 14px; border-radius: 8px;
|
||
transition: all .15s;
|
||
}
|
||
.nav-links a:hover, .nav-links a.active { background: var(--gray-100); color: var(--ink) }
|
||
|
||
/* HERO / STATS */
|
||
.hero { padding: 48px 0 32px }
|
||
.hero h1 { font-size: 36px; font-weight: 800; line-height: 1.2; margin-bottom: 8px }
|
||
.hero .sub { color: var(--gray-500); font-size: 17px; margin-bottom: 36px }
|
||
|
||
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px }
|
||
.stat-card {
|
||
background: var(--white); border-radius: var(--radius);
|
||
padding: 24px; box-shadow: var(--shadow);
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
}
|
||
.stat-card .label { font-size: 13px; font-weight: 600; color: var(--gray-500); text-transform: uppercase; letter-spacing: .4px }
|
||
.stat-card .value { font-size: 36px; font-weight: 800 }
|
||
.stat-card .dot-indicator {
|
||
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
|
||
}
|
||
|
||
/* SECTION HEADER */
|
||
.section-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin: 56px 0 24px; flex-wrap: wrap; gap: 12px;
|
||
}
|
||
.section-header h2 { font-size: 24px; font-weight: 700 }
|
||
|
||
.search-bar {
|
||
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
||
}
|
||
.search-bar input {
|
||
padding: 10px 16px; border: 1px solid var(--gray-200);
|
||
border-radius: 8px; font-size: 14px; outline: none;
|
||
min-width: 220px; background: var(--white);
|
||
}
|
||
.search-bar input:focus { border-color: var(--cyan) }
|
||
.search-bar select {
|
||
padding: 10px 14px; border: 1px solid var(--gray-200);
|
||
border-radius: 8px; font-size: 14px; outline: none;
|
||
background: var(--white); cursor: pointer;
|
||
}
|
||
|
||
/* BUTTONS */
|
||
.btn {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
padding: 10px 20px; border-radius: 8px;
|
||
font-size: 14px; font-weight: 600; text-decoration: none;
|
||
border: none; cursor: pointer;
|
||
transition: all .15s;
|
||
}
|
||
.btn-primary { background: var(--cyan); color: var(--ink) }
|
||
.btn-primary:hover { background: #1be5ff }
|
||
.btn-outline { background: var(--white); color: var(--ink); border: 1.5px solid var(--gray-200) }
|
||
.btn-outline:hover { border-color: var(--gray-500) }
|
||
.btn-sm { padding: 6px 14px; font-size: 13px }
|
||
.btn-danger { background: var(--red-bg); color: var(--red) }
|
||
.btn-danger:hover { background: #FEE2E2 }
|
||
|
||
/* COMPANIES GRID */
|
||
.companies-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
.company-card {
|
||
background: var(--white); border-radius: var(--radius);
|
||
padding: 24px; box-shadow: var(--shadow);
|
||
transition: box-shadow .2s; cursor: pointer;
|
||
border: 2px solid transparent;
|
||
position: relative;
|
||
}
|
||
.company-card:hover { box-shadow: var(--shadow-lg) }
|
||
.company-card.selected { border-color: var(--cyan) }
|
||
.company-card .company-name {
|
||
font-size: 17px; font-weight: 700; margin-bottom: 4px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.company-card .company-avatar {
|
||
width: 40px; height: 40px; border-radius: 10px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 18px; font-weight: 700; color: var(--white);
|
||
flex-shrink: 0;
|
||
}
|
||
.company-avatar.g1 { background: var(--indigo) }
|
||
.company-avatar.g2 { background: #0891B2 }
|
||
.company-avatar.g3 { background: #D946EF }
|
||
.company-avatar.g4 { background: #EA580C }
|
||
.company-avatar.g5 { background: #16A34A }
|
||
|
||
.company-card .task-counts {
|
||
display: flex; gap: 12px; margin-top: 12px; font-size: 13px;
|
||
}
|
||
.company-card .task-counts span { display: flex; align-items: center; gap: 4px }
|
||
.company-card .badge {
|
||
display: inline-block; padding: 3px 10px; border-radius: 20px;
|
||
font-size: 12px; font-weight: 600;
|
||
}
|
||
.company-card .actions { margin-top: 16px; display: flex; gap: 8px }
|
||
|
||
/* TASK TABLE */
|
||
.task-table-wrap { overflow-x: auto }
|
||
.task-table {
|
||
width: 100%; border-collapse: collapse;
|
||
background: var(--white); border-radius: var(--radius);
|
||
box-shadow: var(--shadow); overflow: hidden;
|
||
}
|
||
.task-table th {
|
||
text-align: left; padding: 14px 16px;
|
||
font-size: 12px; font-weight: 700; color: var(--gray-500);
|
||
text-transform: uppercase; letter-spacing: .5px;
|
||
border-bottom: 1px solid var(--gray-200);
|
||
background: var(--gray-100);
|
||
white-space: nowrap;
|
||
}
|
||
.task-table td {
|
||
padding: 14px 16px; font-size: 14px;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
white-space: nowrap;
|
||
}
|
||
.task-table tr:hover td { background: var(--cyan-50) }
|
||
.task-table tr:last-child td { border-bottom: none }
|
||
|
||
.status-pill {
|
||
display: inline-block; padding: 4px 12px; border-radius: 20px;
|
||
font-size: 12px; font-weight: 600;
|
||
}
|
||
.status-pending { background: var(--amber-bg); color: var(--amber) }
|
||
.status-progress { background: var(--indigo-bg); color: var(--indigo) }
|
||
.status-done { background: var(--green-bg); color: var(--green) }
|
||
.status-overdue { background: var(--red-bg); color: var(--red) }
|
||
|
||
.priority-dot {
|
||
width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px;
|
||
}
|
||
.priority-high { background: var(--red) }
|
||
.priority-mid { background: var(--amber) }
|
||
.priority-low { background: var(--gray-200) }
|
||
|
||
/* FAQ */
|
||
.faq-item {
|
||
background: var(--white); border-radius: var(--radius);
|
||
box-shadow: var(--shadow); margin-bottom: 8px; overflow: hidden;
|
||
}
|
||
.faq-q {
|
||
padding: 18px 24px; cursor: pointer;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
font-weight: 600; font-size: 16px;
|
||
user-select: none;
|
||
}
|
||
.faq-q:hover { background: var(--gray-100) }
|
||
.faq-q .arrow { transition: transform .2s; font-size: 12px; color: var(--gray-500) }
|
||
.faq-item.open .faq-q .arrow { transform: rotate(180deg) }
|
||
.faq-a {
|
||
padding: 0 24px 18px; font-size: 15px; color: var(--gray-500);
|
||
line-height: 1.7; display: none;
|
||
}
|
||
.faq-item.open .faq-a { display: block }
|
||
|
||
/* MODAL */
|
||
.modal-overlay {
|
||
position: fixed; inset: 0; z-index: 200;
|
||
background: rgba(15,18,24,.5);
|
||
backdrop-filter: blur(4px);
|
||
display: flex; align-items: center; justify-content: center;
|
||
display: none;
|
||
}
|
||
.modal-overlay.open { display: flex }
|
||
.modal {
|
||
background: var(--white); border-radius: 16px;
|
||
padding: 32px; max-width: 500px; width: 90%;
|
||
box-shadow: var(--shadow-lg); max-height: 85vh; overflow-y: auto;
|
||
}
|
||
.modal h3 { font-size: 20px; margin-bottom: 20px }
|
||
.modal label {
|
||
display: block; font-size: 13px; font-weight: 600;
|
||
color: var(--gray-500); margin-bottom: 4px; margin-top: 16px;
|
||
}
|
||
.modal input, .modal select, .modal textarea {
|
||
width: 100%; padding: 10px 14px; border: 1px solid var(--gray-200);
|
||
border-radius: 8px; font-size: 14px; outline: none; font-family: inherit;
|
||
}
|
||
.modal input:focus, .modal select:focus, .modal textarea:focus { border-color: var(--cyan) }
|
||
.modal textarea { resize: vertical; min-height: 80px }
|
||
.modal .btn-row { display: flex; gap: 10px; margin-top: 24px; justify-content: flex-end }
|
||
|
||
/* EMPTY */
|
||
.empty { text-align: center; padding: 60px 20px; color: var(--gray-500) }
|
||
.empty .icon { font-size: 48px; margin-bottom: 12px }
|
||
|
||
/* FOOTER */
|
||
.footer {
|
||
text-align: center; padding: 32px 24px;
|
||
color: var(--gray-500); font-size: 13px;
|
||
border-top: 1px solid var(--gray-200); margin-top: 64px;
|
||
}
|
||
|
||
/* TABS */
|
||
.tabs { display: flex; gap: 4px; margin-bottom: 24px; background: var(--white); border-radius: 10px; padding: 4px; box-shadow: var(--shadow); width: fit-content }
|
||
.tab-btn {
|
||
padding: 10px 20px; border-radius: 8px; border: none;
|
||
background: transparent; font-size: 14px; font-weight: 600;
|
||
cursor: pointer; color: var(--gray-500); transition: all .15s;
|
||
}
|
||
.tab-btn.active { background: var(--ink); color: var(--white) }
|
||
.tab-content { display: none }
|
||
.tab-content.active { display: block }
|
||
|
||
@media (max-width: 768px) {
|
||
.stats { grid-template-columns: repeat(2, 1fr) }
|
||
.hero h1 { font-size: 28px }
|
||
.companies-grid { grid-template-columns: 1fr }
|
||
.nav-links { display: none }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.stats { grid-template-columns: 1fr }
|
||
.section-header { flex-direction: column; align-items: flex-start }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="nav">
|
||
<div class="container">
|
||
<a href="#" class="nav-logo">
|
||
<div class="dot">📋</div>
|
||
HR Ассистент
|
||
</a>
|
||
<div class="nav-links">
|
||
<a href="#companies" class="active">Компании</a>
|
||
<a href="#tasks">Поручения</a>
|
||
<a href="#faq">HR FAQ</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<section class="hero">
|
||
<div class="container">
|
||
<h1>Контроль поручений портфельным компаниям</h1>
|
||
<p class="sub">Отслеживайте задачи по всем портфельным компаниям АО «Самрук-Қазына» в одном окне</p>
|
||
<div class="stats" id="stats"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="container">
|
||
|
||
<!-- COMPANIES SECTION -->
|
||
<div id="companies">
|
||
<div class="section-header">
|
||
<h2>Портфельные компании</h2>
|
||
<div class="search-bar">
|
||
<input type="text" id="companySearch" placeholder="Поиск по названию..." oninput="renderCompanies()">
|
||
<button class="btn btn-primary" onclick="openModal('company')">+ Добавить компанию</button>
|
||
<button class="btn btn-outline btn-sm" onclick="if(confirm('Сбросить все данные к стандартным?')){localStorage.clear();location.reload()}" title="Сбросить данные">↺ Сброс</button>
|
||
</div>
|
||
</div>
|
||
<div class="companies-grid" id="companiesGrid"></div>
|
||
</div>
|
||
|
||
<!-- TASKS SECTION -->
|
||
<div id="tasks" style="margin-top:56px">
|
||
<div class="section-header">
|
||
<h2>Поручения</h2>
|
||
<div class="search-bar">
|
||
<input type="text" id="taskSearch" placeholder="Поиск поручения..." oninput="renderTasks()">
|
||
<select id="taskFilterCompany" onchange="renderTasks()">
|
||
<option value="">Все компании</option>
|
||
</select>
|
||
<select id="taskFilterStatus" onchange="renderTasks()">
|
||
<option value="">Все статусы</option>
|
||
<option value="pending">Ожидает</option>
|
||
<option value="progress">В работе</option>
|
||
<option value="done">Готово</option>
|
||
<option value="overdue">Просрочено</option>
|
||
</select>
|
||
<button class="btn btn-primary" onclick="openModal('task')">+ Добавить поручение</button>
|
||
</div>
|
||
</div>
|
||
<div class="task-table-wrap" id="taskTableWrap"></div>
|
||
</div>
|
||
|
||
<!-- FAQ -->
|
||
<div id="faq" style="margin-top:56px">
|
||
<div class="section-header"><h2>HR FAQ</h2></div>
|
||
<div id="faqList"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<footer class="footer">
|
||
HR Ассистент — инструмент для контроля поручений © 2026
|
||
</footer>
|
||
|
||
<!-- MODALS -->
|
||
<div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
|
||
<div class="modal" id="modalContent"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// ====== DATA MODEL (localStorage) ======
|
||
|
||
const defaultCompanies = [
|
||
{ id: 'c1', name: 'АО «НК «КазМунайГаз»', color: 'g1' },
|
||
{ id: 'c2', name: 'АО «НК «ҚТЖ»', color: 'g2' },
|
||
{ id: 'c3', name: 'АО «НАК «Казатомпром»', color: 'g3' },
|
||
{ id: 'c4', name: 'АО «Самрук-Энерго»', color: 'g4' },
|
||
{ id: 'c5', name: 'АО «KEGOC»', color: 'g5' },
|
||
{ id: 'c6', name: 'АО «Казпочта»', color: 'g1' },
|
||
{ id: 'c7', name: 'АО «Эйр Астана»', color: 'g2' },
|
||
{ id: 'c8', name: 'АО «НК «QazaqGaz»', color: 'g3' },
|
||
{ id: 'c9', name: 'АО «Тау-Кен Самрук»', color: 'g4' },
|
||
{ id: 'c10', name: 'АО «Казахтелеком»', color: 'g5' },
|
||
{ id: 'c11', name: 'АО «НК «Kazakhstan Engineering»', color: 'g1' },
|
||
{ id: 'c12', name: 'ТОО «ОХК»', color: 'g2' },
|
||
{ id: 'c13', name: 'АО «КазАвтоЖол»', color: 'g3' },
|
||
{ id: 'c14', name: 'АО «Qazaq Air»', color: 'g4' },
|
||
{ id: 'c15', name: 'ТОО «Samruk-Kazyna Construction»', color: 'g5' },
|
||
{ id: 'c16', name: 'ТОО «Samruk-Kazyna Trust»', color: 'g1' },
|
||
{ id: 'c17', name: 'АО «Самрук-Қазына Контракт»', color: 'g2' },
|
||
{ id: 'c18', name: 'АО «KTZ Express»', color: 'g3' },
|
||
];
|
||
|
||
const defaultTasks = [
|
||
{ id: 't1', companyId: 'c1', title: 'Подготовить отчёт по охране труда за Q1', status: 'pending', deadline: '2026-06-20', priority: 'high' },
|
||
{ id: 't2', companyId: 'c2', title: 'Обновить штатное расписание', status: 'progress', deadline: '2026-06-25', priority: 'mid' },
|
||
{ id: 't3', companyId: 'c3', title: 'Согласовать KPI руководителей', status: 'done', deadline: '2026-06-01', priority: 'high' },
|
||
{ id: 't4', companyId: 'c1', title: 'Провести аудит условий труда', status: 'overdue', deadline: '2026-05-15', priority: 'high' },
|
||
{ id: 't5', companyId: 'c4', title: 'Организовать обучение по ТБ', status: 'pending', deadline: '2026-07-01', priority: 'mid' },
|
||
{ id: 't6', companyId: 'c5', title: 'Собрать данные по текучести кадров', status: 'progress', deadline: '2026-06-18', priority: 'low' },
|
||
{ id: 't7', companyId: 'c6', title: 'Проверить трудовые договоры', status: 'pending', deadline: '2026-06-30', priority: 'mid' },
|
||
{ id: 't8', companyId: 'c2', title: 'Запустить программу наставничества', status: 'progress', deadline: '2026-06-10', priority: 'high' },
|
||
{ id: 't9', companyId: 'c8', title: 'Внедрить систему оценки персонала', status: 'overdue', deadline: '2026-04-20', priority: 'high' },
|
||
{ id: 't10', companyId: 'c9', title: 'Актуализировать должностные инструкции', status: 'pending', deadline: '2026-07-10', priority: 'low' },
|
||
];
|
||
|
||
const DATA_VERSION = 'sk-v2';
|
||
|
||
function loadData() {
|
||
const ver = localStorage.getItem('hr-version');
|
||
if (ver !== DATA_VERSION) {
|
||
localStorage.clear();
|
||
localStorage.setItem('hr-version', DATA_VERSION);
|
||
return { companies: defaultCompanies, tasks: defaultTasks };
|
||
}
|
||
const c = localStorage.getItem('hr-companies');
|
||
const t = localStorage.getItem('hr-tasks');
|
||
return {
|
||
companies: c ? JSON.parse(c) : defaultCompanies,
|
||
tasks: t ? JSON.parse(t) : defaultTasks,
|
||
};
|
||
}
|
||
|
||
function saveData(companies, tasks) {
|
||
localStorage.setItem('hr-companies', JSON.stringify(companies));
|
||
localStorage.setItem('hr-tasks', JSON.stringify(tasks));
|
||
}
|
||
|
||
let { companies, tasks } = loadData();
|
||
|
||
// ====== HELPERS ======
|
||
|
||
function getCompany(id) { return companies.find(c => c.id === id) }
|
||
function taskCount(cid, status) { return tasks.filter(t => t.companyId === cid && t.status === status).length }
|
||
|
||
function totalFor(status) { return tasks.filter(t => t.status === status).length }
|
||
|
||
function openModal(type) {
|
||
const overlay = document.getElementById('modalOverlay');
|
||
const content = document.getElementById('modalContent');
|
||
|
||
if (type === 'company') {
|
||
content.innerHTML = `
|
||
<h3>Добавить компанию</h3>
|
||
<label>Название компании</label>
|
||
<input type="text" id="mCompanyName" placeholder="ТОО «...»">
|
||
<div class="btn-row">
|
||
<button class="btn btn-outline" onclick="closeModal()">Отмена</button>
|
||
<button class="btn btn-primary" onclick="addCompany()">Добавить</button>
|
||
</div>
|
||
`;
|
||
} else {
|
||
content.innerHTML = `
|
||
<h3>Добавить поручение</h3>
|
||
<label>Компания</label>
|
||
<select id="mTaskCompany">
|
||
${companies.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('')}
|
||
</select>
|
||
<label>Описание поручения</label>
|
||
<input type="text" id="mTaskTitle" placeholder="Что нужно сделать?">
|
||
<label>Дедлайн</label>
|
||
<input type="date" id="mTaskDeadline">
|
||
<label>Приоритет</label>
|
||
<select id="mTaskPriority">
|
||
<option value="high">Высокий</option>
|
||
<option value="mid" selected>Средний</option>
|
||
<option value="low">Низкий</option>
|
||
</select>
|
||
<label>Статус</label>
|
||
<select id="mTaskStatus">
|
||
<option value="pending">Ожидает</option>
|
||
<option value="progress">В работе</option>
|
||
<option value="done">Готово</option>
|
||
<option value="overdue">Просрочено</option>
|
||
</select>
|
||
<div class="btn-row">
|
||
<button class="btn btn-outline" onclick="closeModal()">Отмена</button>
|
||
<button class="btn btn-primary" onclick="addTask()">Добавить</button>
|
||
</div>
|
||
`;
|
||
}
|
||
overlay.classList.add('open');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modalOverlay').classList.remove('open');
|
||
}
|
||
|
||
function esc(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
function uid() { return 'x' + Date.now() + Math.random().toString(36).slice(2, 8) }
|
||
|
||
function addCompany() {
|
||
const name = document.getElementById('mCompanyName').value.trim();
|
||
if (!name) return;
|
||
const colors = ['g1','g2','g3','g4','g5'];
|
||
companies.push({ id: uid(), name, color: colors[Math.floor(Math.random() * colors.length)] });
|
||
saveData(companies, tasks);
|
||
closeModal();
|
||
renderAll();
|
||
}
|
||
|
||
function addTask() {
|
||
const companyId = document.getElementById('mTaskCompany').value;
|
||
const title = document.getElementById('mTaskTitle').value.trim();
|
||
const deadline = document.getElementById('mTaskDeadline').value;
|
||
const priority = document.getElementById('mTaskPriority').value;
|
||
const status = document.getElementById('mTaskStatus').value;
|
||
if (!title) return;
|
||
tasks.push({ id: uid(), companyId, title, deadline, priority, status });
|
||
saveData(companies, tasks);
|
||
closeModal();
|
||
renderAll();
|
||
}
|
||
|
||
function deleteTask(id) {
|
||
tasks = tasks.filter(t => t.id !== id);
|
||
saveData(companies, tasks);
|
||
renderAll();
|
||
}
|
||
|
||
function deleteCompany(id) {
|
||
if (!confirm('Удалить компанию и все её поручения?')) return;
|
||
companies = companies.filter(c => c.id !== id);
|
||
tasks = tasks.filter(t => t.companyId !== id);
|
||
saveData(companies, tasks);
|
||
renderAll();
|
||
}
|
||
|
||
function changeTaskStatus(id, status) {
|
||
const t = tasks.find(t => t.id === id);
|
||
if (t) { t.status = status; saveData(companies, tasks); renderAll(); }
|
||
}
|
||
|
||
// ====== RENDER ======
|
||
|
||
const priorityLabels = { high: 'Высокий', mid: 'Средний', low: 'Низкий' };
|
||
const statusLabels = { pending: 'Ожидает', progress: 'В работе', done: 'Готово', overdue: 'Просрочено' };
|
||
|
||
function renderStats() {
|
||
const totalCompanies = companies.length;
|
||
const active = totalFor('pending') + totalFor('progress');
|
||
const done = totalFor('done');
|
||
const overdue = totalFor('overdue');
|
||
|
||
document.getElementById('stats').innerHTML = `
|
||
<div class="stat-card">
|
||
<div class="label">Компаний</div>
|
||
<div class="value" style="color:var(--indigo)">${totalCompanies}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Активных поручений</div>
|
||
<div class="value" style="color:var(--amber)">${active}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Выполнено</div>
|
||
<div class="value" style="color:var(--green)">${done}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Просрочено</div>
|
||
<div class="value" style="color:var(--red)">${overdue}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderCompanies() {
|
||
const search = (document.getElementById('companySearch')?.value || '').toLowerCase();
|
||
const filtered = companies.filter(c => c.name.toLowerCase().includes(search));
|
||
|
||
const grid = document.getElementById('companiesGrid');
|
||
if (filtered.length === 0) {
|
||
grid.innerHTML = '<div class="empty"><div class="icon">🏢</div><p>Компании не найдены</p></div>';
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = filtered.map(c => {
|
||
const pending = taskCount(c.id, 'pending');
|
||
const progress = taskCount(c.id, 'progress');
|
||
const done = taskCount(c.id, 'done');
|
||
const overdue = taskCount(c.id, 'overdue');
|
||
return `
|
||
<div class="company-card" onclick="document.getElementById('taskFilterCompany').value='${c.id}';document.getElementById('tasks').scrollIntoView({behavior:'smooth'});renderTasks()">
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<div style="display:flex;align-items:center;gap:12px;min-width:0">
|
||
<div class="company-avatar ${c.color}">${c.name.charAt(0)}</div>
|
||
<div class="company-name" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(c.name)}</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();deleteCompany('${c.id}')" title="Удалить">🗑</button>
|
||
</div>
|
||
<div class="task-counts">
|
||
<span title="Ожидает"><span class="dot-indicator" style="background:var(--amber)"></span>${pending} ожидает</span>
|
||
<span title="В работе"><span class="dot-indicator" style="background:var(--indigo)"></span>${progress} в работе</span>
|
||
<span title="Готово"><span class="dot-indicator" style="background:var(--green)"></span>${done} готово</span>
|
||
<span title="Просрочено"><span class="dot-indicator" style="background:var(--red)"></span>${overdue} просрочено</span>
|
||
</div>
|
||
${overdue > 0 ? `<div style="margin-top:12px"><span class="badge" style="background:var(--red-bg);color:var(--red)">⚠️ Есть просроченные</span></div>` : ''}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderTasks() {
|
||
const searchText = (document.getElementById('taskSearch')?.value || '').toLowerCase();
|
||
const filterCompany = document.getElementById('taskFilterCompany')?.value || '';
|
||
const filterStatus = document.getElementById('taskFilterStatus')?.value || '';
|
||
|
||
let filtered = tasks;
|
||
if (searchText) filtered = filtered.filter(t => t.title.toLowerCase().includes(searchText));
|
||
if (filterCompany) filtered = filtered.filter(t => t.companyId === filterCompany);
|
||
if (filterStatus) filtered = filtered.filter(t => t.status === filterStatus);
|
||
|
||
// update company filter dropdown
|
||
const sel = document.getElementById('taskFilterCompany');
|
||
if (sel) {
|
||
const cur = sel.value;
|
||
sel.innerHTML = '<option value="">Все компании</option>' +
|
||
companies.map(c => `<option value="${c.id}" ${c.id === cur ? 'selected' : ''}>${esc(c.name)}</option>`).join('');
|
||
}
|
||
|
||
const wrap = document.getElementById('taskTableWrap');
|
||
if (filtered.length === 0) {
|
||
wrap.innerHTML = '<div class="empty"><div class="icon">📝</div><p>Поручений нет</p></div>';
|
||
return;
|
||
}
|
||
|
||
wrap.innerHTML = `
|
||
<table class="task-table">
|
||
<thead><tr>
|
||
<th>Компания</th>
|
||
<th>Поручение</th>
|
||
<th>Дедлайн</th>
|
||
<th>Приоритет</th>
|
||
<th>Статус</th>
|
||
<th></th>
|
||
</tr></thead>
|
||
<tbody>
|
||
${filtered.map(t => {
|
||
const c = getCompany(t.companyId);
|
||
const deadlineDisplay = t.deadline ? new Date(t.deadline).toLocaleDateString('ru-RU') : '—';
|
||
return `
|
||
<tr>
|
||
<td style="font-weight:600">${c ? esc(c.name) : '—'}</td>
|
||
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis">${esc(t.title)}</td>
|
||
<td>${deadlineDisplay}</td>
|
||
<td><span class="priority-dot priority-${t.priority}"></span>${priorityLabels[t.priority] || t.priority}</td>
|
||
<td>
|
||
<select class="status-pill status-${t.status}" style="border:none;cursor:pointer;font-family:inherit;font-weight:600;font-size:12px;padding:4px 12px;border-radius:20px;background:var(--${t.status === 'pending' ? 'amber' : t.status === 'progress' ? 'indigo' : t.status === 'done' ? 'green' : 'red'}-bg);color:var(--${t.status === 'pending' ? 'amber' : t.status === 'progress' ? 'indigo' : t.status === 'done' ? 'green' : 'red'})" onchange="changeTaskStatus('${t.id}', this.value)">
|
||
<option value="pending" ${t.status==='pending'?'selected':''}>Ожидает</option>
|
||
<option value="progress" ${t.status==='progress'?'selected':''}>В работе</option>
|
||
<option value="done" ${t.status==='done'?'selected':''}>Готово</option>
|
||
<option value="overdue" ${t.status==='overdue'?'selected':''}>Просрочено</option>
|
||
</select>
|
||
</td>
|
||
<td><button class="btn btn-sm btn-danger" onclick="deleteTask('${t.id}')">🗑</button></td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
// ====== FAQ ======
|
||
const faqData = [
|
||
{ q: 'Как оформить приказ о приёме на работу?', a: 'Приказ оформляется по форме Т-1 в течение 3 рабочих дней после подписания трудового договора. Обязательно ознакомить сотрудника под подпись.' },
|
||
{ q: 'Какие сроки уведомления о сокращении?', a: 'Работодатель обязан письменно уведомить сотрудника не менее чем за 1 месяц до расторжения трудового договора по сокращению штата (ст. 53 ТК РК).' },
|
||
{ q: 'Как рассчитать отпускные?', a: 'Средний дневной заработок = сумма зарплаты за 12 месяцев / количество рабочих дней. Умножаем на количество дней отпуска. Выплатить не позднее чем за 3 дня до начала.' },
|
||
{ q: 'Обязан ли работодатель проводить медосмотр?', a: 'Да, для сотрудников занятых на тяжёлых работах, с вредными/опасными условиями труда, а также для работников пищевой промышленности, медицины и образования — обязательно.' },
|
||
{ q: 'Как перевести сотрудника в другую дочернюю компанию?', a: 'Перевод оформляется через увольнение из одной компании и приём в другую, либо через трёхстороннее соглашение о переводе (ст. 42 ТК РК). В обоих случаях требуется письменное согласие сотрудника.' },
|
||
{ q: 'Что делать при несчастном случае на производстве?', a: 'Оказать первую помощь → вызвать скорую → сохранить место происшествия → создать комиссию по расследованию → уведомить госинспекцию труда в течение суток → оформить акт Н-1.' },
|
||
{ q: 'Можно ли установить испытательный срок для руководителя?', a: 'Да, до 3 месяцев. Для руководителей организаций и их заместителей, главных бухгалтеров — до 6 месяцев (ст. 36 ТК РК).' },
|
||
{ q: 'Как контролировать исполнение поручений дочерним компаниям?', a: 'Рекомендуется: 1) Письменная фиксация поручений с дедлайнами, 2) Еженедельный мониторинг статусов, 3) Единая система отчётности, 4) Регулярные статус-встречи с руководителями ДК.' },
|
||
];
|
||
|
||
function renderFAQ() {
|
||
document.getElementById('faqList').innerHTML = faqData.map((item, i) => `
|
||
<div class="faq-item">
|
||
<div class="faq-q" onclick="this.parentElement.classList.toggle('open')">
|
||
<span>${esc(item.q)}</span>
|
||
<span class="arrow">▼</span>
|
||
</div>
|
||
<div class="faq-a">${esc(item.a)}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// ====== RENDER ALL ======
|
||
function renderAll() {
|
||
renderStats();
|
||
renderCompanies();
|
||
renderTasks();
|
||
}
|
||
|
||
renderAll();
|
||
renderFAQ();
|
||
|
||
// Nav smooth scroll
|
||
document.querySelectorAll('.nav-links a').forEach(a => {
|
||
a.addEventListener('click', e => {
|
||
e.preventDefault();
|
||
const target = document.querySelector(a.getAttribute('href'));
|
||
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
||
document.querySelectorAll('.nav-links a').forEach(x => x.classList.remove('active'));
|
||
a.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Keyboard shortcut
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') closeModal();
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||
e.preventDefault();
|
||
document.getElementById('taskSearch').focus();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|