287 lines
12 KiB
HTML
287 lines
12 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 id="pageTitle">Карточка поручения</h1>
|
||
</div>
|
||
<nav class="nav" id="mainNav">
|
||
<a href="index.html">Дашборд</a>
|
||
<a href="registry.html">Реестр</a>
|
||
<a href="card.html?id=new" id="newBtn" style="display:none">Новое поручение</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<main class="card-page">
|
||
<div class="card-form">
|
||
<h2 id="formTitle">Новое поручение</h2>
|
||
<form id="instructionForm">
|
||
<input type="hidden" id="instructionId">
|
||
|
||
<div class="form-group">
|
||
<label for="title">Поручение *</label>
|
||
<textarea id="title" name="title" required placeholder="Формулировка поручения СД"></textarea>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="status">Статус *</label>
|
||
<select id="status" name="status" required>
|
||
<option value="В работе">В работе</option>
|
||
<option value="На контроле">На контроле</option>
|
||
<option value="Выполнено">Выполнено</option>
|
||
<option value="Просрочено">Просрочено</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="rm">РМ (Ответственный менеджер)</label>
|
||
<input type="text" id="rm" name="rm" placeholder="ФИО руководителя проекта">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="issueDate">Дата выдачи *</label>
|
||
<input type="date" id="issueDate" name="issueDate" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="planDate">Плановый срок *</label>
|
||
<input type="date" id="planDate" name="planDate" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="factDate">Фактический срок</label>
|
||
<input type="date" id="factDate" name="factDate">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="delegate">Исполнитель (делегирование)</label>
|
||
<input type="text" id="delegate" name="delegate" placeholder="Подразделение или ФИО">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="note">Примечание</label>
|
||
<textarea id="note" name="note" placeholder="Комментарии и уточнения"></textarea>
|
||
</div>
|
||
|
||
<div class="btn-group">
|
||
<button type="submit" class="btn btn-primary" id="saveBtn">Сохранить</button>
|
||
<button type="button" class="btn btn-secondary" onclick="window.location='registry.html'">Отмена</button>
|
||
<button type="button" class="btn btn-danger" id="deleteBtn" onclick="deleteInstruction()" style="display:none">Удалить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="answers-section" id="answersSection" style="display:none">
|
||
<h3>Промежуточные ответы</h3>
|
||
<div class="answers-list" id="answersList"></div>
|
||
<div style="margin-top:16px;display:flex;gap:12px">
|
||
<input type="text" id="answerText" placeholder="Добавить комментарий о ходе выполнения" style="flex:1;padding:10px;border:1px solid #ddd;border-radius:6px">
|
||
<input type="text" id="answerAuthor" placeholder="Автор" style="width:200px;padding:10px;border:1px solid #ddd;border-radius:6px">
|
||
<button class="btn btn-primary" onclick="addAnswer()">Добавить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="history-section" id="historySection" style="display:none">
|
||
<h3>История изменений</h3>
|
||
<div class="history-list" id="historyList"></div>
|
||
</div>
|
||
|
||
<div class="history-section" id="timelineSection" style="display:none">
|
||
<h3>Временная шкала исполнения</h3>
|
||
<div class="timeline" id="timeline"></div>
|
||
</div>
|
||
</main>
|
||
|
||
<script src="auth.js"></script>
|
||
<script>
|
||
const user = requireAuth();
|
||
renderUserMenu();
|
||
|
||
// Права на редактирование
|
||
const canEditAccess = canEdit();
|
||
const canDeleteAccess = canDelete();
|
||
|
||
if (canEditAccess) {
|
||
document.getElementById("newBtn").style.display = "inline-block";
|
||
}
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const instructionId = urlParams.get("id");
|
||
let instruction = null;
|
||
|
||
// Блокируем форму для тех, кто не может редактировать
|
||
if (!canEditAccess && instructionId !== "new") {
|
||
document.getElementById("title").disabled = true;
|
||
document.getElementById("status").disabled = true;
|
||
document.getElementById("rm").disabled = true;
|
||
document.getElementById("issueDate").disabled = true;
|
||
document.getElementById("planDate").disabled = true;
|
||
document.getElementById("factDate").disabled = true;
|
||
document.getElementById("delegate").disabled = true;
|
||
document.getElementById("note").disabled = true;
|
||
document.getElementById("saveBtn").style.display = "none";
|
||
}
|
||
|
||
async function load() {
|
||
if (instructionId === "new") {
|
||
if (!canEditAccess) {
|
||
alert("У вас нет прав на создание поручений");
|
||
window.location = "index.html";
|
||
return;
|
||
}
|
||
document.getElementById("formTitle").textContent = "Новое поручение";
|
||
document.getElementById("deleteBtn").style.display = "none";
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
document.getElementById("issueDate").value = today;
|
||
document.getElementById("answerAuthor").value = user.name;
|
||
return;
|
||
}
|
||
|
||
const res = await fetch(`/api/instructions/${instructionId}`);
|
||
if (!res.ok) {
|
||
alert("Поручение не найдено");
|
||
window.location = "registry.html";
|
||
return;
|
||
}
|
||
instruction = await res.json();
|
||
|
||
document.getElementById("formTitle").textContent = `Поручение №${instruction.id}`;
|
||
document.getElementById("pageTitle").textContent = `Поручение №${instruction.id}`;
|
||
document.getElementById("instructionId").value = instruction.id;
|
||
document.getElementById("title").value = instruction.title || "";
|
||
document.getElementById("status").value = instruction.status || "В работе";
|
||
document.getElementById("rm").value = instruction.rm || "";
|
||
document.getElementById("issueDate").value = instruction.issueDate || "";
|
||
document.getElementById("planDate").value = instruction.planDate || "";
|
||
document.getElementById("factDate").value = instruction.factDate || "";
|
||
document.getElementById("delegate").value = instruction.delegate || "";
|
||
document.getElementById("note").value = instruction.note || "";
|
||
|
||
if (canDeleteAccess) {
|
||
document.getElementById("deleteBtn").style.display = "inline-block";
|
||
}
|
||
document.getElementById("answerAuthor").value = user.name;
|
||
|
||
// Answers
|
||
if (instruction.answers && instruction.answers.length) {
|
||
document.getElementById("answersSection").style.display = "block";
|
||
document.getElementById("answersList").innerHTML = instruction.answers.map(a => `
|
||
<div class="answer-item">
|
||
<div class="author">${a.author || "Автор"} — ${a.date || ""}</div>
|
||
<div class="text">${a.text || ""}</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
// History
|
||
if (instruction.history && instruction.history.length) {
|
||
document.getElementById("historySection").style.display = "block";
|
||
document.getElementById("historyList").innerHTML = instruction.history.map(h => `
|
||
<div class="history-item">
|
||
<div class="date">${h.date}</div>
|
||
<div><strong>${h.user}</strong>: ${h.field} → ${h.newValue}</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
// Timeline
|
||
if (instruction.history && instruction.history.length) {
|
||
document.getElementById("timelineSection").style.display = "block";
|
||
const sorted = [...instruction.history].sort((a, b) => new Date(b.date) - new Date(a.date));
|
||
document.getElementById("timeline").innerHTML = sorted.map(h => `
|
||
<div class="timeline-item ${h.field === 'Создано' ? 'done' : h.field === 'Статус' && h.newValue === 'Просрочено' ? 'overdue' : ''}">
|
||
<div class="timeline-date">${h.date}</div>
|
||
<div class="timeline-title">${h.user}: ${h.field}</div>
|
||
<div class="timeline-desc">${h.newValue}</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
}
|
||
|
||
document.getElementById("instructionForm").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
if (!canEditAccess) {
|
||
alert("У вас нет прав на редактирование");
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
title: document.getElementById("title").value,
|
||
status: document.getElementById("status").value,
|
||
rm: document.getElementById("rm").value,
|
||
issueDate: document.getElementById("issueDate").value,
|
||
planDate: document.getElementById("planDate").value,
|
||
factDate: document.getElementById("factDate").value,
|
||
delegate: document.getElementById("delegate").value,
|
||
note: document.getElementById("note").value,
|
||
user: user.name
|
||
};
|
||
|
||
const url = instructionId === "new" ? "/api/instructions" : `/api/instructions/${instructionId}`;
|
||
const method = instructionId === "new" ? "POST" : "PUT";
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (res.ok) {
|
||
const result = await res.json();
|
||
window.location = instructionId === "new" ? `card.html?id=${result.id}` : "registry.html";
|
||
} else {
|
||
alert("Ошибка сохранения");
|
||
}
|
||
});
|
||
|
||
async function addAnswer() {
|
||
if (!instructionId || instructionId === "new") return;
|
||
const text = document.getElementById("answerText").value;
|
||
const author = document.getElementById("answerAuthor").value || user.name;
|
||
if (!text) return;
|
||
|
||
const res = await fetch(`/api/instructions/${instructionId}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
answer: { text, author },
|
||
user: user.name
|
||
})
|
||
});
|
||
|
||
if (res.ok) {
|
||
document.getElementById("answerText").value = "";
|
||
load();
|
||
} else {
|
||
alert("Ошибка добавления ответа");
|
||
}
|
||
}
|
||
|
||
async function deleteInstruction() {
|
||
if (!canDeleteAccess) {
|
||
alert("У вас нет прав на удаление");
|
||
return;
|
||
}
|
||
if (!confirm("Удалить это поручение?")) return;
|
||
const res = await fetch(`/api/instructions/${instructionId}`, { method: "DELETE" });
|
||
if (res.ok) {
|
||
window.location = "registry.html";
|
||
} else {
|
||
alert("Ошибка удаления");
|
||
}
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|