var TIMETRACK_EMPLOYEES = [ {id:1, name:'Алексей Петров', initials:'АП', dept:'Розробка', position:'Frontend розробник', type:'office'}, {id:2, name:'Мария Иванова', initials:'МІ', dept:'HR', position:'HR менеджер', type:'office'}, {id:3, name:'Дмитрий Козлов', initials:'ДК', dept:'Дизайн', position:'Дизайнер', type:'office'}, {id:4, name:'Елена Смирнова', initials:'ЄС', dept:'Фінанси', position:'Бухгалтер', type:'office'}, {id:5, name:'Игорь Волков', initials:'ІВ', dept:'Розробка', position:'Тімлід', type:'office'}, {id:6, name:'Ольга Новикова', initials:'ОН', dept:'Маркетинг', position:'Маркетолог', type:'office'}, {id:7, name:'Оксана Мельник', initials:'ОМ', dept:'А01', position:'Фармацевт', type:'pharma'}, {id:8, name:'Катерина Андріїв', initials:'КА', dept:'А07', position:'Завідуючий', type:'pharma'}, {id:9, name:'Василь Нечипор', initials:'ВН', dept:'А07', position:'Фармацевт', type:'pharma'}, {id:10,name:'Людмила Приходько',initials:'ЛП', dept:'А01', position:'Касир', type:'pharma'}, ]; // Генерируем записи за текущий месяц var TIMETRACK_RECORDS = (function() { var records = []; var now = new Date(); var year = now.getFullYear(); var month = now.getMonth(); var daysInMonth = new Date(year, month + 1, 0).getDate(); var today = now.getDate(); TIMETRACK_EMPLOYEES.forEach(function(emp) { for (var d = 1; d <= Math.min(today, daysInMonth); d++) { var date = new Date(year, month, d); var dow = date.getDay(); if (dow === 0 || dow === 6) continue; // пропускаем выходные var rand = Math.random(); var status, checkIn, checkOut, hours; if (rand < 0.05) { status = 'absent'; checkIn = null; checkOut = null; hours = 0; } else if (rand < 0.12) { status = 'sick'; checkIn = null; checkOut = null; hours = 0; } else if (rand < 0.18) { status = 'vacation'; checkIn = null; checkOut = null; hours = 0; } else if (rand < 0.25) { // опоздание var lateMin = 10 + Math.floor(Math.random() * 50); var inH = 9, inM = lateMin; checkIn = pad(inH) + ':' + pad(inM); checkOut = '18:' + pad(Math.floor(Math.random() * 30)); hours = Math.round(((18 * 60 + 15) - (inH * 60 + inM)) / 60 * 10) / 10; status = 'late'; } else { var inM2 = Math.floor(Math.random() * 15); checkIn = '09:' + pad(inM2); var outM = 0 + Math.floor(Math.random() * 30); checkOut = '18:' + pad(outM); hours = Math.round(((18 * 60 + outM) - (9 * 60 + inM2)) / 60 * 10) / 10; status = 'present'; } records.push({ empId: emp.id, date: year + '-' + pad(month + 1) + '-' + pad(d), dayLabel: pad(d) + '.' + pad(month + 1), dow: dow, status: status, checkIn: checkIn, checkOut: checkOut, hours: hours }); } }); return records; })(); function pad(n) { return n < 10 ? '0' + n : '' + n; } var TTSTATUS = { present: {label:'Присутній', cls:'badge-success', color:'#22c55e'}, late: {label:'Запізнення',cls:'badge-warning', color:'#f59e0b'}, absent: {label:'Відсутній', cls:'badge-danger', color:'#ef4444'}, sick: {label:'Хворіє', cls:'badge-info', color:'#3b82f6'}, vacation:{label:'Відпустка', cls:'badge-muted', color:'#94a3b8'}, }; var ttState = { view: 'table', // table | calendar filter: 'all', // all | office | pharma empId: null, // выбранный сотрудник для детали }; function ttInit() { if (!document.getElementById('tt-tbody')) return; ttRenderSummary(); ttRenderTable(); } function ttRenderSummary() { var now = new Date(); var year = now.getFullYear(), month = now.getMonth(); var workdays = 0; for (var d = 1; d <= now.getDate(); d++) { var dow = new Date(year, month, d).getDay(); if (dow > 0 && dow < 6) workdays++; } var recs = TIMETRACK_RECORDS; var present = recs.filter(function(r){ return r.status === 'present'; }).length; var late = recs.filter(function(r){ return r.status === 'late'; }).length; var absent = recs.filter(function(r){ return r.status === 'absent'; }).length; var sick = recs.filter(function(r){ return r.status === 'sick'; }).length; var vacation = recs.filter(function(r){ return r.status === 'vacation'; }).length; var totalHours = recs.reduce(function(s,r){ return s + r.hours; }, 0); setText('tt-stat-workdays', workdays + ' дн.'); setText('tt-stat-present', present); setText('tt-stat-late', late); setText('tt-stat-absent', absent + sick); setText('tt-stat-hours', Math.round(totalHours) + ' год'); } function setText(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; } function ttGetFiltered() { var emps = TIMETRACK_EMPLOYEES.filter(function(e) { if (ttState.filter === 'office') return e.type === 'office'; if (ttState.filter === 'pharma') return e.type === 'pharma'; return true; }); return emps; } function ttRenderTable() { var tbody = document.getElementById('tt-tbody'); if (!tbody) return; var emps = ttGetFiltered(); var now = new Date(); var year = now.getFullYear(), month = now.getMonth(); tbody.innerHTML = emps.map(function(emp) { var empRecs = TIMETRACK_RECORDS.filter(function(r){ return r.empId === emp.id; }); var totalH = empRecs.reduce(function(s,r){ return s + r.hours; }, 0); var days = empRecs.length; var lates = empRecs.filter(function(r){ return r.status === 'late'; }).length; var absents = empRecs.filter(function(r){ return r.status === 'absent'; }).length; var sicks = empRecs.filter(function(r){ return r.status === 'sick'; }).length; // Мини-тепловая карта последних 7 рабочих дней var lastRecs = empRecs.slice(-7); var heatmap = lastRecs.map(function(r) { var c = TTSTATUS[r.status] || TTSTATUS.present; return ''; }).join(''); var typeBadge = emp.type === 'pharma' ? '' + emp.dept + '' : '' + emp.dept + ''; return '' + '' + '
' + '
' + emp.initials + '
' + '
' + emp.name + '
' + '
' + emp.position + '
' + '
' + '' + '' + typeBadge + '' + '' + Math.round(totalH) + ' год' + '' + days + '' + '' + lates + '' + '' + (absents + sicks) + '' + '' + heatmap + '' + ''; }).join(''); var countEl = document.getElementById('tt-count'); if (countEl) countEl.textContent = 'Показано ' + emps.length + ' з ' + TIMETRACK_EMPLOYEES.length + ' співробітників'; } function ttShowDetail(empId) { ttState.empId = empId; var emp = TIMETRACK_EMPLOYEES.find(function(e){ return e.id === empId; }); var empRecs = TIMETRACK_RECORDS.filter(function(r){ return r.empId === empId; }); var panel = document.getElementById('tt-detail'); var overlay = document.getElementById('tt-detail-overlay'); if (!panel || !emp) return; var rows = empRecs.map(function(r) { var sc = TTSTATUS[r.status] || TTSTATUS.present; return '' + '' + r.dayLabel + '' + '' + (r.checkIn || '—') + '' + '' + (r.checkOut || '—') + '' + '' + (r.hours > 0 ? r.hours + ' год' : '—') + '' + '' + sc.label + '' + ''; }).join(''); panel.innerHTML = '
' + '
' + '
' + emp.initials + '
' + '
' + emp.name + '
' + '
' + emp.position + ' · ' + emp.dept + '
' + '
' + '' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + rows + '' + '
ДатаПрихідВідхідГодинСтатус
' + '
'; if (overlay) overlay.style.display = 'none'; panel.style.display = 'flex'; } function ttCloseDetail() { var panel = document.getElementById('tt-detail'); var overlay = document.getElementById('tt-detail-overlay'); if (panel) panel.style.display = 'none'; if (overlay) overlay.style.display = 'none'; } function ttSetFilter(f) { ttState.filter = f; ['tt-filter-all','tt-filter-office','tt-filter-pharma'].forEach(function(id) { var el = document.getElementById(id); if (!el) return; var active = id === 'tt-filter-' + f; el.style.background = active ? 'var(--color-card)' : 'transparent'; el.style.color = active ? 'var(--color-foreground)' : 'var(--color-muted-foreground)'; }); ttRenderTable(); }