365 lines
16 KiB
JavaScript
365 lines
16 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
var page = document.currentScript.getAttribute("data-page");
|
||
|
||
function esc(s) {
|
||
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
||
});
|
||
}
|
||
function fmtDate(iso) {
|
||
if (!iso) return "—";
|
||
var d = new Date(iso);
|
||
var p = function (n) { return (n < 10 ? "0" : "") + n; };
|
||
return p(d.getDate()) + "." + p(d.getMonth() + 1) + "." + d.getFullYear();
|
||
}
|
||
function toastStack() {
|
||
var el = document.getElementById("toasts");
|
||
if (!el) { toastStack.el = (toastStack.el = el = document.createElement("div")); el.className = "kt-ai-toast-stack"; document.body.appendChild(el); }
|
||
return el;
|
||
}
|
||
function toast(text, tone) {
|
||
var t = document.createElement("div");
|
||
t.className = "kt-ai-toast";
|
||
if (tone) t.setAttribute("data-tone", tone);
|
||
t.textContent = text;
|
||
toastStack().appendChild(t);
|
||
setTimeout(function () { t.remove(); }, 4500);
|
||
}
|
||
async function api(path, opts) {
|
||
var r = await fetch("api/" + path, opts);
|
||
var j = null;
|
||
try { j = await r.json(); } catch (_) {}
|
||
if (!r.ok) throw new Error((j && j.error) || "Ошибка сети, попробуйте ещё раз");
|
||
return j;
|
||
}
|
||
|
||
if (page === "form") initForm();
|
||
if (page === "app") initApp();
|
||
|
||
/* ================= Страница формы ================= */
|
||
function initForm() {
|
||
var form = document.getElementById("regForm");
|
||
var btn = document.getElementById("regSubmit");
|
||
var errFor = { fName: 0, fEmail: 1, fPhone: 2 };
|
||
|
||
function check(id, ok) {
|
||
var field = document.getElementById(id).closest(".kt-ai-field");
|
||
var err = field.querySelector(".error");
|
||
field.setAttribute("data-invalid", ok ? "false" : "true");
|
||
if (err) err.hidden = ok;
|
||
return ok;
|
||
}
|
||
["fName", "fEmail", "fPhone"].forEach(function (id) {
|
||
document.getElementById(id).addEventListener("input", function () { check(id, true); });
|
||
});
|
||
|
||
form.addEventListener("submit", async function (e) {
|
||
e.preventDefault();
|
||
var okName = check("fName", document.getElementById("fName").value.trim().length >= 3);
|
||
var okEmail = check("fEmail", /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(document.getElementById("fEmail").value.trim()));
|
||
var okPhone = check("fPhone", document.getElementById("fPhone").value.replace(/\D/g, "").length >= 10);
|
||
if (!(okName && okEmail && okPhone)) return;
|
||
btn.setAttribute("data-loading", "true");
|
||
try {
|
||
var data = {};
|
||
Array.prototype.forEach.call(form.elements, function (el) {
|
||
if (el.name) data[el.name] = el.value.trim();
|
||
});
|
||
await api("registrations", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data)
|
||
});
|
||
document.getElementById("formBlock").hidden = true;
|
||
document.getElementById("successBlock").hidden = false;
|
||
form.reset();
|
||
toast("Заявка отправлена организаторам", "ok");
|
||
} catch (err) {
|
||
toast(err.message, "risk");
|
||
} finally {
|
||
btn.removeAttribute("data-loading");
|
||
}
|
||
});
|
||
|
||
document.getElementById("againBtn").addEventListener("click", function () {
|
||
document.getElementById("formBlock").hidden = false;
|
||
document.getElementById("successBlock").hidden = true;
|
||
document.getElementById("fName").focus();
|
||
});
|
||
}
|
||
|
||
/* ================= Кабинет организатора ================= */
|
||
var ALL = [];
|
||
var state = { q: "", status: "" };
|
||
|
||
function initApp() {
|
||
var search = document.getElementById("topSearch");
|
||
var seg = document.getElementById("statusSeg");
|
||
|
||
search.addEventListener("input", debounce(function () {
|
||
state.q = search.value.trim().toLowerCase();
|
||
refreshTable();
|
||
}, 150));
|
||
seg.addEventListener("click", function (e) {
|
||
var b = e.target.closest("button[data-status]");
|
||
if (!b) return;
|
||
state.status = b.getAttribute("data-status");
|
||
markSeg();
|
||
refreshTable();
|
||
});
|
||
document.getElementById("exportBtn").addEventListener("click", exportCsv);
|
||
document.getElementById("drawer").addEventListener("click", function (e) {
|
||
if (e.target === e.currentTarget) closeDrawer();
|
||
});
|
||
document.addEventListener("keydown", function (e) {
|
||
if (e.key === "Escape") closeDrawer();
|
||
});
|
||
|
||
renderTable = renderTable.bind(null);
|
||
load();
|
||
markSeg();
|
||
}
|
||
|
||
function debounce(fn, ms) {
|
||
var t;
|
||
return function () {
|
||
var a = arguments, c = this;
|
||
clearTimeout(t);
|
||
t = setTimeout(function () { fn.apply(c, a); }, ms);
|
||
};
|
||
}
|
||
|
||
function refreshTable() {
|
||
var rows = visible();
|
||
renderEventHead();
|
||
renderKpi(rows);
|
||
renderTable(rows);
|
||
}
|
||
|
||
function markSeg() {
|
||
var seg = document.getElementById("statusSeg");
|
||
Array.prototype.forEach.call(seg.children, function (b) {
|
||
b.setAttribute("data-active", b.getAttribute("data-status") === state.status ? "true" : "false");
|
||
});
|
||
}
|
||
|
||
async function load() {
|
||
var d;
|
||
try {
|
||
d = await api("registrations");
|
||
ALL = d.registrations || [];
|
||
} catch (err) {
|
||
ALL = null;
|
||
}
|
||
document.getElementById("navCount").textContent = ALL ? ALL.length : "";
|
||
if (!ALL) {
|
||
var evHead = document.getElementById("eventHead");
|
||
if (evHead) evHead.innerHTML = "";
|
||
var el = document.getElementById("regEmpty");
|
||
el.hidden = false;
|
||
el.innerHTML = '<div class="kt-ai-empty"><div class="icon-spot"><svg class="kt-icon kt-icon-lg" aria-hidden="true"><use href="#alert-circle"></use></svg></div><span class="title">Список заявок недоступен</span><span>' + esc(err.message) + "</span></div>";
|
||
document.getElementById("regTable").style.display = "none";
|
||
return;
|
||
}
|
||
refreshTable();
|
||
}
|
||
|
||
function renderEventHead() {
|
||
var head = document.getElementById("eventHead");
|
||
if (!head) return;
|
||
var byRole = {};
|
||
(ALL || []).forEach(function (r) {
|
||
if (r.status !== "Отменена") byRole[r.role] = (byRole[r.role] || 0) + 1;
|
||
});
|
||
var roles = Object.keys(byRole).map(function (k) {
|
||
return '<span class="kt-ai-chip">' + esc(k) + " · " + byRole[k] + "</span>";
|
||
}).join(" ");
|
||
head.innerHTML =
|
||
'<h1 class="evt-title" style="font-size:var(--kt-ai-text-xl);color:var(--kt-ai-fg)">Корпоративный ивент 2026 · 30 окт, Алматы, ДК «Жибек жолы»</h1>' +
|
||
(roles ? '<div style="display:flex;flex-wrap:wrap;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-4)">' + roles + "</div>" : "");
|
||
}
|
||
|
||
function visible() {
|
||
return ALL.filter(function (r) {
|
||
if (state.status && r.status !== state.status) return false;
|
||
if (state.q) {
|
||
var hay = [r.name, r.email, r.phone, r.department, r.position, r.role, r.track].join(" ").toLowerCase();
|
||
if (hay.indexOf(state.q) === -1) return false;
|
||
}
|
||
return true;
|
||
}).sort(function (a, b) { return b.id - a.id; });
|
||
}
|
||
|
||
function renderTable(rows) {
|
||
if (!rows) rows = visible();
|
||
var body = document.getElementById("regBody");
|
||
var table = document.getElementById("regTable");
|
||
var empty = document.getElementById("regEmpty");
|
||
|
||
renderKpi(rows);
|
||
if (!rows.length) {
|
||
table.style.display = "none";
|
||
empty.hidden = false;
|
||
empty.innerHTML = '<div class="kt-ai-empty"><div class="icon-spot"><svg class="kt-icon kt-icon-lg" aria-hidden="true"><use href="#check-circle"></use></svg></div><span class="title">' +
|
||
(ALL.length ? "По фильтру ничего нет" : "Заявок пока нет") + "</span><span>" +
|
||
(ALL.length ? "Сбросьте статус или поправьте поиск." : "Разошлите коллегам ссылку на форму — заявки появятся здесь.") +
|
||
"</span></div>";
|
||
return;
|
||
}
|
||
table.style.display = "";
|
||
empty.hidden = true;
|
||
body.innerHTML = rows.map(function (r) {
|
||
return '<tr data-id="' + r.id + '" tabindex="0" style="cursor:pointer">' +
|
||
'<td><svg class="kt-icon kt-icon-sm" aria-hidden="true" style="color:var(--kt-ai-fg-faint)"><use href="#chevron-right"></use></svg></td>' +
|
||
"<td data-label='Участник'><div class='cell-title'>" + esc(r.name) + "</div><div class='cell-sub'>" + esc(r.role) + "</div></td>" +
|
||
"<td data-label='Контакты'>" + esc(r.email) + "<div class='cell-sub'>" + esc(r.phone) + "</div></td>" +
|
||
"<td data-label='Филиал / должность'>" + esc(r.department || "—") + (r.position ? "<div class='cell-sub'>" + esc(r.position) + "</div>" : "") + "</td>" +
|
||
"<td data-label='Сессия · Питание'>" + esc(r.track) + "<div class='cell-sub'>" + esc(r.food) + "</div></td>" +
|
||
"<td data-label='Заявка' class='num'>" + fmtDate(r.createdAt) + "</td>" +
|
||
"<td data-label='Статус'>" + statusPill(r.status) + "</td></tr>";
|
||
}).join("");
|
||
};
|
||
|
||
function statusPill(s) {
|
||
var m = { "Заявка": "info", "Подтверждена": "ok", "Отменена": "muted" };
|
||
return '<span class="kt-ai-status-pill" data-status="' + (m[s] || "muted") + '"><span class="dot"></span>' + esc(s) + "</span>";
|
||
}
|
||
|
||
function renderKpi(rows) {
|
||
var all = ALL || [];
|
||
var total = all.length;
|
||
var confirmed = all.filter(function (r) { return r.status === "Подтверждена"; }).length;
|
||
var cancelled = all.filter(function (r) { return r.status === "Отменена"; }).length;
|
||
var pending = total - confirmed - cancelled;
|
||
var tracks = {};
|
||
rows.forEach(function (r) { tracks[r.track] = (tracks[r.track] || 0) + 1; });
|
||
var top = Object.keys(tracks).sort(function (a, b) { return tracks[b] - tracks[a]; })[0];
|
||
var strip = document.getElementById("kpiStrip");
|
||
strip.innerHTML =
|
||
kpi(String(total), "всего заявок") +
|
||
kpi(String(pending), "ждут подтверждения") +
|
||
kpi(String(confirmed), "подтверждено") +
|
||
(top ? kpi(String(tracks[top]), "мест: " + top) : "");
|
||
renderConclusion(rows, total, confirmed);
|
||
}
|
||
function kpi(value, label) {
|
||
return '<div class="kt-ai-kpi"><span class="value">' + esc(value) + '</span><span class="label">' + esc(label) + "</span></div>";
|
||
}
|
||
|
||
function renderConclusion(rows, total, confirmed) {
|
||
var el = document.getElementById("conclusion");
|
||
if (!total) { el.innerHTML = ""; return; }
|
||
var cancelled = (ALL || []).filter(function (r) { return r.status === "Отменена"; }).length;
|
||
el.innerHTML = '<div class="title">Состояние потоков</div><div class="text">' +
|
||
"Всего заявок: " + total + ". Подтверждено: " + confirmed + "." +
|
||
(cancelled ? " Отменено: " + cancelled + "." : "") +
|
||
(rows.length === total ? " Фильтры не применены." : " По текущему фильтру: " + rows.length + ".") +
|
||
" Данные обновляются после каждого действия — статусы меняются из карточки заявки, а список записей доступен во вкладке «Данные» с выгрузкой в Excel.</div>";
|
||
}
|
||
|
||
function exportCsv() {
|
||
var rows = visible();
|
||
if (!rows.length) { toast("Экспортировать нечего", "warn"); return; }
|
||
var cols = ["id", "name", "email", "phone", "department", "position", "role", "track", "food", "status", "note", "createdAt"];
|
||
var head = ["id", "ФИО", "Почта", "Телефон", "Филиал", "Должность", "Роль", "Сессия", "Питание", "Статус", "Примечание", "Заявка"];
|
||
var lines = [head.join(",")].concat(rows.map(function (r) {
|
||
return cols.map(function (c) {
|
||
var v = r[c];
|
||
v = v == null ? "" : String(v);
|
||
return '"' + v.replace(/"/g, '""') + '"';
|
||
}).join(",");
|
||
}));
|
||
var blob = new Blob(["\uFEFF" + lines.join("\n")], { type: "text/csv;charset=utf-8" });
|
||
var a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = "registratsii-" + new Date().toISOString().slice(0, 10) + ".csv";
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
toast("Таблица выгружена");
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById("drawer").hidden = true;
|
||
}
|
||
|
||
document.getElementById("regBody").addEventListener("click", function (e) {
|
||
var tr = e.target.closest("tr[data-id]");
|
||
if (tr) openDrawer(Number(tr.getAttribute("data-id")));
|
||
});
|
||
document.getElementById("regBody").addEventListener("keydown", function (e) {
|
||
if (e.key === "Enter") {
|
||
var tr = e.target.closest("tr[data-id]");
|
||
if (tr) openDrawer(Number(tr.getAttribute("data-id")));
|
||
}
|
||
});
|
||
|
||
function openDrawer(id) {
|
||
var r = (ALL || []).find(function (x) { return x.id === id; });
|
||
if (!r) return;
|
||
var d = document.getElementById("drawer");
|
||
d.innerHTML =
|
||
'<div class="drawer-head"><div class="t"><div class="kt-ai-h2" id="drawerTitle">' + esc(r.name) + "</div>" +
|
||
statusPill(r.status) + '<div style="margin-top:6px;font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted);z-index:0;position:relative">Заявка от ' + fmtDate(r.createdAt) + "</div></div>" +
|
||
'<button class="kt-ai-icon-button" aria-label="Закрыть" id="drawerClose"><svg class="kt-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div>' +
|
||
'<div class="drawer-sec">' + props(r) + "</div>" +
|
||
'<div class="drawer-sec"><div class="kt-ai-h3" style="margin-bottom:var(--kt-ai-space-4)">Примечание</div>' +
|
||
'<textarea class="kt-ai-textarea" id="drawerNote" rows="3" placeholder="Необязательно">' + esc(r.note || "") + "</textarea></div>" +
|
||
'<div class="kt-ai-drawer-actions">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" data-act="Подтверждена" style="width:100%;justify-content:center;display:flex">Подтвердить участие</button>' +
|
||
'<button class="kt-ai-btn" data-act="Отменена" style="width:100%;justify-content:center;display:flex">Отменить заявку</button>' +
|
||
'<button class="kt-ai-btn" id="drawerSaveNote" style="width:100%;justify-content:center;display:flex">Сохранить примечание</button>' +
|
||
"</div>";
|
||
d.hidden = false;
|
||
d.focus();
|
||
d.querySelector("#drawerClose").addEventListener("click", closeDrawer);
|
||
d.querySelectorAll("button[data-act]").forEach(function (b) {
|
||
b.addEventListener("click", function () { changeStatus(id, b.getAttribute("data-act")); });
|
||
});
|
||
d.querySelector("#drawerSaveNote").addEventListener("click", function () { changeStatus(id, null, d.querySelector("#drawerNote").value); });
|
||
}
|
||
|
||
function props(r) {
|
||
var items = [
|
||
["Почта", r.email], ["Телефон", r.phone],
|
||
["Филиал / подразделение", r.department], ["Должность", r.position],
|
||
["Роль", r.role], ["Сессия", r.track], ["Питание", r.food]
|
||
];
|
||
return items.map(function (it) {
|
||
return '<div class="kt-ai-prop"><span class="k">' + esc(it[0]) + '</span><span class="v" ' + (it[1] ? "" : 'data-empty="true"') + ">" + esc(it[1] || "—") + "</span></div>";
|
||
}).join("");
|
||
}
|
||
|
||
window.addEventListener("hashchange", function () {
|
||
if (location.hash === "#new") {
|
||
location.replace("index.html");
|
||
}
|
||
});
|
||
if (location.hash === "#new") location.replace("index.html");
|
||
|
||
async function changeStatus(id, status, note) {
|
||
try {
|
||
var body = {};
|
||
if (status) body.status = status;
|
||
if (typeof note === "string") body.note = note;
|
||
await api("registrations/" + id, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body)
|
||
});
|
||
var r = (ALL || []).find(function (x) { return x.id === id; });
|
||
if (r) {
|
||
if (status) r.status = status;
|
||
if (typeof note === "string") r.note = note;
|
||
}
|
||
closeDrawer();
|
||
refreshTable();
|
||
document.getElementById("navCount").textContent = ALL.length;
|
||
toast(status ? ("Статус: " + status) : "Примечание сохранено", "ok");
|
||
} catch (err) {
|
||
toast(err.message, "risk");
|
||
}
|
||
}
|
||
})();
|