(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 = '
Список заявок недоступен' + esc(err.message) + "
"; 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 '' + esc(k) + " · " + byRole[k] + ""; }).join(" "); head.innerHTML = '

Корпоративный ивент 2026 · 30 окт, Алматы, ДК «Жибек жолы»

' + (roles ? '
' + roles + "
" : ""); } 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 = '
' + (ALL.length ? "По фильтру ничего нет" : "Заявок пока нет") + "" + (ALL.length ? "Сбросьте статус или поправьте поиск." : "Разошлите коллегам ссылку на форму — заявки появятся здесь.") + "
"; return; } table.style.display = ""; empty.hidden = true; body.innerHTML = rows.map(function (r) { return '' + '' + "
" + esc(r.name) + "
" + esc(r.role) + "
" + "" + esc(r.email) + "
" + esc(r.phone) + "
" + "" + esc(r.department || "—") + (r.position ? "
" + esc(r.position) + "
" : "") + "" + "" + esc(r.track) + "
" + esc(r.food) + "
" + "" + fmtDate(r.createdAt) + "" + "" + statusPill(r.status) + ""; }).join(""); }; function statusPill(s) { var m = { "Заявка": "info", "Подтверждена": "ok", "Отменена": "muted" }; return '' + esc(s) + ""; } 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 '
' + esc(value) + '' + esc(label) + "
"; } 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 = '
Состояние потоков
' + "Всего заявок: " + total + ". Подтверждено: " + confirmed + "." + (cancelled ? " Отменено: " + cancelled + "." : "") + (rows.length === total ? " Фильтры не применены." : " По текущему фильтру: " + rows.length + ".") + " Данные обновляются после каждого действия — статусы меняются из карточки заявки, а список записей доступен во вкладке «Данные» с выгрузкой в Excel.
"; } 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 = '
' + esc(r.name) + "
" + statusPill(r.status) + '
Заявка от ' + fmtDate(r.createdAt) + "
" + '
' + '
' + props(r) + "
" + '
Примечание
' + '
" + '
' + '' + '' + '' + "
"; 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 '
' + esc(it[0]) + '" + esc(it[1] || "—") + "
"; }).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"); } } })();