288 lines
10 KiB
JavaScript
288 lines
10 KiB
JavaScript
let allOrders = [];
|
||
let currentFilter = { branch: 'all', type: 'all', search: '' };
|
||
let selectedOrder = null;
|
||
let charts = {};
|
||
|
||
async function loadOrders() {
|
||
try {
|
||
const res = await fetch('/api/items');
|
||
const data = await res.json();
|
||
allOrders = data.items || [];
|
||
renderTable();
|
||
updateKPI();
|
||
} catch (e) {
|
||
console.error('Failed to load orders:', e);
|
||
}
|
||
}
|
||
|
||
function formatDate(iso) {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||
}
|
||
|
||
function getTypeClass(type) {
|
||
if (!type) return 'type-prikaz';
|
||
const t = type.toLowerCase();
|
||
if (t.includes('приказ')) return 'type-prikaz';
|
||
if (t.includes('распоряж')) return 'type-rasp';
|
||
if (t.includes('утвержд')) return 'type-utv';
|
||
if (t.includes('решение')) return 'type-resh';
|
||
return 'type-prikaz';
|
||
}
|
||
|
||
function getStatusClass(status) {
|
||
if (!status) return 'status-draft';
|
||
const s = status.toLowerCase();
|
||
if (s.includes('действ')) return 'status-active';
|
||
if (s.includes('истёк') || s.includes('истек')) return 'status-expired';
|
||
return 'status-draft';
|
||
}
|
||
|
||
function getFilteredOrders() {
|
||
return allOrders.filter(o => {
|
||
if (currentFilter.branch !== 'all' && o.branch !== currentFilter.branch) return false;
|
||
if (currentFilter.type !== 'all' && o.type !== currentFilter.type) return false;
|
||
if (currentFilter.search) {
|
||
const s = currentFilter.search.toLowerCase();
|
||
const match = (o.number || '').toLowerCase().includes(s) ||
|
||
(o.title || '').toLowerCase().includes(s) ||
|
||
(o.branch || '').toLowerCase().includes(s);
|
||
if (!match) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function renderTable() {
|
||
const orders = getFilteredOrders();
|
||
const tbody = document.getElementById('ordersBody');
|
||
const empty = document.getElementById('emptyState');
|
||
|
||
if (orders.length === 0) {
|
||
tbody.innerHTML = '';
|
||
empty.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
empty.style.display = 'none';
|
||
tbody.innerHTML = orders.map(o => `
|
||
<tr data-id="${o.id}">
|
||
<td><span class="type-badge ${getTypeClass(o.type)}">${o.type || 'Приказ'}</span></td>
|
||
<td style="font-family:var(--kt-ai-font-mono)">${o.number || ''}</td>
|
||
<td>${formatDate(o.date)}</td>
|
||
<td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${o.title || ''}</td>
|
||
<td>${o.branch || ''}</td>
|
||
<td><span class="status-pill ${getStatusClass(o.status)}">${o.status || 'Проект'}</span></td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
tbody.querySelectorAll('tr').forEach(tr => {
|
||
tr.addEventListener('click', () => {
|
||
const id = parseInt(tr.dataset.id);
|
||
const order = allOrders.find(o => o.id === id);
|
||
if (order) openDrawer(order);
|
||
});
|
||
});
|
||
}
|
||
|
||
function updateKPI() {
|
||
const now = new Date();
|
||
const thisMonth = now.toISOString().slice(0, 7);
|
||
const branches = new Set(allOrders.map(o => o.branch).filter(Boolean));
|
||
const active = allOrders.filter(o => (o.status || '').toLowerCase().includes('действ')).length;
|
||
const monthCount = allOrders.filter(o => (o.date || '').startsWith(thisMonth)).length;
|
||
|
||
document.getElementById('kpi-total').textContent = allOrders.length;
|
||
document.getElementById('kpi-active').textContent = active;
|
||
document.getElementById('kpi-month').textContent = monthCount;
|
||
document.getElementById('kpi-branches').textContent = branches.size;
|
||
}
|
||
|
||
function openDrawer(order) {
|
||
selectedOrder = order;
|
||
document.getElementById('drawerTitle').textContent = order.title || 'Без названия';
|
||
document.getElementById('drawerMeta').textContent = `${order.number || ''} • ${formatDate(order.date)} • ${order.branch || ''}`;
|
||
|
||
document.getElementById('drawerContent').innerHTML = `
|
||
<div class="detail-row">
|
||
<div class="detail-label">Тип документа</div>
|
||
<div class="detail-value"><span class="type-badge ${getTypeClass(order.type)}">${order.type || 'Приказ'}</span></div>
|
||
</div>
|
||
<div class="detail-row">
|
||
<div class="detail-label">Номер</div>
|
||
<div class="detail-value">${order.number || '—'}</div>
|
||
</div>
|
||
<div class="detail-row">
|
||
<div class="detail-label">Дата</div>
|
||
<div class="detail-value">${formatDate(order.date)}</div>
|
||
</div>
|
||
<div class="detail-row">
|
||
<div class="detail-label">Филиал</div>
|
||
<div class="detail-value">${order.branch || '—'}</div>
|
||
</div>
|
||
<div class="detail-row">
|
||
<div class="detail-label">Статус</div>
|
||
<div class="detail-value"><span class="status-pill ${getStatusClass(order.status)}">${order.status || 'Проект'}</span></div>
|
||
</div>
|
||
<div class="detail-row">
|
||
<div class="detail-label">Содержание</div>
|
||
<div class="detail-value" style="white-space:pre-wrap">${order.content || 'Нет содержания'}</div>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('drawer').classList.add('open');
|
||
document.getElementById('drawerOverlay').classList.add('open');
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById('drawer').classList.remove('open');
|
||
document.getElementById('drawerOverlay').classList.remove('open');
|
||
selectedOrder = null;
|
||
}
|
||
|
||
function renderCharts() {
|
||
const ctxTypes = document.getElementById('chartTypes').getContext('2d');
|
||
const ctxBranches = document.getElementById('chartBranches').getContext('2d');
|
||
const ctxMonths = document.getElementById('chartMonths').getContext('2d');
|
||
const ctxStatus = document.getElementById('chartStatus').getContext('2d');
|
||
|
||
// По типам
|
||
const byType = {};
|
||
allOrders.forEach(o => { byType[o.type || 'Приказ'] = (byType[o.type || 'Приказ'] || 0) + 1; });
|
||
|
||
// По филиалам
|
||
const byBranch = {};
|
||
allOrders.forEach(o => { byBranch[o.branch || 'Не указан'] = (byBranch[o.branch || 'Не указан'] || 0) + 1; });
|
||
|
||
// По месяцам
|
||
const byMonth = {};
|
||
allOrders.forEach(o => {
|
||
const m = (o.date || '').slice(0, 7) || 'Не указана';
|
||
byMonth[m] = (byMonth[m] || 0) + 1;
|
||
});
|
||
|
||
// По статусам
|
||
const byStatus = {};
|
||
allOrders.forEach(o => { byStatus[o.status || 'Проект'] = (byStatus[o.status || 'Проект'] || 0) + 1; });
|
||
|
||
const common = {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: {
|
||
x: { grid: { display: false } },
|
||
y: { beginAtZero: true, ticks: { precision: 0 } }
|
||
}
|
||
};
|
||
|
||
if (charts.types) charts.types.destroy();
|
||
charts.types = new Chart(ctxTypes, {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: Object.keys(byType),
|
||
datasets: [{ data: Object.values(byType), backgroundColor: ['#3b82f6', '#ec4899', '#10b981', '#f59e0b'] }]
|
||
},
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
|
||
});
|
||
|
||
if (charts.branches) charts.branches.destroy();
|
||
charts.branches = new Chart(ctxBranches, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: Object.keys(byBranch),
|
||
datasets: [{ data: Object.values(byBranch), backgroundColor: '#3b82f6' }]
|
||
},
|
||
options: { ...common, indexAxis: 'y' }
|
||
});
|
||
|
||
if (charts.months) charts.months.destroy();
|
||
charts.months = new Chart(ctxMonths, {
|
||
type: 'line',
|
||
data: {
|
||
labels: Object.keys(byMonth).sort(),
|
||
datasets: [{ data: Object.values(byMonth), borderColor: '#10b981', fill: true, backgroundColor: 'rgba(16,185,129,0.1)' }]
|
||
},
|
||
options: common
|
||
});
|
||
|
||
if (charts.status) charts.status.destroy();
|
||
charts.status = new Chart(ctxStatus, {
|
||
type: 'pie',
|
||
data: {
|
||
labels: Object.keys(byStatus),
|
||
datasets: [{ data: Object.values(byStatus), backgroundColor: ['#10b981', '#ef4444', '#f59e0b'] }]
|
||
},
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
|
||
});
|
||
}
|
||
|
||
// Event listeners
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadOrders();
|
||
|
||
// Поиск
|
||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||
currentFilter.search = e.target.value;
|
||
renderTable();
|
||
});
|
||
|
||
// Фильтры по типу
|
||
document.querySelectorAll('[data-type]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('[data-type]').forEach(b => b.classList.remove('is-active'));
|
||
btn.classList.add('is-active');
|
||
currentFilter.type = btn.dataset.type;
|
||
renderTable();
|
||
});
|
||
});
|
||
|
||
// Фильтры по филиалам
|
||
document.querySelectorAll('[data-branch]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
currentFilter.branch = btn.dataset.branch;
|
||
renderTable();
|
||
});
|
||
});
|
||
|
||
// Открытие drawer
|
||
document.getElementById('drawerOverlay').addEventListener('click', closeDrawer);
|
||
document.getElementById('closeDrawer').addEventListener('click', closeDrawer);
|
||
|
||
// Статистика
|
||
document.getElementById('openStats').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
renderCharts();
|
||
document.getElementById('statsModal').classList.add('open');
|
||
});
|
||
|
||
document.getElementById('closeStats').addEventListener('click', () => {
|
||
document.getElementById('statsModal').classList.remove('open');
|
||
});
|
||
|
||
// Кнопки добавления
|
||
document.getElementById('addBtn').addEventListener('click', () => {
|
||
alert('Форма добавления документа будет в следующей версии');
|
||
});
|
||
document.getElementById('addBtnSidebar').addEventListener('click', () => {
|
||
alert('Форма добавления документа будет в следующей версии');
|
||
});
|
||
|
||
// Удаление
|
||
document.getElementById('deleteBtn').addEventListener('click', async () => {
|
||
if (!selectedOrder || !confirm('Удалить документ ' + selectedOrder.number + '?')) return;
|
||
try {
|
||
await fetch('/api/items', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: selectedOrder.id })
|
||
});
|
||
closeDrawer();
|
||
loadOrders();
|
||
} catch (e) {
|
||
console.error('Delete failed:', e);
|
||
}
|
||
});
|
||
});
|