privet/upload.html

263 lines
13 KiB
HTML
Raw 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" 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>