426 lines
20 KiB
HTML
426 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<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">
|
||
<style>
|
||
a.kt-ai-btn { text-decoration: none; }
|
||
.kt-ai-app { min-height: 100vh; display: flex; flex-direction: column; }
|
||
.kt-ai-main { flex: 1; padding: var(--kt-ai-space-8) 0; }
|
||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--kt-ai-space-6); }
|
||
.status-bar { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
|
||
.table-responsive { overflow-x: auto; }
|
||
.view-toggle { display: flex; gap: var(--kt-ai-space-2); margin-bottom: var(--kt-ai-space-4); }
|
||
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
|
||
.calendar-day { min-height: 80px; border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-md); padding: var(--kt-ai-space-2); }
|
||
.calendar-day.header { font-weight: var(--kt-ai-weight-semibold); text-align: center; }
|
||
.calendar-day.empty { background: var(--kt-ai-bg-soft); }
|
||
.duty-badge { font-size: var(--kt-ai-text-xs); padding: 2px 6px; border-radius: var(--kt-ai-radius-md); margin-top: 4px; display: block; }
|
||
.filter-active { background: var(--kt-ai-primary); color: white; }
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app">
|
||
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3)">
|
||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||
<strong>График дежурств</strong>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="toggleView()">Переключить вид</button>
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="showTab('form')">Новая заявка</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="kt-ai-main">
|
||
<div class="kt-ai-wrap">
|
||
|
||
<div class="kt-ai-kpi-strip" style="margin-bottom: var(--kt-ai-space-8)">
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-total">0</div>
|
||
<div class="label">Всего заявок</div>
|
||
<div class="hint">За все время</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-pending" style="color: var(--kt-ai-chart-orange)">0</div>
|
||
<div class="label">На рассмотрении</div>
|
||
<div class="hint">Ожидают подтверждения</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-approved" style="color: var(--kt-ai-chart-green)">0</div>
|
||
<div class="label">Утверждено</div>
|
||
<div class="hint">Включено в график</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-rejected" style="color: var(--kt-ai-chart-red)">0</div>
|
||
<div class="label">Отклонено</div>
|
||
<div class="hint">Требуют внимания</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-form">
|
||
<div class="kt-ai-card" style="padding: var(--kt-ai-space-8); margin-bottom: var(--kt-ai-space-8)">
|
||
<h2 style="margin-bottom: var(--kt-ai-space-6)">Заявка на дежурство</h2>
|
||
<form id="dutyForm" onsubmit="submitForm(event)">
|
||
<div class="form-grid">
|
||
<div class="kt-ai-field">
|
||
<label>ФИО *</label>
|
||
<input type="text" class="kt-ai-input" id="fio" placeholder="Иванов Иван Иванович" required>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Должность *</label>
|
||
<input type="text" class="kt-ai-input" id="position" placeholder="Инженер технической поддержки" required>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Подразделение</label>
|
||
<input type="text" class="kt-ai-input" id="department" placeholder="Департамент ИТ">
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Контактный телефон</label>
|
||
<input type="tel" class="kt-ai-input" id="phone" placeholder="+7 (___) ___-__-__">
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Дата начала дежурства *</label>
|
||
<input type="date" class="kt-ai-input" id="dateStart" required>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Дата окончания дежурства *</label>
|
||
<input type="date" class="kt-ai-input" id="dateEnd" required>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Тип дежурства *</label>
|
||
<select class="kt-ai-select" id="dutyType" required>
|
||
<option value="">Выберите тип</option>
|
||
<option value="weekday">Будний день</option>
|
||
<option value="weekend">Выходной день</option>
|
||
<option value="holiday">Праздничный день</option>
|
||
<option value="night">Ночное дежурство</option>
|
||
<option value="full">Круглосуточное</option>
|
||
</select>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Приоритет</label>
|
||
<select class="kt-ai-select" id="priority">
|
||
<option value="normal">Обычный</option>
|
||
<option value="high">Высокий</option>
|
||
<option value="urgent">Срочный</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="kt-ai-field" style="margin-top: var(--kt-ai-space-6)">
|
||
<label>Комментарий (пожелания по графику)</label>
|
||
<textarea class="kt-ai-textarea" id="comment" rows="3" placeholder="Например: предпочтительно утренние смены"></textarea>
|
||
</div>
|
||
<div style="display: flex; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-8)">
|
||
<button type="submit" class="kt-ai-btn" data-variant="primary" data-size="lg">Сохранить заявку</button>
|
||
<button type="button" class="kt-ai-btn" data-variant="ghost" data-size="lg" onclick="clearForm()">Очистить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-list" style="display: none;">
|
||
<div class="kt-ai-card" style="padding: var(--kt-ai-space-6)">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-4); flex-wrap: wrap; gap: var(--kt-ai-space-4)">
|
||
<h2 style="margin: 0">Заявки</h2>
|
||
<input type="text" class="kt-ai-input" placeholder="Поиск по ФИО..." oninput="filterTable(this.value)" style="max-width: 280px">
|
||
</div>
|
||
|
||
<div class="status-bar">
|
||
<button class="kt-ai-chip filter-active" onclick="filterStatus('all', this)">Все</button>
|
||
<button class="kt-ai-chip" onclick="filterStatus('pending', this)">На рассмотрении</button>
|
||
<button class="kt-ai-chip" onclick="filterStatus('approved', this)">Утверждено</button>
|
||
<button class="kt-ai-chip" onclick="filterStatus('rejected', this)">Отклонено</button>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="kt-ai-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ФИО</th>
|
||
<th>Должность</th>
|
||
<th>Даты</th>
|
||
<th>Тип</th>
|
||
<th>Статус</th>
|
||
<th>Дата подачи</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="applicationsTable"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-calendar" style="display: none;">
|
||
<div class="kt-ai-card" style="padding: var(--kt-ai-space-6)">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-6)">
|
||
<h2 style="margin: 0">Календарь дежурств</h2>
|
||
<div style="display: flex; gap: var(--kt-ai-space-2)">
|
||
<button class="kt-ai-btn" onclick="prevMonth()">←</button>
|
||
<span id="currentMonth" style="font-weight: var(--kt-ai-weight-semibold); min-width: 180px; text-align: center;"></span>
|
||
<button class="kt-ai-btn" onclick="nextMonth()">→</button>
|
||
</div>
|
||
</div>
|
||
<div class="calendar-grid" id="calendarGrid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
|
||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||
<span>© 2026 Казахтелеком</span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span>Система учёта дежурств</span>
|
||
</footer>
|
||
|
||
<script>
|
||
let applications = [];
|
||
let currentFilter = 'all';
|
||
let currentDate = new Date();
|
||
|
||
const DEMO_DATA = [
|
||
{ id: 1, fio: 'Иванов Иван Иванович', position: 'Инженер технической поддержки', department: 'Департамент ИТ', phone: '+7 (777) 123-45-67', dateStart: '2026-09-15', dateEnd: '2026-09-15', dutyType: 'weekday', priority: 'normal', comment: '', status: 'approved', submittedAt: '2026-09-01' },
|
||
{ id: 2, fio: 'Петров Пётр Петрович', position: 'Системный администратор', department: 'Отдел инфраструктуры', phone: '+7 (777) 234-56-78', dateStart: '2026-09-20', dateEnd: '2026-09-21', dutyType: 'weekend', priority: 'high', comment: 'Можно заменить', status: 'pending', submittedAt: '2026-09-03' },
|
||
{ id: 3, fio: 'Сидорова Анна Михайловна', position: 'Специалист кол-центра', department: 'Департамент сервиса', phone: '+7 (777) 345-67-89', dateStart: '2026-09-01', dateEnd: '2026-09-01', dutyType: 'holiday', priority: 'urgent', comment: '', status: 'approved', submittedAt: '2026-08-25' },
|
||
{ id: 4, fio: 'Касымов Ержан Болатович', position: 'Ведущий инженер', department: 'Служба безопасности', phone: '+7 (777) 456-78-90', dateStart: '2026-09-10', dateEnd: '2026-09-12', dutyType: 'night', priority: 'normal', comment: 'Предпочитаю ночь', status: 'rejected', submittedAt: '2026-09-05' },
|
||
{ id: 5, fio: 'Алиева Гульнара Талгатовна', position: 'Менеджер проектов', department: 'Департамент развития', phone: '+7 (777) 567-89-01', dateStart: '2026-09-25', dateEnd: '2026-09-25', dutyType: 'full', priority: 'high', comment: '', status: 'pending', submittedAt: '2026-09-08' }
|
||
];
|
||
|
||
function init() {
|
||
const stored = localStorage.getItem('dutyApplications');
|
||
applications = stored ? JSON.parse(stored) : [...DEMO_DATA];
|
||
updateKPI();
|
||
renderTable();
|
||
renderCalendar();
|
||
updateMonthLabel();
|
||
}
|
||
|
||
function saveApplications() {
|
||
localStorage.setItem('dutyApplications', JSON.stringify(applications));
|
||
updateKPI();
|
||
renderTable();
|
||
renderCalendar();
|
||
}
|
||
|
||
function updateKPI() {
|
||
document.getElementById('kpi-total').textContent = applications.length;
|
||
document.getElementById('kpi-pending').textContent = applications.filter(a => a.status === 'pending').length;
|
||
document.getElementById('kpi-approved').textContent = applications.filter(a => a.status === 'approved').length;
|
||
document.getElementById('kpi-rejected').textContent = applications.filter(a => a.status === 'rejected').length;
|
||
}
|
||
|
||
function submitForm(e) {
|
||
e.preventDefault();
|
||
const newApp = {
|
||
id: Date.now(),
|
||
fio: document.getElementById('fio').value,
|
||
position: document.getElementById('position').value,
|
||
department: document.getElementById('department').value,
|
||
phone: document.getElementById('phone').value,
|
||
dateStart: document.getElementById('dateStart').value,
|
||
dateEnd: document.getElementById('dateEnd').value,
|
||
dutyType: document.getElementById('dutyType').value,
|
||
priority: document.getElementById('priority').value,
|
||
comment: document.getElementById('comment').value,
|
||
status: 'pending',
|
||
submittedAt: new Date().toISOString().split('T')[0]
|
||
};
|
||
applications.push(newApp);
|
||
saveApplications();
|
||
clearForm();
|
||
showTab('list');
|
||
showNotification('Заявка успешно сохранена!', 'success');
|
||
}
|
||
|
||
function clearForm() {
|
||
document.getElementById('dutyForm').reset();
|
||
}
|
||
|
||
function renderTable(filterText = '') {
|
||
const tbody = document.getElementById('applicationsTable');
|
||
let filtered = applications;
|
||
|
||
if (currentFilter !== 'all') {
|
||
filtered = filtered.filter(a => a.status === currentFilter);
|
||
}
|
||
|
||
if (filterText) {
|
||
const lower = filterText.toLowerCase();
|
||
filtered = filtered.filter(a => a.fio.toLowerCase().includes(lower));
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map(app => {
|
||
const statusConfig = getStatusConfig(app.status);
|
||
const typeLabel = getTypeLabel(app.dutyType);
|
||
return `
|
||
<tr>
|
||
<td style="font-weight: var(--kt-ai-weight-medium)">${app.fio}</td>
|
||
<td>${app.position}</td>
|
||
<td>${formatDateRange(app.dateStart, app.dateEnd)}</td>
|
||
<td>${typeLabel}</td>
|
||
<td><span class="kt-ai-chip" style="background: ${statusConfig.bg}; color: ${statusConfig.color}">${statusConfig.label}</span></td>
|
||
<td>${app.submittedAt}</td>
|
||
<td>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editApp(${app.id})">✏️</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="deleteApp(${app.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function getStatusConfig(status) {
|
||
const configs = {
|
||
pending: { label: 'На рассмотрении', bg: 'var(--kt-ai-chart-orange)', color: 'white' },
|
||
approved: { label: 'Утверждено', bg: 'var(--kt-ai-chart-green)', color: 'white' },
|
||
rejected: { label: 'Отклонено', bg: 'var(--kt-ai-chart-red)', color: 'white' }
|
||
};
|
||
return configs[status] || configs.pending;
|
||
}
|
||
|
||
function getTypeLabel(type) {
|
||
const labels = {
|
||
weekday: 'Будний день',
|
||
weekend: 'Выходной',
|
||
holiday: 'Праздничный',
|
||
night: 'Ночное',
|
||
full: 'Круглосуточное'
|
||
};
|
||
return labels[type] || type;
|
||
}
|
||
|
||
function formatDateRange(start, end) {
|
||
const s = new Date(start).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
||
const e = new Date(end).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: '2-digit' });
|
||
return s === e ? s : `${s} — ${e}`;
|
||
}
|
||
|
||
function filterStatus(status, btn) {
|
||
currentFilter = status;
|
||
document.querySelectorAll('.status-bar .kt-ai-chip').forEach(b => b.classList.remove('filter-active'));
|
||
btn.classList.add('filter-active');
|
||
renderTable();
|
||
}
|
||
|
||
function filterTable(value) {
|
||
renderTable(value);
|
||
}
|
||
|
||
function editApp(id) {
|
||
const app = applications.find(a => a.id === id);
|
||
if (!app) return;
|
||
document.getElementById('fio').value = app.fio;
|
||
document.getElementById('position').value = app.position;
|
||
document.getElementById('department').value = app.department;
|
||
document.getElementById('phone').value = app.phone;
|
||
document.getElementById('dateStart').value = app.dateStart;
|
||
document.getElementById('dateEnd').value = app.dateEnd;
|
||
document.getElementById('dutyType').value = app.dutyType;
|
||
document.getElementById('priority').value = app.priority;
|
||
document.getElementById('comment').value = app.comment;
|
||
showTab('form');
|
||
}
|
||
|
||
function deleteApp(id) {
|
||
if (confirm('Удалить эту заявку?')) {
|
||
applications = applications.filter(a => a.id !== id);
|
||
saveApplications();
|
||
showNotification('Заявка удалена', 'info');
|
||
}
|
||
}
|
||
|
||
function showTab(tab) {
|
||
document.getElementById('tab-form').style.display = tab === 'form' ? 'block' : 'none';
|
||
document.getElementById('tab-list').style.display = tab === 'list' ? 'block' : 'none';
|
||
document.getElementById('tab-calendar').style.display = tab === 'calendar' ? 'block' : 'none';
|
||
}
|
||
|
||
function toggleView() {
|
||
const current = document.getElementById('tab-list').style.display === 'block' ? 'list' :
|
||
document.getElementById('tab-calendar').style.display === 'block' ? 'calendar' : 'form';
|
||
const next = current === 'list' ? 'calendar' : current === 'calendar' ? 'form' : 'list';
|
||
showTab(next);
|
||
}
|
||
|
||
function renderCalendar() {
|
||
const grid = document.getElementById('calendarGrid');
|
||
const year = currentDate.getFullYear();
|
||
const month = currentDate.getMonth();
|
||
|
||
const firstDay = new Date(year, month, 1);
|
||
const lastDay = new Date(year, month + 1, 0);
|
||
const startDay = firstDay.getDay() || 7;
|
||
|
||
const days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||
grid.innerHTML = days.map(d => `<div class="calendar-day header">${d}</div>`).join('');
|
||
|
||
for (let i = 1; i < startDay; i++) {
|
||
grid.innerHTML += '<div class="calendar-day empty"></div>';
|
||
}
|
||
|
||
const monthApps = applications.filter(a => {
|
||
const start = new Date(a.dateStart);
|
||
const end = new Date(a.dateEnd);
|
||
return start.getMonth() === month && start.getFullYear() === year ||
|
||
end.getMonth() === month && end.getFullYear() === year ||
|
||
(start < firstDay && end > lastDay);
|
||
});
|
||
|
||
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||
const date = new Date(year, month, day);
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
const dayApps = monthApps.filter(a => date >= new Date(a.dateStart) && date <= new Date(a.dateEnd));
|
||
|
||
grid.innerHTML += `
|
||
<div class="calendar-day">
|
||
<div style="font-weight: var(--kt-ai-weight-semibold); margin-bottom: 4px">${day}</div>
|
||
${dayApps.map(a => `
|
||
<span class="duty-badge" style="background: ${getStatusConfig(a.status).bg}; color: ${getStatusConfig(a.status).color}" title="${a.fio}">
|
||
${a.fio.split(' ').slice(0, 2).join(' ').slice(0, 15)}
|
||
</span>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function updateMonthLabel() {
|
||
const months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
|
||
document.getElementById('currentMonth').textContent = `${months[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
|
||
}
|
||
|
||
function prevMonth() {
|
||
currentDate.setMonth(currentDate.getMonth() - 1);
|
||
updateMonthLabel();
|
||
renderCalendar();
|
||
}
|
||
|
||
function nextMonth() {
|
||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||
updateMonthLabel();
|
||
renderCalendar();
|
||
}
|
||
|
||
function showNotification(message, type) {
|
||
const colors = { success: 'var(--kt-ai-chart-green)', info: 'var(--kt-ai-chart-blue)', error: 'var(--kt-ai-chart-red)' };
|
||
const notif = document.createElement('div');
|
||
notif.style.cssText = `position: fixed; bottom: var(--kt-ai-space-6); right: var(--kt-ai-space-6); padding: var(--kt-ai-space-4) var(--kt-ai-space-6); background: ${colors[type] || colors.info}; color: white; border-radius: var(--kt-ai-radius-xl); font-weight: var(--kt-ai-weight-medium); z-index: 1000; animation: slideIn 0.3s ease`;
|
||
notif.textContent = message;
|
||
document.body.appendChild(notif);
|
||
setTimeout(() => notif.remove(), 3000);
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|