tehnicheskoe-zadanie-na-razr/index.html

749 lines
33 KiB
HTML
Raw Permalink 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.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Секретарь комиссии</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f7fa; color: #1a1a1a; }
.header { background: #1e3a5f; color: white; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 20px; font-weight: 600; }
.header nav { display: flex; gap: 8px; }
.header nav button { background: rgba(255,255,255,0.1); border: none; color: white; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
.header nav button:hover, .header nav button.active { background: rgba(255,255,255,0.2); }
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-card .value { font-size: 32px; font-weight: 700; color: #1e3a5f; }
.stat-card .label { font-size: 14px; color: #666; margin-top: 4px; }
.card { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
.card h2 { font-size: 18px; margin-bottom: 16px; color: #1e3a5f; }
.btn { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #1e3a5f; color: white; }
.btn-primary:hover { background: #2a4a6f; }
.btn-secondary { background: #e0e0e0; color: #333; }
.btn-secondary:hover { background: #d0d0d0; }
.btn-success { background: #22c55e; color: white; }
.btn-danger { background: #ef4444; color: white; }
.btn-warning { background: #f59e0b; color: white; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: #333; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid #d0d0d0; border-radius: 6px; font-size: 14px; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #1e3a5f; }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; }
.upload-zone { border: 2px dashed #d0d0d0; border-radius: 8px; padding: 40px; text-align: center; cursor: pointer; transition: all 0.3s; }
.upload-zone:hover, .upload-zone.dragover { border-color: #1e3a5f; background: #f0f4f8; }
.upload-zone p { color: #666; margin-bottom: 8px; }
.upload-zone input { display: none; }
.doc-list { margin-top: 16px; }
.doc-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; border: 1px solid #e0e0e0; border-radius: 6px; margin-bottom: 8px; }
.doc-item .info { flex: 1; }
.doc-item .name { font-weight: 500; }
.doc-item .meta { font-size: 12px; color: #666; margin-top: 4px; }
.doc-item .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; margin-left: 8px; }
.badge-success { background: #dcfce7; color: #166534; }
.badge-warning { background: #fef3c7; color: #92400e; }
.badge-danger { background: #fee2e2; color: #991b1b; }
.badge-info { background: #dbeafe; color: #1e40af; }
.status-indicator { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 20px; font-weight: 500; }
.status-green { background: #dcfce7; color: #166534; }
.status-yellow { background: #fef3c7; color: #92400e; }
.status-red { background: #fee2e2; color: #991b1b; }
.remark-card { border-left: 4px solid #e0e0e0; padding: 12px 16px; background: #f9f9f9; margin-bottom: 12px; border-radius: 0 6px 6px 0; }
.remark-card.crit { border-left-color: #ef4444; background: #fef2f2; }
.remark-card.warn { border-left-color: #f59e0b; background: #fffbeb; }
.remark-card.tech { border-left-color: #6b7280; background: #f3f4f6; }
.remark-card .category { font-size: 11px; font-weight: 600; text-transform: uppercase; margin-bottom: 6px; }
.remark-card .text { font-size: 14px; margin-bottom: 8px; }
.remark-card .actions { display: flex; gap: 8px; margin-top: 8px; }
.remark-card .actions button { font-size: 12px; padding: 4px 10px; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: 12px; text-align: left; border-bottom: 1px solid #e0e0e0; }
.table th { font-weight: 600; color: #666; font-size: 13px; }
.table tr:hover { background: #f9f9f9; }
.tabs { display: flex; border-bottom: 2px solid #e0e0e0; margin-bottom: 20px; }
.tab { padding: 12px 20px; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; }
.tab.active { border-bottom-color: #1e3a5f; color: #1e3a5f; font-weight: 500; }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.hidden { display: none; }
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal { background: white; border-radius: 8px; padding: 24px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal h3 { margin-bottom: 16px; }
.checklist { list-style: none; }
.checklist li { padding: 8px 0; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 8px; }
.checklist li:last-child { border-bottom: none; }
.checklist input[type="checkbox"] { width: 18px; height: 18px; }
.completeness-table { width: 100%; border-collapse: collapse; margin-top: 16px; }
.completeness-table th, .completeness-table td { padding: 10px; border: 1px solid #e0e0e0; text-align: left; }
.completeness-table th { background: #f5f7fa; }
</style>
</head>
<body>
<div class="header">
<h1>📋 Секретарь комиссии</h1>
<nav>
<button class="active" data-view="dashboard">Главная</button>
<button data-view="new-check">Новая проверка</button>
<button data-view="checks">Проверки</button>
<button data-view="templates">Шаблоны</button>
<button data-view="remarks">Типовые замечания</button>
</nav>
</div>
<div class="container">
<div id="view-dashboard">
<div class="stats">
<div class="stat-card">
<div class="value" id="stat-new">0</div>
<div class="label">Новые</div>
</div>
<div class="stat-card">
<div class="value" id="stat-in-progress">0</div>
<div class="label">На проверке</div>
</div>
<div class="stat-card">
<div class="value" id="stat-revision">0</div>
<div class="label">На доработке</div>
</div>
<div class="stat-card">
<div class="value" id="stat-ready">0</div>
<div class="label">Готовы к комиссии</div>
</div>
<div class="stat-card">
<div class="value" id="stat-critical">0</div>
<div class="label">Критические замечания</div>
</div>
</div>
<div class="card">
<h2>Последние проверки</h2>
<table class="table">
<thead>
<tr>
<th>Дата</th>
<th>Вопрос</th>
<th>Инициатор</th>
<th>Договор</th>
<th>Статус</th>
<th>Замечания</th>
</tr>
</thead>
<tbody id="recent-checks"></tbody>
</table>
</div>
</div>
<div id="view-new-check" class="hidden">
<div class="card">
<h2>Новая проверка</h2>
<form id="check-form">
<div class="form-row">
<div class="form-group">
<label>Наименование вопроса *</label>
<input type="text" name="question" required placeholder="Например: Рассмотрение штрафных санкций по договору №123">
</div>
<div class="form-group">
<label>Тип вопроса *</label>
<select name="questionType" required id="question-type">
<option value="">Выберите тип</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Инициатор</label>
<input type="text" name="initiator" placeholder="ФИО или подразделение">
</div>
<div class="form-group">
<label>Структурное подразделение</label>
<input type="text" name="department" placeholder="Название подразделения">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Исполнитель</label>
<input type="text" name="performer" placeholder="ФИО исполнителя">
</div>
<div class="form-group">
<label>Комиссия</label>
<input type="text" name="commission" placeholder="Название комиссии">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Номер договора</label>
<input type="text" name="contractNumber" placeholder="№ договора">
</div>
<div class="form-group">
<label>Дата договора</label>
<input type="date" name="contractDate">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Контрагент</label>
<input type="text" name="counterparty" placeholder="Название организации">
</div>
<div class="form-group">
<label>Дата предполагаемого заседания</label>
<input type="date" name="meetingDate">
</div>
</div>
<div class="form-group">
<label>Комментарий</label>
<textarea name="comment" rows="3" placeholder="Дополнительная информация"></textarea>
</div>
<button type="submit" class="btn btn-primary">Создать проверку</button>
</form>
</div>
</div>
<div id="view-check-detail" class="hidden">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 id="detail-title">Проверка</h2>
<div id="detail-status"></div>
</div>
<div class="tabs">
<div class="tab active" data-tab="documents">Документы</div>
<div class="tab" data-tab="completeness">Комплектность</div>
<div class="tab" data-tab="analysis">Анализ</div>
<div class="tab" data-tab="report">Отчёт</div>
</div>
<div id="tab-documents">
<div class="upload-zone" id="upload-zone">
<p>📎 Перетащите файлы сюда или нажмите для выбора</p>
<p style="font-size: 13px; color: #999;">PDF, DOCX, XLSX, XLS, JPG, PNG (макс. 50 МБ)</p>
<input type="file" id="file-input" multiple accept=".pdf,.docx,.xlsx,.xls,.jpg,.jpeg,.png,.txt">
</div>
<div class="doc-list" id="doc-list"></div>
<div style="margin-top: 20px;">
<button class="btn btn-success" id="btn-analyze">🔍 Провести анализ</button>
</div>
</div>
<div id="tab-completeness" class="hidden">
<h3>Проверка комплектности</h3>
<table class="completeness-table" id="completeness-table">
<thead>
<tr>
<th>Требуемый документ</th>
<th>Представлен</th>
<th>Результат</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="tab-analysis" class="hidden">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3>Выявленные замечания</h3>
<button class="btn btn-primary" onclick="openAddRemarkModal()">+ Добавить замечание</button>
</div>
<div id="remarks-list" style="margin-top: 16px;"></div>
</div>
<div id="tab-report" class="hidden">
<h3>Итоговое заключение</h3>
<div id="report-content" style="margin-top: 16px;"></div>
<div style="margin-top: 20px; display: flex; gap: 10px;">
<button class="btn btn-primary" id="btn-export-word">📄 Экспорт в Word</button>
<button class="btn btn-secondary" id="btn-copy-response">📋 Копировать ответ</button>
</div>
</div>
</div>
</div>
<div id="view-checks" class="hidden">
<div class="card">
<h2>Все проверки</h2>
<table class="table">
<thead>
<tr>
<th>Дата</th>
<th>Вопрос</th>
<th>Инициатор</th>
<th>Договор</th>
<th>Контрагент</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="all-checks"></tbody>
</table>
</div>
</div>
<div id="view-templates" class="hidden">
<div class="card">
<h2>Шаблоны комплектов документов</h2>
<div id="templates-list"></div>
</div>
</div>
<div id="view-remarks" class="hidden">
<div class="card">
<h2>Типовые замечания</h2>
<div id="remarks-db-list"></div>
</div>
</div>
</div>
<div id="modal-add-remark" class="modal-overlay hidden">
<div class="modal">
<h3>Добавить замечание</h3>
<form id="add-remark-form">
<div class="form-group">
<label>Категория</label>
<select name="category" required>
<option value="КРИТИЧНО">КРИТИЧНО</option>
<option value="ТРЕБУЕТ ДОРАБОТКИ">ТРЕБУЕТ ДОРАБОТКИ</option>
<option value="ТЕХНИЧЕСКОЕ">ТЕХНИЧЕСКОЕ</option>
</select>
</div>
<div class="form-group">
<label>Тип</label>
<input type="text" name="type" placeholder="Например: Расхождение">
</div>
<div class="form-group">
<label>Документ</label>
<input type="text" name="document" placeholder="Название документа">
</div>
<div class="form-group">
<label>Текст замечания *</label>
<textarea name="text" required rows="3"></textarea>
</div>
<div class="form-group">
<label>Рекомендация</label>
<textarea name="recommendation" rows="2"></textarea>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="submit" class="btn btn-primary">Добавить</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
</div>
</form>
</div>
</div>
<script>
let currentCheckId = null;
let db = { checks: [], templates: [], typicalRemarks: [] };
async function load() {
try {
const res = await fetch("/api/data");
db = await res.json();
updateStats();
renderRecentChecks();
renderTemplates();
renderRemarksDb();
populateQuestionTypes();
} catch (err) {
console.error("Ошибка загрузки данных:", err);
}
}
function updateStats() {
document.getElementById("stat-new").textContent = db.checks.filter(c => c.status === "Новая").length;
document.getElementById("stat-in-progress").textContent = db.checks.filter(c => c.status === "На проверке").length;
document.getElementById("stat-revision").textContent = db.checks.filter(c => c.status === "На доработке").length;
document.getElementById("stat-ready").textContent = db.checks.filter(c => c.status === "Готово к комиссии").length;
document.getElementById("stat-critical").textContent = db.checks.reduce((sum, c) => sum + (c.remarks?.filter(r => r.category === "КРИТИЧНО" && r.confirmed).length || 0), 0);
}
function renderRecentChecks() {
const recent = db.checks.slice(-10).reverse();
document.getElementById("recent-checks").innerHTML = recent.map(c => `
<tr>
<td>${new Date(c.createdAt).toLocaleDateString("ru-RU")}</td>
<td>${c.question || "—"}</td>
<td>${c.initiator || "—"}</td>
<td>${c.contractNumber || "—"}</td>
<td>${renderStatus(c.status)}</td>
<td>${c.remarks?.filter(r => r.confirmed).length || 0}</td>
</tr>
`).join("");
}
function renderStatus(status) {
const map = {
"Новая": "badge-info",
"На проверке": "badge-warning",
"На доработке": "badge-danger",
"Готово к комиссии": "badge-success"
};
return `<span class="badge ${map[status] || "badge-info"}">${status}</span>`;
}
function populateQuestionTypes() {
const select = document.getElementById("question-type");
db.templates.forEach(t => {
const opt = document.createElement("option");
opt.value = t.name;
opt.textContent = t.name;
select.appendChild(opt);
});
}
document.getElementById("check-form").addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
try {
const res = await fetch("/api/checks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
const result = await res.json();
if (result.ok) {
currentCheckId = result.check.id;
showView("check-detail");
loadCheckDetail(currentCheckId);
} else {
alert("Ошибка создания проверки: " + (result.error || "Неизвестная ошибка"));
}
} catch (err) {
alert("Ошибка создания проверки: " + err.message);
}
});
async function loadCheckDetail(id) {
try {
const res = await fetch(`/api/checks/${id}`);
const check = await res.json();
if (!check || !check.id) {
alert("Проверка не найдена");
showView("checks");
return;
}
document.getElementById("detail-title").textContent = check.question || "Проверка";
const statusEl = document.getElementById("detail-status");
const statusClass = check.status === "Готово к комиссии" ? "status-green" : check.status === "На доработке" ? "status-red" : "status-yellow";
statusEl.innerHTML = `<span class="status-indicator ${statusClass}">${check.status || "Новая"}</span>`;
renderDocuments(check.documents || []);
renderCompleteness(check);
renderRemarks(check.remarks || []);
renderReport(check);
} catch (err) {
console.error("Ошибка загрузки проверки:", err);
alert("Ошибка загрузки данных проверки");
}
}
function renderDocuments(docs) {
document.getElementById("doc-list").innerHTML = docs.map(d => `
<div class="doc-item">
<div class="info">
<div class="name">${d.name}
<span class="badge badge-info">${d.docType || "Не определён"}</span>
</div>
<div class="meta">${d.format} • ${(d.size / 1024).toFixed(1)} КБ • ${d.pages} стр.</div>
</div>
<span class="badge ${d.status === "Обработан" ? "badge-success" : "badge-warning"}">${d.status}</span>
</div>
`).join("");
}
function renderCompleteness(check) {
const template = db.templates.find(t => t.name === check.questionType);
const providedTypes = (check.documents || []).map(d => d.docType);
if (!template) {
document.getElementById("completeness-table").querySelector("tbody").innerHTML = "<tr><td colspan='3'>Шаблон для этого типа вопроса не найден</td></tr>";
return;
}
document.getElementById("completeness-table").querySelector("tbody").innerHTML = template.requiredDocs.map(req => {
const isProvided = providedTypes.some(t => t.includes(req) || req.includes(t));
const result = isProvided ? "Соответствует" : "Отсутствует";
const badge = isProvided ? "badge-success" : "badge-danger";
return `
<tr>
<td>${req}</td>
<td>${isProvided ? "Да" : "Нет"}</td>
<td><span class="badge ${badge}">${result}</span></td>
</tr>
`;
}).join("");
}
function renderRemarks(remarks) {
const safeText = (text) => String(text || "").replace(/"/g, '&quot;').replace(/'/g, '&#39;');
document.getElementById("remarks-list").innerHTML = remarks.map(r => `
<div class="remark-card ${r.category === "КРИТИЧНО" ? "crit" : r.category === "ТРЕБУЕТ ДОРАБОТКИ" ? "warn" : "tech"}">
<div class="category">${r.category} • ${r.type}</div>
<div class="text"><strong>${r.document || "Общее"}:</strong> ${safeText(r.text)}</div>
<div style="font-size: 13px; color: #666;">${safeText(r.recommendation || "")}</div>
<div class="actions">
<button class="btn btn-secondary" onclick="toggleRemark(${r.id})">${r.confirmed ? "✓ Подтверждено" : "Подтвердить"}</button>
<button class="btn btn-secondary" onclick="deleteRemark(${r.id})">Отклонить</button>
</div>
</div>
`).join("");
}
function renderReport(check) {
const critical = (check.remarks || []).filter(r => r.category === "КРИТИЧНО" && r.confirmed).length;
const warning = (check.remarks || []).filter(r => r.category === "ТРЕБУЕТ ДОРАБОТКИ" && r.confirmed).length;
const tech = (check.remarks || []).filter(r => r.category === "ТЕХНИЧЕСКОЕ" && r.confirmed).length;
const verdict = critical === 0 ? "🟢 Можно принимать к рассмотрению" : "🔴 Вернуть на доработку";
document.getElementById("report-content").innerHTML = `
<div class="card" style="background: ${critical === 0 ? "#f0fdf4" : "#fef2f2"}; border: 1px solid ${critical === 0 ? "#22c55e" : "#ef4444"};">
<h3 style="margin-bottom: 12px;">${verdict}</h3>
</div>
<div class="grid-2" style="margin-top: 20px;">
<div>
<h4>📊 Статистика</h4>
<ul class="checklist" style="margin-top: 10px;">
<li>Документов: ${(check.documents || []).length}</li>
<li>Критических замечаний: ${critical}</li>
<li>Требует доработки: ${warning}</li>
<li>Технических: ${tech}</li>
</ul>
</div>
<div>
<h4>📋 Контрольный чек-лист</h4>
<ul class="checklist" style="margin-top: 10px;">
<li><input type="checkbox" ${critical === 0 ? "checked" : ""}> Комплектность документов</li>
<li><input type="checkbox" ${critical === 0 ? "checked" : ""}> Подписи и реквизиты</li>
<li><input type="checkbox" ${critical === 0 ? "checked" : ""}> Даты и сроки</li>
<li><input type="checkbox" ${critical === 0 ? "checked" : ""}> Суммы и арифметика</li>
</ul>
</div>
</div>
<div style="margin-top: 20px;">
<h4>📝 Ответ исполнителю</h4>
<div id="response-text" style="background: #f9f9f9; padding: 16px; border-radius: 6px; margin-top: 10px; font-size: 14px; line-height: 1.6;">
<strong>По результатам предварительной проверки материалов, представленных для вынесения вопроса на рассмотрение Заседания комиссии, необходимо дополнительно представить/скорректировать следующее:</strong><br><br>
${(check.remarks || []).filter(r => r.confirmed).map((r, i) => `${i + 1}. ${String(r.text).replace(/</g, '&lt;').replace(/>/g, '&gt;')}`).join("<br>") || "Замечаний нет. Материалы могут быть рассмотрены на комиссии."}<br><br>
<strong>После устранения указанных замечаний просим повторно направить комплект документов для рассмотрения.</strong>
</div>
</div>
`;
}
const uploadZone = document.getElementById("upload-zone");
const fileInput = document.getElementById("file-input");
uploadZone.addEventListener("click", () => fileInput.click());
uploadZone.addEventListener("dragover", (e) => { e.preventDefault(); uploadZone.classList.add("dragover"); });
uploadZone.addEventListener("dragleave", () => uploadZone.classList.remove("dragover"));
uploadZone.addEventListener("drop", (e) => {
e.preventDefault();
uploadZone.classList.remove("dragover");
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener("change", (e) => handleFiles(e.target.files));
async function handleFiles(files) {
if (!currentCheckId) {
alert("Сначала создайте проверку!");
return;
}
for (const file of files) {
const formData = new FormData();
formData.append("file", file);
try {
const res = await fetch(`/api/upload?checkId=${currentCheckId}`, {
method: "POST",
body: formData
});
const result = await res.json();
if (!result.ok && result.error) {
alert(`Ошибка загрузки файла ${file.name}: ${result.error}`);
}
} catch (err) {
alert(`Ошибка загрузки файла ${file.name}: ${err.message}`);
}
}
loadCheckDetail(currentCheckId);
}
document.getElementById("btn-analyze").addEventListener("click", async () => {
const res = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ checkId: currentCheckId })
});
const result = await res.json();
if (result.ok) {
loadCheckDetail(currentCheckId);
}
});
async function toggleRemark(id) {
const check = await (await fetch(`/api/checks/${currentCheckId}`)).json();
const remark = check.remarks.find(r => r.id === id);
if (remark) {
remark.confirmed = !remark.confirmed;
await fetch(`/api/checks/${currentCheckId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ remarks: check.remarks })
});
renderRemarks(check.remarks);
renderReport(check);
load();
}
}
async function deleteRemark(id) {
const check = await (await fetch(`/api/checks/${currentCheckId}`)).json();
check.remarks = check.remarks.filter(r => r.id !== id);
await fetch(`/api/checks/${currentCheckId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ remarks: check.remarks })
});
renderRemarks(check.remarks);
renderReport(check);
load();
}
document.getElementById("btn-copy-response").addEventListener("click", () => {
const text = document.getElementById("response-text").innerText;
navigator.clipboard.writeText(text);
alert("Текст ответа скопирован в буфер обмена");
});
function renderTemplates() {
document.getElementById("templates-list").innerHTML = db.templates.map(t => `
<div class="card" style="padding: 16px;">
<h3>${t.name}</h3>
<ul class="checklist" style="margin-top: 10px;">
${t.requiredDocs.map(d => `<li>📄 ${d}</li>`).join("")}
</ul>
</div>
`).join("");
}
function renderRemarksDb() {
document.getElementById("remarks-db-list").innerHTML = db.typicalRemarks.map(r => `
<div class="remark-card ${r.category === "КРИТИЧНО" ? "crit" : r.category === "ТРЕБУЕТ ДОРАБОТКИ" ? "warn" : "tech"}">
<div class="category">${r.category} • ${r.type}</div>
<div class="text">${r.text}</div>
</div>
`).join("");
}
function showView(view) {
document.querySelectorAll("[id^='view-']").forEach(el => el.classList.add("hidden"));
document.getElementById(`view-${view}`).classList.remove("hidden");
document.querySelectorAll(".header nav button").forEach(btn => btn.classList.remove("active"));
document.querySelector(`[data-view="${view}"]`)?.classList.add("active");
}
document.querySelectorAll(".header nav button").forEach(btn => {
btn.addEventListener("click", () => {
const view = btn.dataset.view;
showView(view);
if (view === "checks") renderAllChecks();
});
});
function renderAllChecks() {
document.getElementById("all-checks").innerHTML = db.checks.map(c => `
<tr>
<td>${new Date(c.createdAt).toLocaleDateString("ru-RU")}</td>
<td>${c.question || "—"}</td>
<td>${c.initiator || "—"}</td>
<td>${c.contractNumber || "—"}</td>
<td>${c.counterparty || "—"}</td>
<td>${renderStatus(c.status)}</td>
<td><button class="btn btn-secondary" onclick="openCheck(${c.id})">Открыть</button></td>
</tr>
`).join("");
}
function openCheck(id) {
currentCheckId = id;
showView("check-detail");
loadCheckDetail(id);
}
document.querySelectorAll(".tab").forEach(tab => {
tab.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
tab.classList.add("active");
const tabName = tab.dataset.tab;
document.querySelectorAll("[id^='tab-']").forEach(el => el.classList.add("hidden"));
document.getElementById(`tab-${tabName}`).classList.remove("hidden");
});
});
function closeModal() {
document.getElementById("modal-add-remark").classList.add("hidden");
document.getElementById("add-remark-form").reset();
}
function openAddRemarkModal() {
document.getElementById("modal-add-remark").classList.remove("hidden");
}
document.getElementById("add-remark-form").addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
const check = await (await fetch(`/api/checks/${currentCheckId}`)).json();
const newRemark = {
id: Date.now(),
category: data.category,
type: data.type || "Ручное",
document: data.document || "Общее",
text: data.text,
recommendation: data.recommendation || "",
confirmed: true,
page: null
};
check.remarks = check.remarks || [];
check.remarks.push(newRemark);
await fetch(`/api/checks/${currentCheckId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ remarks: check.remarks })
});
closeModal();
renderRemarks(check.remarks);
renderReport(check);
load();
});
load();
</script>
</body>
</html>