Initial commit — ACORE pharmacy network management system

Django project: employees, locations, visit_manager, it_manager,
mattermost_connector and stub connectors (ad, mail, portal).
This commit is contained in:
2026-05-31 14:21:47 +03:00
commit 131cb27d16
166 changed files with 14990 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
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();
}