Files
ACORE/static/assets/js/timetrack.js
T
lokses 131cb27d16 Initial commit — ACORE pharmacy network management system
Django project: employees, locations, visit_manager, it_manager,
mattermost_connector and stub connectors (ad, mail, portal).
2026-05-31 14:21:47 +03:00

243 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 '<span title="' + r.dayLabel + ': ' + (TTSTATUS[r.status] || {label:''}).label + '" style="display:inline-block;width:10px;height:18px;border-radius:2px;background:' + c.color + ';opacity:0.8;margin:0 1px;"></span>';
}).join('');
var typeBadge = emp.type === 'pharma'
? '<span style="font-size:0.625rem;padding:1px 5px;border-radius:10px;background:#f0fdf4;color:#15803d;font-weight:500;">' + emp.dept + '</span>'
: '<span style="font-size:0.625rem;padding:1px 5px;border-radius:10px;background:#eff6ff;color:#1d4ed8;font-weight:500;">' + emp.dept + '</span>';
return '<tr class="tr-hover" style="border-bottom:1px solid var(--color-border);cursor:pointer;" onclick="ttShowDetail(' + emp.id + ')">' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;">' +
'<div style="display:flex;align-items:center;gap:0.625rem;">' +
'<div style="width:2rem;height:2rem;border-radius:50%;background:color-mix(in srgb,var(--color-primary) 10%,transparent);color:var(--color-primary);display:flex;align-items:center;justify-content:center;font-size:0.6875rem;font-weight:500;flex-shrink:0;">' + emp.initials + '</div>' +
'<div><div style="font-weight:500;color:var(--color-card-foreground);font-size:0.875rem;">' + emp.name + '</div>' +
'<div style="font-size:0.75rem;color:var(--color-muted-foreground);margin-top:1px;">' + emp.position + '</div></div>' +
'</div>' +
'</td>' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;">' + typeBadge + '</td>' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;color:var(--color-foreground);font-weight:500;">' + Math.round(totalH) + ' год</td>' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;color:var(--color-foreground);">' + days + '</td>' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;color:' + (lates > 0 ? 'var(--color-warning)' : 'var(--color-muted-foreground)') + ';">' + lates + '</td>' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;color:' + (absents + sicks > 0 ? 'var(--color-destructive)' : 'var(--color-muted-foreground)') + ';">' + (absents + sicks) + '</td>' +
'<td style="padding:0.875rem 1rem;vertical-align:middle;">' + heatmap + '</td>' +
'</tr>';
}).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 '<tr class="tr-hover" style="border-bottom:1px solid var(--color-border);">' +
'<td style="padding:0.625rem 1rem;color:var(--color-muted-foreground);font-size:0.8125rem;">' + r.dayLabel + '</td>' +
'<td style="padding:0.625rem 1rem;font-size:0.8125rem;color:var(--color-foreground);">' + (r.checkIn || '—') + '</td>' +
'<td style="padding:0.625rem 1rem;font-size:0.8125rem;color:var(--color-foreground);">' + (r.checkOut || '—') + '</td>' +
'<td style="padding:0.625rem 1rem;font-size:0.8125rem;color:var(--color-foreground);">' + (r.hours > 0 ? r.hours + ' год' : '—') + '</td>' +
'<td style="padding:0.625rem 1rem;"><span class="badge ' + sc.cls + '">' + sc.label + '</span></td>' +
'</tr>';
}).join('');
panel.innerHTML =
'<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--color-border);">' +
'<div style="display:flex;align-items:center;gap:0.75rem;">' +
'<div style="width:2.25rem;height:2.25rem;border-radius:50%;background:color-mix(in srgb,var(--color-primary) 10%,transparent);color:var(--color-primary);display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:500;">' + emp.initials + '</div>' +
'<div><div style="font-weight:600;font-size:0.9375rem;color:var(--color-foreground);">' + emp.name + '</div>' +
'<div style="font-size:0.75rem;color:var(--color-muted-foreground);">' + emp.position + ' · ' + emp.dept + '</div></div>' +
'</div>' +
'<button onclick="ttCloseDetail()" class="btn btn-ghost btn-icon">' +
'<svg width="16" height="16" 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"/></svg>' +
'</button>' +
'</div>' +
'<div style="overflow-y:auto;max-height:calc(100% - 60px);">' +
'<table style="width:100%;border-collapse:collapse;">' +
'<thead><tr style="border-bottom:1px solid var(--color-border);">' +
'<th style="padding:0.625rem 1rem;text-align:left;font-size:0.75rem;color:var(--color-muted-foreground);font-weight:600;">Дата</th>' +
'<th style="padding:0.625rem 1rem;text-align:left;font-size:0.75rem;color:var(--color-muted-foreground);font-weight:600;">Прихід</th>' +
'<th style="padding:0.625rem 1rem;text-align:left;font-size:0.75rem;color:var(--color-muted-foreground);font-weight:600;">Відхід</th>' +
'<th style="padding:0.625rem 1rem;text-align:left;font-size:0.75rem;color:var(--color-muted-foreground);font-weight:600;">Годин</th>' +
'<th style="padding:0.625rem 1rem;text-align:left;font-size:0.75rem;color:var(--color-muted-foreground);font-weight:600;">Статус</th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'</div>';
overlay.style.display = 'block';
panel.style.transform = 'translateX(0)';
}
function ttCloseDetail() {
var panel = document.getElementById('tt-detail');
var overlay = document.getElementById('tt-detail-overlay');
if (panel) panel.style.transform = 'translateX(100%)';
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();
}