717 lines
32 KiB
HTML
717 lines
32 KiB
HTML
<!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() {
|
||
const res = await fetch("/api/data");
|
||
db = await res.json();
|
||
updateStats();
|
||
renderRecentChecks();
|
||
renderTemplates();
|
||
renderRemarksDb();
|
||
populateQuestionTypes();
|
||
}
|
||
|
||
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());
|
||
|
||
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);
|
||
}
|
||
});
|
||
|
||
async function loadCheckDetail(id) {
|
||
const res = await fetch(`/api/checks/${id}`);
|
||
const check = await res.json();
|
||
if (!check.id) 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);
|
||
}
|
||
|
||
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) {
|
||
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> ${r.text}</div>
|
||
<div style="font-size: 13px; color: #666;">${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}. ${r.text}`).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) {
|
||
for (const file of files) {
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
|
||
await fetch(`/api/upload?checkId=${currentCheckId}`, {
|
||
method: "POST",
|
||
body: formData
|
||
});
|
||
}
|
||
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>
|