216 lines
9.4 KiB
HTML
216 lines
9.4 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Контроль исполнения поручений СД</title>
|
||
<link rel="stylesheet" href="style.css">
|
||
</head>
|
||
<body>
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<h1>Контроль исполнения поручений Совета директоров</h1>
|
||
</div>
|
||
<nav class="nav" id="mainNav">
|
||
<a href="index.html" class="active">Дашборд</a>
|
||
<a href="registry.html">Реестр</a>
|
||
<a href="card.html?id=new" id="newBtn" style="display:none">Новое поручение</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<main class="dashboard">
|
||
<section class="metrics">
|
||
<div class="metric total">
|
||
<div class="metric-value" id="total">0</div>
|
||
<div class="metric-label">Всего поручений</div>
|
||
</div>
|
||
<div class="metric done">
|
||
<div class="metric-value" id="done">0</div>
|
||
<div class="metric-label">Выполнено</div>
|
||
</div>
|
||
<div class="metric progress">
|
||
<div class="metric-value" id="progress">0</div>
|
||
<div class="metric-label">В работе</div>
|
||
</div>
|
||
<div class="metric control">
|
||
<div class="metric-value" id="control">0</div>
|
||
<div class="metric-label">На контроле</div>
|
||
</div>
|
||
<div class="metric overdue">
|
||
<div class="metric-value" id="overdue">0</div>
|
||
<div class="metric-label">Просрочено</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="filters">
|
||
<select id="filterStatus">
|
||
<option value="">Все статусы</option>
|
||
<option value="В работе">В работе</option>
|
||
<option value="На контроле">На контроле</option>
|
||
<option value="Выполнено">Выполнено</option>
|
||
<option value="Просрочено">Просрочено</option>
|
||
</select>
|
||
<select id="filterRm">
|
||
<option value="">Все РМ</option>
|
||
</select>
|
||
<input type="date" id="filterFrom" placeholder="С даты">
|
||
<input type="date" id="filterTo" placeholder="По дату">
|
||
<button onclick="applyFilters()">Применить</button>
|
||
<button onclick="clearFilters()">Сбросить</button>
|
||
<button onclick="exportCsv()" class="export-btn">Выгрузить в Excel</button>
|
||
</section>
|
||
|
||
<section class="alerts">
|
||
<h2>⚠️ Просроченные поручения</h2>
|
||
<div id="overdueList" class="list"></div>
|
||
</section>
|
||
|
||
<section class="coming-soon">
|
||
<h2>📅 Ближайшие сроки исполнения</h2>
|
||
<div id="comingList" class="list"></div>
|
||
</section>
|
||
|
||
<section class="critical">
|
||
<h2>🔴 Критические поручения (просрочено > 3 дней)</h2>
|
||
<div id="criticalList" class="list"></div>
|
||
</section>
|
||
</main>
|
||
|
||
<script src="auth.js"></script>
|
||
<script>
|
||
// Проверка авторизации
|
||
const user = requireAuth();
|
||
renderUserMenu();
|
||
|
||
// Показываем кнопку "Новое поручение" только для admin/pmo
|
||
if (canEdit()) {
|
||
document.getElementById("newBtn").style.display = "inline-block";
|
||
}
|
||
|
||
let allInstructions = [];
|
||
|
||
async function load() {
|
||
const res = await fetch("/api/instructions");
|
||
allInstructions = await res.json();
|
||
renderMetrics();
|
||
renderFilters();
|
||
renderLists();
|
||
}
|
||
|
||
function renderMetrics() {
|
||
const total = allInstructions.length;
|
||
const done = allInstructions.filter(i => i.status === "Выполнено").length;
|
||
const progress = allInstructions.filter(i => i.status === "В работе").length;
|
||
const control = allInstructions.filter(i => i.status === "На контроле").length;
|
||
const overdue = allInstructions.filter(i => i.status === "Просрочено").length;
|
||
|
||
document.getElementById("total").textContent = total;
|
||
document.getElementById("done").textContent = done;
|
||
document.getElementById("progress").textContent = progress;
|
||
document.getElementById("control").textContent = control;
|
||
document.getElementById("overdue").textContent = overdue;
|
||
}
|
||
|
||
function renderFilters() {
|
||
const rms = [...new Set(allInstructions.map(i => i.rm).filter(Boolean))];
|
||
const select = document.getElementById("filterRm");
|
||
rms.forEach(rm => {
|
||
const opt = document.createElement("option");
|
||
opt.value = rm;
|
||
opt.textContent = rm;
|
||
select.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
function renderLists() {
|
||
const today = new Date();
|
||
const overdue = allInstructions.filter(i => i.status === "Просрочено");
|
||
const critical = overdue.filter(i => {
|
||
const plan = new Date(i.planDate);
|
||
const diff = Math.floor((today - plan) / (1000 * 60 * 60 * 24));
|
||
return diff > 3;
|
||
});
|
||
const coming = allInstructions
|
||
.filter(i => i.status !== "Выполнено" && i.status !== "Просрочено")
|
||
.sort((a, b) => new Date(a.planDate) - new Date(b.planDate))
|
||
.slice(0, 5);
|
||
|
||
document.getElementById("overdueList").innerHTML = overdue.length
|
||
? overdue.map(i => `<div class="list-item overdue"><a href="card.html?id=${i.id}">${i.title}</a><span>РМ: ${i.rm || "—"}</span><span>Срок: ${i.planDate}</span></div>`).join("")
|
||
: "<p>Нет просроченных поручений</p>";
|
||
|
||
document.getElementById("criticalList").innerHTML = critical.length
|
||
? critical.map(i => `<div class="list-item critical"><a href="card.html?id=${i.id}">${i.title}</a><span>Просрочено на ${Math.floor((today - new Date(i.planDate)) / (1000 * 60 * 60 * 24))} дн.</span></div>`).join("")
|
||
: "<p>Нет критических поручений</p>";
|
||
|
||
document.getElementById("comingList").innerHTML = coming.length
|
||
? coming.map(i => `<div class="list-item"><a href="card.html?id=${i.id}">${i.title}</a><span>РМ: ${i.rm || "—"}</span><span>Срок: ${i.planDate}</span></div>`).join("")
|
||
: "<p>Нет поручений с ближайшими сроками</p>";
|
||
}
|
||
|
||
function applyFilters() {
|
||
const status = document.getElementById("filterStatus").value;
|
||
const rm = document.getElementById("filterRm").value;
|
||
const from = document.getElementById("filterFrom").value;
|
||
const to = document.getElementById("filterTo").value;
|
||
|
||
let filtered = allInstructions;
|
||
if (status) filtered = filtered.filter(i => i.status === status);
|
||
if (rm) filtered = filtered.filter(i => i.rm === rm);
|
||
if (from) filtered = filtered.filter(i => i.issueDate >= from);
|
||
if (to) filtered = filtered.filter(i => i.issueDate <= to);
|
||
|
||
const total = filtered.length;
|
||
const done = filtered.filter(i => i.status === "Выполнено").length;
|
||
const progress = filtered.filter(i => i.status === "В работе").length;
|
||
const control = filtered.filter(i => i.status === "На контроле").length;
|
||
const overdue = filtered.filter(i => i.status === "Просрочено").length;
|
||
|
||
document.getElementById("total").textContent = total;
|
||
document.getElementById("done").textContent = done;
|
||
document.getElementById("progress").textContent = progress;
|
||
document.getElementById("control").textContent = control;
|
||
document.getElementById("overdue").textContent = overdue;
|
||
|
||
const today = new Date();
|
||
const overdueList = filtered.filter(i => i.status === "Просрочено");
|
||
const critical = overdueList.filter(i => {
|
||
const plan = new Date(i.planDate);
|
||
const diff = Math.floor((today - plan) / (1000 * 60 * 60 * 24));
|
||
return diff > 3;
|
||
});
|
||
const coming = filtered
|
||
.filter(i => i.status !== "Выполнено" && i.status !== "Просрочено")
|
||
.sort((a, b) => new Date(a.planDate) - new Date(b.planDate))
|
||
.slice(0, 5);
|
||
|
||
document.getElementById("overdueList").innerHTML = overdueList.length
|
||
? overdueList.map(i => `<div class="list-item overdue"><a href="card.html?id=${i.id}">${i.title}</a><span>РМ: ${i.rm || "—"}</span><span>Срок: ${i.planDate}</span></div>`).join("")
|
||
: "<p>Нет просроченных поручений</p>";
|
||
|
||
document.getElementById("criticalList").innerHTML = critical.length
|
||
? critical.map(i => `<div class="list-item critical"><a href="card.html?id=${i.id}">${i.title}</a><span>Просрочено на ${Math.floor((today - new Date(i.planDate)) / (1000 * 60 * 60 * 24))} дн.</span></div>`).join("")
|
||
: "<p>Нет критических поручений</p>";
|
||
|
||
document.getElementById("comingList").innerHTML = coming.length
|
||
? coming.map(i => `<div class="list-item"><a href="card.html?id=${i.id}">${i.title}</a><span>РМ: ${i.rm || "—"}</span><span>Срок: ${i.planDate}</span></div>`).join("")
|
||
: "<p>Нет поручений с ближайшими сроками</p>";
|
||
}
|
||
|
||
function clearFilters() {
|
||
document.getElementById("filterStatus").value = "";
|
||
document.getElementById("filterRm").value = "";
|
||
document.getElementById("filterFrom").value = "";
|
||
document.getElementById("filterTo").value = "";
|
||
load();
|
||
}
|
||
|
||
function exportCsv() {
|
||
window.open("/api/export", "_blank");
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|