KT AI DS: прототип согласования отпусков

This commit is contained in:
tore 2026-09-08 21:11:59 +00:00
parent c6ac026233
commit fecfe48edf

View File

@ -4,113 +4,67 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Согласование отпусков</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<link rel="stylesheet" href="./design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
<link rel="stylesheet" href="./design-system/kt-ai-page.css">
<style>
.kt-ai-app { min-height: 100vh; background: var(--kt-ai-bg); color: var(--kt-ai-fg); }
.app-layout { display: flex; min-height: 100vh; background: var(--kt-ai-bg); }
.sidebar { position: fixed; left: 0; top: 0; bottom: 0; width: 260px; background: var(--kt-ai-bg-soft); border-right: 1px solid var(--kt-ai-border); padding: var(--kt-ai-space-4); display: flex; flex-direction: column; }
.sidebar-brand { display: flex; align-items: center; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-6); }
.sidebar-brand svg { width: 22px; height: 22px; color: var(--kt-ai-primary); }
.sidebar-brand span { font-weight: var(--kt-ai-weight-semibold); font-size: 15px; }
.sidebar-nav { display: flex; flex-direction: column; gap: var(--kt-ai-space-1); }
.nav-item { display: flex; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-2) var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-lg); color: var(--kt-ai-fg-muted); text-decoration: none; font-size: 14px; transition: all 0.15s; }
.nav-item:hover { background: var(--kt-ai-bg-hover); color: var(--kt-ai-fg); }
.nav-item.active { background: var(--kt-ai-bg-active); color: var(--kt-ai-fg); font-weight: var(--kt-ai-weight-medium); }
.nav-item svg { width: 18px; height: 18px; }
.nav-label { font-size: 11px; color: var(--kt-ai-fg-faint); text-transform: uppercase; letter-spacing: 0.05em; margin: var(--kt-ai-space-4) 0 var(--kt-ai-space-2); font-weight: var(--kt-ai-weight-medium); }
.main-content { margin-left: 260px; padding: var(--kt-ai-space-6) var(--kt-ai-space-8); }
.main-content { margin-left: 260px; padding: var(--kt-ai-space-6) var(--kt-ai-space-8); flex: 1; }
.topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--kt-ai-space-6); }
.topbar-title h1 { font-size: 20px; font-weight: var(--kt-ai-weight-semibold); margin: 0; }
.topbar-title h1 { font-size: 20px; font-weight: var(--kt-ai-weight-semibold); margin: 0; color: var(--kt-ai-fg); }
.topbar-title p { font-size: 13px; color: var(--kt-ai-fg-muted); margin: 4px 0 0; }
.topbar-actions { display: flex; align-items: center; gap: var(--kt-ai-space-3); }
.kpi-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-5); }
.kpi-value { font-size: 28px; font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); line-height: 1; margin-bottom: var(--kt-ai-space-2); }
.kpi-label { font-size: 13px; color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-1); }
.kpi-hint { font-size: 11px; color: var(--kt-ai-fg-faint); }
.table-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-2xl); overflow: hidden; }
.table-header { display: grid; grid-template-columns: 2fr 1.5fr 1.2fr 1fr 1fr 120px; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-divider); font-size: 12px; font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); }
.table-container { border-radius: var(--kt-ai-radius-2xl); overflow: hidden; }
.table-header { display: grid; grid-template-columns: 2fr 1.5fr 1.2fr 1fr 1fr 120px; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-divider); font-size: 12px; font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); background: var(--kt-ai-bg-soft); }
.table-row { display: grid; grid-template-columns: 2fr 1.5fr 1.2fr 1fr 1fr 120px; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-divider); align-items: center; font-size: 13px; transition: background 0.12s; }
.table-row:hover { background: var(--kt-ai-bg-hover); }
.table-row:last-child { border-bottom: none; }
.entity-name { font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
.entity-meta { font-size: 11px; color: var(--kt-ai-fg-faint); margin-top: 2px; }
.text-right { text-align: right; }
.status-pill { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: 11px; font-weight: var(--kt-ai-weight-medium); }
.status-new { background: #fef3c7; color: #92400e; }
.status-approved { background: #d1fae5; color: #065f46; }
.status-rejected { background: #fee2e2; color: #991b1b; }
.status-pending { background: #e0e7ff; color: #3730a3; }
.btn-group { display: flex; gap: var(--kt-ai-space-2); }
.btn-sm { padding: 6px 12px; font-size: 12px; border-radius: var(--kt-ai-radius-lg); border: none; cursor: pointer; font-weight: var(--kt-ai-weight-medium); transition: all 0.12s; }
.btn-primary { background: var(--kt-ai-primary); color: white; }
.btn-primary:hover { opacity: 0.9; }
.btn-ghost { background: transparent; color: var(--kt-ai-fg-muted); border: 1px solid var(--kt-ai-border); }
.btn-ghost:hover { background: var(--kt-ai-bg-hover); color: var(--kt-ai-fg); }
.btn-danger { background: #fee2e2; color: #991b1b; }
.btn-danger:hover { background: #fecaca; }
.filter-bar { display: flex; gap: var(--kt-ai-space-2); margin-bottom: var(--kt-ai-space-5); flex-wrap: wrap; }
.filter-chip { padding: 6px 14px; border-radius: var(--kt-ai-radius-full); font-size: 13px; border: 1px solid var(--kt-ai-border); background: transparent; color: var(--kt-ai-fg-muted); cursor: pointer; transition: all 0.12s; }
.filter-chip:hover { background: var(--kt-ai-bg-hover); }
.filter-chip { padding: 6px 14px; border-radius: var(--kt-ai-radius-full); font-size: 13px; border: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg); color: var(--kt-ai-fg-muted); cursor: pointer; transition: all 0.12s; }
.filter-chip:hover { background: var(--kt-ai-bg-hover); color: var(--kt-ai-fg); }
.filter-chip.active { background: var(--kt-ai-primary); color: white; border-color: var(--kt-ai-primary); }
.empty-state { text-align: center; padding: var(--kt-ai-space-12) var(--kt-ai-space-6); color: var(--kt-ai-fg-muted); }
.empty-state svg { width: 48px; height: 48px; margin-bottom: var(--kt-ai-space-4); opacity: 0.5; }
.empty-state h3 { font-size: 16px; font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); color: var(--kt-ai-fg); }
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; z-index: 100; }
.drawer-overlay.open { display: block; }
.drawer { position: fixed; right: -480px; top: 0; bottom: 0; width: 480px; background: var(--kt-ai-bg); border-left: 1px solid var(--kt-ai-border); padding: var(--kt-ai-space-6); transition: right 0.25s; z-index: 101; overflow-y: auto; }
.drawer.open { right: 0; }
.drawer-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: var(--kt-ai-space-5); }
.drawer-header h2 { font-size: 18px; font-weight: var(--kt-ai-weight-semibold); margin: 0; }
.drawer-close { background: none; border: none; font-size: 24px; color: var(--kt-ai-fg-muted); cursor: pointer; padding: 0; line-height: 1; }
.drawer-close:hover { color: var(--kt-ai-fg); }
.drawer-section { margin-bottom: var(--kt-ai-space-5); }
.drawer-section h3 { font-size: 13px; font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--kt-ai-space-3); }
.info-row { display: flex; justify-content: space-between; padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: 14px; }
.info-row:last-child { border-bottom: none; }
.drawer-section { margin-bottom: var(--kt-ai-space-5); padding-bottom: var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-divider); }
.drawer-section:last-child { border-bottom: none; }
.info-row { display: flex; justify-content: space-between; padding: var(--kt-ai-space-3) 0; font-size: 14px; }
.info-label { color: var(--kt-ai-fg-muted); }
.info-value { font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
.rule-check { display: flex; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-3) 0; }
.rule-check { display: flex; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-3) 0; font-size: 14px; }
.rule-check svg { width: 18px; height: 18px; flex-shrink: 0; }
.rule-check.ok { color: var(--kt-ai-fg); }
.rule-check.fail { color: #991b1b; }
.rule-check svg.ok { color: #059669; }
.rule-check svg.fail { color: #dc2626; }
.toast { position: fixed; bottom: var(--kt-ai-space-6); right: var(--kt-ai-space-6); background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); box-shadow: 0 10px 40px rgba(0,0,0,0.15); display: none; z-index: 200; max-width: 400px; }
.toast.show { display: block; animation: slideIn 0.25s ease; }
.toast.success { border-left: 4px solid #059669; }
.toast.error { border-left: 4px solid #dc2626; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
</style>
</head>
<body class="kt-ai-app">
<body>
<div class="app-layout">
<aside class="sidebar">
<div class="sidebar-brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-6)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:22px;height:22px;color:var(--kt-ai-primary)">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<span>Согласование отпусков</span>
<strong style="font-size:15px;color:var(--kt-ai-fg)">Согласование отпусков</strong>
</div>
<nav class="sidebar-nav">
<a href="#" class="nav-item active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</svg>
<nav style="display:flex;flex-direction:column;gap:var(--kt-ai-space-1)">
<a href="#" class="kt-ai-btn" data-variant="ghost" style="justify-content:flex-start">
<svg class="kt-icon" style="width:18px;height:18px;margin-right:var(--kt-ai-space-3)"><use href="#layout-grid"/></svg>
<span>Заявки</span>
</a>
<a href="#" class="nav-item" onclick="showGraph(); return false;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 20V10M12 20V4M6 20v-6"/>
</svg>
<a href="#" class="kt-ai-btn" data-variant="ghost" style="justify-content:flex-start" onclick="showGraph();return false;">
<svg class="kt-icon" style="width:18px;height:18px;margin-right:var(--kt-ai-space-3)"><use href="#bar-chart-3"/></svg>
<span>График отпусков</span>
</a>
<div class="nav-label">Сотрудники</div>
<div id="team-list" style="display:flex;flex-direction:column;gap:2px;"></div>
<div style="font-size:11px;color:var(--kt-ai-fg-faint);text-transform:uppercase;letter-spacing:0.05em;margin:var(--kt-ai-space-4) 0 var(--kt-ai-space-2);font-weight:var(--kt-ai-weight-medium)">Сотрудники</div>
<div id="team-list"></div>
</nav>
</aside>
@ -120,38 +74,38 @@
<h1>Заявки на отпуск</h1>
<p>Согласование и учёт ежегодных отпусков отдела</p>
</div>
<div class="topbar-actions">
<div style="display:flex;gap:var(--kt-ai-space-3)">
<button class="kt-ai-btn" data-variant="ghost" onclick="resetData()">
<svg class="kt-icon" style="width:16px;height:16px;margin-right:6px"><use href="#rotateCcw"/></svg>
<svg class="kt-icon" style="width:16px;height:16px;margin-right:6px"><use href="#rotate-ccw"/></svg>
Сбросить
</button>
<div class="kt-ai-btn" data-variant="primary" onclick="createDemoRequest()">
<button class="kt-ai-btn" data-variant="primary" onclick="createDemoRequest()">
<svg class="kt-icon" style="width:16px;height:16px;margin-right:6px"><use href="#plus"/></svg>
Новая заявка
</div>
</button>
</div>
</div>
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-value" id="kpi-total">0</div>
<div class="kpi-label">Всего заявок</div>
<div class="kpi-hint">за 2026 год</div>
<div class="kt-ai-kpi-strip">
<div class="kt-ai-kpi">
<div class="value" id="kpi-total">0</div>
<div class="label">Всего заявок</div>
<div class="hint">за 2026 год</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-pending">0</div>
<div class="kpi-label">Требуют решения</div>
<div class="kpi-hint">ожидают согласования</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-pending">0</div>
<div class="label">Требуют решения</div>
<div class="hint">ожидают согласования</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-approved">0</div>
<div class="kpi-label">Согласовано</div>
<div class="kpi-hint">в этом месяце</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-approved">0</div>
<div class="label">Согласовано</div>
<div class="hint">в этом месяце</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-conflicts">0</div>
<div class="kpi-label">Конфликтов</div>
<div class="kpi-hint">нарушений правил</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-conflicts">0</div>
<div class="label">Конфликтов</div>
<div class="hint">нарушений правил</div>
</div>
</div>
@ -163,24 +117,27 @@
<button class="filter-chip" data-filter="rejected">Отклонено</button>
</div>
<div class="table-wrap">
<div class="kt-ai-card table-container">
<div class="table-header">
<div>Сотрудник</div>
<div>Даты</div>
<div>Тип</div>
<div>Остаток</div>
<div class="text-right">Остаток</div>
<div>Статус</div>
<div class="text-right">Действия</div>
</div>
<div id="requests-table"></div>
</div>
</main>
</div>
<div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
<div class="drawer" id="drawer">
<div class="drawer-header">
<h2 id="drawer-title">Заявка</h2>
<button class="drawer-close" onclick="closeDrawer()">×</button>
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:var(--kt-ai-space-5)">
<h2 style="font-size:18px;font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg);margin:0" id="drawer-title">Заявка</h2>
<button class="kt-ai-btn" data-variant="ghost" onclick="closeDrawer()" style="padding:var(--kt-ai-space-1)">
<svg class="kt-icon" style="width:20px;height:20px"><use href="#x"/></svg>
</button>
</div>
<div id="drawer-content"></div>
</div>
@ -261,8 +218,8 @@ function resetData() {
function renderTeamList() {
const container = document.getElementById('team-list');
container.innerHTML = employees.slice(0, 5).map(emp => `
<div style="display:flex;align-items:center;gap:8px;padding:6px 8px;font-size:13px;color:var(--kt-ai-fg-muted)">
<div style="width:24px;height:24px;border-radius:50%;background:var(--kt-ai-primary);color:white;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600">
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-2);padding:var(--kt-ai-space-2);font-size:13px;color:var(--kt-ai-fg-muted)">
<div style="width:24px;height:24px;border-radius:var(--kt-ai-radius-full);background:var(--kt-ai-primary);color:white;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600">
${emp.name.split(' ').map(n=>n[0]).join('').slice(0,2)}
</div>
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${emp.name.split(' ')[1] || emp.name}</span>
@ -270,6 +227,16 @@ function renderTeamList() {
`).join('');
}
function getStatusVariant(status) {
const variants = {
new: { bg: 'var(--kt-ai-status-warn-bg)', color: 'var(--kt-ai-status-warn-fg)', label: 'Новая' },
pending: { bg: 'var(--kt-ai-status-info-bg)', color: 'var(--kt-ai-status-info-fg)', label: 'На согласовании' },
approved: { bg: 'var(--kt-ai-status-ok-bg)', color: 'var(--kt-ai-status-ok-fg)', label: 'Согласовано' },
rejected: { bg: 'var(--kt-ai-status-risk-bg)', color: 'var(--kt-ai-status-risk-fg)', label: 'Отклонено' }
};
return variants[status] || variants.new;
}
function renderRequests() {
const container = document.getElementById('requests-table');
let filtered = [...requests];
@ -284,11 +251,9 @@ function renderRequests() {
if (filtered.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<h3>Нет заявок</h3>
<p>Заявок с выбранным статусом пока нет</p>
<svg class="kt-icon" style="width:48px;height:48px;color:var(--kt-ai-fg-muted);opacity:0.5;margin-bottom:var(--kt-ai-space-4)"><use href="#inbox"/></svg>
<h3 style="font-size:16px;font-weight:var(--kt-ai-weight-medium);color:var(--kt-ai-fg);margin-bottom:var(--kt-ai-space-2)">Нет заявок</h3>
<p style="font-size:13px;color:var(--kt-ai-fg-muted)">Заявок с выбранным статусом пока нет</p>
</div>
`;
return;
@ -296,26 +261,29 @@ function renderRequests() {
container.innerHTML = filtered.map(req => {
const emp = employees.find(e => e.id === req.employeeId);
const statusClass = `status-${req.status}`;
const statusLabels = { new: 'Новая', pending: 'На согласовании', approved: 'Согласовано', rejected: 'Отклонено' };
const variant = getStatusVariant(req.status);
return `
<div class="table-row" onclick="openDrawer(${req.id})" style="cursor:pointer">
<div>
<div class="entity-name">${req.employeeName}</div>
<div class="entity-meta">${emp ? emp.position : ''}</div>
<div style="font-weight:var(--kt-ai-weight-medium);color:var(--kt-ai-fg)">${req.employeeName}</div>
<div style="font-size:11px;color:var(--kt-ai-fg-faint);margin-top:2px">${emp ? emp.position : ''}</div>
</div>
<div>${formatDateRange(req.startDate, req.endDate)}</div>
<div>${req.type}</div>
<div class="text-right">${emp ? emp.vacationDays : 0} дн.</div>
<div><span class="status-pill ${statusClass}">${statusLabels[req.status]}</span></div>
<div><span class="kt-ai-chip" style="background:${variant.bg};color:${variant.color}">${variant.label}</span></div>
<div class="text-right">
${req.status === 'new' || req.status === 'pending' ? `
<div class="btn-group" onclick="event.stopPropagation()">
<button class="btn-sm btn-danger" onclick="rejectRequest(${req.id})">✕</button>
<button class="btn-sm btn-primary" onclick="approveRequest(${req.id})">✓</button>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="rejectRequest(${req.id})" title="Отклонить">
<svg class="kt-icon" style="width:14px;height:14px;color:var(--kt-ai-status-risk-fg)"><use href="#x"/></svg>
</button>
<button class="kt-ai-btn" data-variant="primary" data-size="sm" onclick="approveRequest(${req.id})" title="Согласовать">
<svg class="kt-icon" style="width:14px;height:14px"><use href="#check"/></svg>
</button>
</div>
` : '<span style="color:var(--kt-ai-fg-faint);font-size:18px">›</span>'}
` : `<svg class="kt-icon" style="width:18px;height:18px;color:var(--kt-ai-fg-faint)"><use href="#chevron-right"/></svg>`}
</div>
</div>
`;
@ -374,11 +342,12 @@ function openDrawer(id) {
const overlap = checkOverlaps(req);
const hasConflict = overlap.length > 0;
const seniorOnVacation = checkSeniorConflict(req);
const daysOk = emp && emp.vacationDays >= req.days;
document.getElementById('drawer-title').textContent = `Заявка №${req.id}`;
document.getElementById('drawer-content').innerHTML = `
<div class="drawer-section">
<h3>Основная информация</h3>
<h3 style="font-size:13px;font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:var(--kt-ai-space-3)">Основная информация</h3>
<div class="info-row">
<span class="info-label">Сотрудник</span>
<span class="info-value">${req.employeeName}</span>
@ -406,38 +375,32 @@ function openDrawer(id) {
</div>
<div class="drawer-section">
<h3>Проверка правил</h3>
<div class="rule-check ${emp && emp.vacationDays >= req.days ? 'ok' : 'fail'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="${emp && emp.vacationDays >= req.days ? 'ok' : 'fail'}">
${emp && emp.vacationDays >= req.days ? '<path d="M20 6L9 17l-5-5"/>' : '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>'}
</svg>
<h3 style="font-size:13px;font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:var(--kt-ai-space-3)">Проверка правил</h3>
<div class="rule-check" style="color:${daysOk ? 'var(--kt-ai-status-ok-fg)' : 'var(--kt-ai-status-risk-fg)'}">
<svg class="kt-icon" style="color:${daysOk ? 'var(--kt-ai-status-ok)' : 'var(--kt-ai-status-risk)'}"><use href="${daysOk ? '#check-circle' : '#x-circle'}"/></svg>
<span>Достаточно дней отпуска (${req.days} из ${emp ? emp.vacationDays : 0})</span>
</div>
<div class="rule-check ${!hasConflict ? 'ok' : 'fail'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="${!hasConflict ? 'ok' : 'fail'}">
${!hasConflict ? '<path d="M20 6L9 17l-5-5"/>' : '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>'}
</svg>
<div class="rule-check" style="color:${!hasConflict ? 'var(--kt-ai-status-ok-fg)' : 'var(--kt-ai-status-risk-fg)'}">
<svg class="kt-icon" style="color:${!hasConflict ? 'var(--kt-ai-status-ok)' : 'var(--kt-ai-status-risk)'}"><use href="${!hasConflict ? '#check-circle' : '#x-circle'}"/></svg>
<span>Лимит simultaneity (≤2 человека) ${hasConflict ? `— конфликт с: ${overlap.map(o => o.employeeName).join(', ')}` : ''}</span>
</div>
<div class="rule-check ${!seniorOnVacation ? 'ok' : 'fail'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="${!seniorOnVacation ? 'ok' : 'fail'}">
${!seniorOnVacation ? '<path d="M20 6L9 17l-5-5"/>' : '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>'}
</svg>
<div class="rule-check" style="color:${!seniorOnVacation ? 'var(--kt-ai-status-ok-fg)' : 'var(--kt-ai-status-risk-fg)'}">
<svg class="kt-icon" style="color:${!seniorOnVacation ? 'var(--kt-ai-status-ok)' : 'var(--kt-ai-status-risk)'}"><use href="${!seniorOnVacation ? '#check-circle' : '#x-circle'}"/></svg>
<span>Наличие дежурного старшего ${seniorOnVacation ? '— все старшие в отпуске' : ''}</span>
</div>
</div>
${req.note ? `
<div class="drawer-section">
<h3>Примечание</h3>
<p style="font-size:14px;color:var(--kt-ai-fg-muted);line-height:1.6">${req.note}</p>
<h3 style="font-size:13px;font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:var(--kt-ai-space-3)">Примечание</h3>
<p style="font-size:14px;color:var(--kt-ai-fg-muted);line-height:1.6;margin:0">${req.note}</p>
</div>
` : ''}
${req.rejectReason ? `
<div class="drawer-section">
<h3 style="color:#991b1b">Причина отказа</h3>
<p style="font-size:14px;color:#991b1b;line-height:1.6">${req.rejectReason}</p>
<h3 style="font-size:13px;font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-status-risk-fg);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:var(--kt-ai-space-3)">Причина отказа</h3>
<p style="font-size:14px;color:var(--kt-ai-status-risk-fg);line-height:1.6;margin:0">${req.rejectReason}</p>
</div>
` : ''}
@ -541,6 +504,7 @@ function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast show ${type}`;
toast.style.borderLeft = `4px solid ${type === 'success' ? 'var(--kt-ai-status-ok)' : 'var(--kt-ai-status-risk)'}`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
@ -581,7 +545,15 @@ init();
<svg style="display:none">
<symbol id="plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
<symbol id="rotateCcw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 4v6h6M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></symbol>
<symbol id="rotate-ccw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 4v6h6M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></symbol>
<symbol id="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
<symbol id="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></symbol>
<symbol id="chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></symbol>
<symbol id="layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></symbol>
<symbol id="bar-chart-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10M12 20V4M6 20v-6"/></symbol>
<symbol id="inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1"/><path d="M22 12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h10"/><rect x="8" y="7" width="8" height="8"/></symbol>
<symbol id="check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></symbol>
<symbol id="x-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></symbol>
</svg>
</body>