263 lines
13 KiB
HTML
263 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Загрузка отчёта — ИИ-агент КФУ</title>
|
||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||
<link rel="stylesheet" href="design-system/vibe-theme.css">
|
||
<style>
|
||
a.kt-ai-btn { text-decoration: none; }
|
||
.kfu-top {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 14px 20px;
|
||
background: var(--kt-ai-card-bg);
|
||
border-bottom: 1px solid var(--kt-ai-card-border);
|
||
}
|
||
.kfu-top strong { font-size: 14px; font-weight: 600; }
|
||
.kfu-top .kt-ai-spacer { flex: 1; }
|
||
.kfu-main { max-width: 640px; margin: 0 auto; padding: var(--kt-ai-space-8, 28px) 20px 60px; }
|
||
.kfu-head { margin-bottom: var(--kt-ai-space-6, 22px); }
|
||
.kfu-head h1 { font-size: 20px; margin: 0 0 4px; }
|
||
.kfu-head p { margin: 0; color: var(--kt-ai-fg-muted); font-size: 14px; }
|
||
.kfu-drop {
|
||
border: 1.5px dashed var(--kt-ai-card-border);
|
||
border-radius: 12px;
|
||
padding: 34px 20px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: border-color .15s, background .15s;
|
||
background: var(--kt-ai-card-bg);
|
||
}
|
||
.kfu-drop:hover, .kfu-drop.is-over { border-color: var(--kt-ai-primary); background: var(--kt-ai-bg-active, transparent); }
|
||
.kfu-drop svg { width: 34px; height: 34px; margin-bottom: 10px; color: var(--kt-ai-fg-muted); }
|
||
.kfu-drop b { display: block; font-size: 14px; font-weight: 600; }
|
||
.kfu-drop span { display: block; margin-top: 4px; font-size: 13px; color: var(--kt-ai-fg-muted); }
|
||
.kfu-file { display: none; margin-top: 12px; align-items: center; gap: 10px; font-size: 13px; background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: 8px; padding: 10px 12px; }
|
||
.kfu-file.show { display: flex; }
|
||
.kfu-file .kt-ai-spacer { flex: 1; }
|
||
.kfu-file button { background: none; border: 0; color: var(--kt-ai-danger); cursor: pointer; font-size: 13px; }
|
||
.kfu-form { margin-top: var(--kt-ai-space-6, 22px); display: flex; flex-direction: column; gap: var(--kt-ai-space-5, 16px); }
|
||
.kfu-note { font-size: 12.5px; color: var(--kt-ai-fg-muted); }
|
||
.kfu-done { text-align: center; padding: 48px 20px; }
|
||
.kfu-done svg { width: 54px; height: 54px; color: var(--kt-ai-success-token); margin-bottom: 16px; }
|
||
.kfu-done h1 { font-size: 22px; margin: 0 0 8px; }
|
||
.kfu-done p { color: var(--kt-ai-fg-muted); font-size: 14px; margin: 0 0 26px; }
|
||
.kfu-status-chip { margin-top: var(--kt-ai-space-6, 22px); text-align: center; font-size: 13px; color: var(--kt-ai-fg-muted); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<script src="design-system/icons/kt-ai-icons.js"></script>
|
||
<header class="kfu-top">
|
||
<strong>ИИ-агент КФУ</strong>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span id="who" style="font-size:13px;color:var(--kt-ai-fg-muted)"></span>
|
||
<a class="kt-ai-btn" data-variant="ghost" href="#" id="logoutBtn">Выйти</a>
|
||
</header>
|
||
<main class="kfu-main" id="mainView">
|
||
<div class="kfu-head">
|
||
<h1 id="periodTitle">Загрузка отчёта</h1>
|
||
<p id="periodSub" class="kfu-note"></p>
|
||
</div>
|
||
<div class="kfu-drop" id="drop" tabindex="0" role="button" aria-label="Выбрать файл">
|
||
<svg viewBox="0 0 24 24" fill="none"><path d="M12 16V4m0 0 4 4m-4-4-4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 17v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
|
||
<b>Перетащите файл сюда или нажмите для выбора</b>
|
||
<span>xlsx, xlsm или pdf · до 50 МБ</span>
|
||
</div>
|
||
<input type="file" id="fileInput" accept=".xlsx,.xlsm,.pdf" hidden>
|
||
<div class="kfu-file" id="fileBox">
|
||
<span id="fileName" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px"></span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span id="fileSize" style="color:var(--kt-ai-fg-muted)"></span>
|
||
<button type="button" id="fileClear" aria-label="Убрать файл">Убрать</button>
|
||
</div>
|
||
<form class="kfu-form" id="upForm" autocomplete="off">
|
||
<label class="kt-ai-field">
|
||
<span>Комментарий <em style="color:var(--kt-ai-fg-muted);font-style:normal;font-size:12px">(необязательно)</em></span>
|
||
<textarea class="kt-ai-textarea" id="comment" rows="3" maxlength="2000" placeholder="Например: корректура по позиции «Долг»"></textarea>
|
||
</label>
|
||
<p class="kfu-note">Повторная загрузка за тот же период заменит предыдущую — на дашборде будет видна последняя.</p>
|
||
<button class="kt-ai-btn" data-variant="primary" data-size="lg" type="submit" id="sendBtn" disabled>Отправить</button>
|
||
</form>
|
||
<div class="kfu-status-chip" id="statusChip"></div>
|
||
</main>
|
||
<main class="kfu-main kfu-done" id="doneView" hidden>
|
||
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.6"/><path d="m8 12.5 2.6 2.6L16 9.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
<h1>Отчёт принят</h1>
|
||
<p id="doneSub"></p>
|
||
<a class="kt-ai-btn" data-size="lg" href="upload.html">Загрузить другой период / файл</a>
|
||
</main>
|
||
<script>
|
||
(function () {
|
||
var who = document.getElementById("who");
|
||
var periodTitle = document.getElementById("periodTitle");
|
||
var periodSub = document.getElementById("periodSub");
|
||
var drop = document.getElementById("drop");
|
||
var fileInput = document.getElementById("fileInput");
|
||
var fileBox = document.getElementById("fileBox");
|
||
var fileName = document.getElementById("fileName");
|
||
var fileSize = document.getElementById("fileSize");
|
||
var fileClear = document.getElementById("fileClear");
|
||
var commentEl = document.getElementById("comment");
|
||
var sendBtn = document.getElementById("sendBtn");
|
||
var upForm = document.getElementById("upForm");
|
||
var statusChip = document.getElementById("statusChip");
|
||
var mainView = document.getElementById("mainView");
|
||
var doneView = document.getElementById("doneView");
|
||
|
||
var ME = null;
|
||
var selectedFile = null;
|
||
|
||
function fmtSize(n) {
|
||
if (!n && n !== 0) return "";
|
||
if (n < 1048576) return (n / 1024).toFixed(0) + " КБ";
|
||
return (n / 1048576).toFixed(1) + " МБ";
|
||
}
|
||
function fmtDate(iso) {
|
||
if (!iso) return "";
|
||
var d = new Date(iso);
|
||
var p = function (x) { return (x < 10 ? "0" : "") + x; };
|
||
return p(d.getDate()) + "." + p(d.getMonth() + 1) + "." + d.getFullYear() + " " + p(d.getHours()) + ":" + p(d.getMinutes());
|
||
}
|
||
function fmtDeadline(s) {
|
||
if (!s) return "";
|
||
var parts = s.split("-");
|
||
if (parts.length !== 3) return s;
|
||
return parts[2] + "." + parts[1] + "." + parts[0];
|
||
}
|
||
|
||
function refreshSend() {
|
||
sendBtn.disabled = !selectedFile;
|
||
drop.style.display = selectedFile ? "none" : "block";
|
||
fileBox.classList.toggle("show", !!selectedFile);
|
||
}
|
||
|
||
function pickFile(f) {
|
||
if (!f) return;
|
||
var ok = /\.(xlsx|xlsm|pdf)$/i.test(f.name);
|
||
if (!ok) { alert("Поддерживаются файлы xlsx, xlsm, pdf"); return; }
|
||
if (f.size > 50 * 1048576) { alert("Файл больше 50 МБ"); return; }
|
||
selectedFile = f;
|
||
if (f.size >= 20 * 1048576) {
|
||
// big file: keep, just note
|
||
}
|
||
fileName.textContent = f.name;
|
||
fileSize.textContent = fmtSize(f.size);
|
||
refreshSend();
|
||
}
|
||
|
||
drop.addEventListener("click", function () { fileInput.click(); });
|
||
drop.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fileInput.click(); } });
|
||
drop.addEventListener("dragover", function (e) { e.preventDefault(); drop.classList.add("is-over"); });
|
||
drop.addEventListener("dragleave", function () { drop.classList.remove("is-over"); });
|
||
drop.addEventListener("drop", function (e) {
|
||
e.preventDefault();
|
||
drop.classList.remove("is-over");
|
||
if (e.dataTransfer.files && e.dataTransfer.files.length) pickFile(e.dataTransfer.files[0]);
|
||
});
|
||
fileInput.addEventListener("change", function () { pickFile(fileInput.files[0]); });
|
||
fileClear.addEventListener("click", function () {
|
||
selectedFile = null;
|
||
fileInput.value = "";
|
||
refreshSend();
|
||
});
|
||
|
||
function showDone(uploadedAt) {
|
||
mainView.hidden = true;
|
||
doneView.hidden = false;
|
||
document.getElementById("doneSub").textContent =
|
||
"Файл сохранён, расчёт коэффициентов идёт. Дата принятия: " + fmtDate(uploadedAt) + ".";
|
||
}
|
||
|
||
upForm.addEventListener("submit", function (e) {
|
||
e.preventDefault();
|
||
if (!selectedFile) return;
|
||
sendBtn.disabled = true;
|
||
sendBtn.textContent = "Отправка…";
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open("POST", "api/upload");
|
||
xhr.setRequestHeader("X-Filename", encodedName(selectedFile.name));
|
||
xhr.setRequestHeader("X-Comment", commentEl.value.slice(0, 2000));
|
||
xhr.upload.onprogress = function (ev) {
|
||
if (ev.lengthComputable) sendBtn.textContent = "Отправка… " + Math.round(ev.loaded * 100 / ev.total) + "%";
|
||
};
|
||
function failMessage(status) {
|
||
try {
|
||
var b = JSON.parse(xhr.responseText || "{}");
|
||
var msg = (b && b.error) || "";
|
||
if (xhr.status === 401) return "Сессия истекла — войдите заново.";
|
||
if (!msg) {
|
||
if (xhr.status === 413) msg = "Файл не принят: файл больше 50 МБ";
|
||
else if (xhr.status === 415) msg = "Файл не принят: поддерживаются только xlsx, xlsm, pdf";
|
||
else if (xhr.status === 423) msg = "Слишком много попыток — попробуйте позже";
|
||
else if (xhr.status >= 500) msg = "Ошибка сервера (" + xhr.status + "). Попробуйте ещё раз.";
|
||
else msg = "Файл не принят: причина — " + (xhr.statusText || xhr.status);
|
||
}
|
||
return msg;
|
||
} catch (err) {
|
||
if (xhr.status === 401) return "Сессия истекла — войдите заново.";
|
||
return "Ошибка сервера: " + xhr.status + (xhr.statusText ? " " + xhr.statusText : "");
|
||
}
|
||
}
|
||
xhr.onload = function () {
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
try {
|
||
var b = JSON.parse(xhr.responseText || "{}");
|
||
if (b.ok) { showDone(b.uploadedAt); return; }
|
||
} catch (err) {}
|
||
}
|
||
alert(failMessage(xhr.status));
|
||
sendBtn.disabled = false;
|
||
sendBtn.textContent = "Отправить";
|
||
if (xhr.status === 401) setTimeout(function () { window.location.href = "index.html"; }, 1500);
|
||
};
|
||
xhr.onerror = function () {
|
||
alert("Сервер недоступен. Проверьте соединение и попробуйте ещё раз.");
|
||
sendBtn.disabled = false;
|
||
sendBtn.textContent = "Отправить";
|
||
};
|
||
xhr.send(selectedFile);
|
||
});
|
||
|
||
function encodedName(n) {
|
||
return encodeURIComponent(n);
|
||
}
|
||
|
||
function statusChipText(st) {
|
||
if (!st) return "";
|
||
var m = {
|
||
not_submitted: "За отчётный период ещё не загружено.",
|
||
processing: "Загружено: расчёт коэффициентов идёт.",
|
||
error: "Загружено: у агента ошибка расчёта — свяжитесь со сводящим.",
|
||
done: "За отчётный период отчёт загружен. Повторная загрузка заменит его."
|
||
};
|
||
return m[st] || "";
|
||
}
|
||
|
||
fetch("api/me", { credentials: "same-origin" }).then(function (r) {
|
||
if (r.status === 401) { window.location.href = "index.html"; return null; }
|
||
return r.json();
|
||
}).then(function (me) {
|
||
if (!me) return;
|
||
if (me.role !== "subsidiary") { window.location.href = "index.html"; return; }
|
||
ME = me;
|
||
who.textContent = me.companyName;
|
||
periodTitle.textContent = me.companyName;
|
||
periodSub.textContent = "отчёт за " + (me.period || "") + " · срок до " + fmtDeadline(me.deadline);
|
||
statusChip.textContent = statusChipText(me.lastStatus);
|
||
}).catch(function () {
|
||
window.location.href = "index.html";
|
||
});
|
||
|
||
document.getElementById("logoutBtn").addEventListener("click", function (e) {
|
||
e.preventDefault();
|
||
fetch("api/logout", { method: "POST" }).then(function () { window.location.href = "index.html"; });
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|