/* Тимбилдинг ДЦБ Астана — регистрация участников и список администратора. Список участников хранится на сервере (api/participants → data.json) и виден всем; если сервер недоступен (открывают статичную страницу) — работаем в localStorage браузера. */ (function () { "use strict"; var LS_KEY = "teambuilding_dcb_participants"; var usingLocal = false; var STATUS = { waiting: { label: "Ждёт подтверждения", tone: "warn" }, confirmed: { label: "Подтверждён", tone: "ok" } }; var SEED = [ { id: 1, name: "Данияр", lastname: "Орынбаев", phone: "+7 705 214 88 30", status: "confirmed", date: "08.09.2026" }, { id: 2, name: "Айгерим", lastname: "Сатыбаева", phone: "+7 702 551 40 12", status: "confirmed", date: "09.09.2026" }, { id: 3, name: "Ержан", lastname: "Абдрахимов", phone: "+7 771 902 33 56", status: "waiting", date: "09.09.2026" }, { id: 4, name: "Гульнара", lastname: "Касымжанова", phone: "+7 747 635 21 08", status: "confirmed", date: "10.09.2026" }, { id: 5, name: "Мадина", lastname: "Турсынбаева", phone: "+7 708 442 17 65", status: "confirmed", date: "11.09.2026" }, { id: 6, name: "Асхат", lastname: "Жаманкулов", phone: "+7 700 118 92 44", status: "waiting", date: "12.09.2026" }, { id: 7, name: "Сейіл", lastname: "Кенжарик", phone: "+7 748 276 50 91", status: "confirmed", date: "13.09.2026" }, { id: 8, name: "Жанна", lastname: "Мырзахметова", phone: "+7 706 390 77 14", status: "confirmed", date: "15.09.2026" }, { id: 9, name: "Бахыт", lastname: "Айтимов", phone: "+7 775 813 26 40", status: "confirmed", date: "15.09.2026" }, { id: 10, name: "Алия", lastname: "Исмухамбетова", phone: "+7 707 564 38 29", status: "waiting", date: "16.09.2026" }, { id: 11, name: "Нурлан", lastname: "Сейткалиев", phone: "+7 702 837 15 73", status: "confirmed", date: "16.09.2026" }, { id: 12, name: "Карлыгаш", lastname: "Ералиева", phone: "+7 749 620 84 57", status: "waiting", date: "17.09.2026" } ]; /* ── Хранилище ── */ function lsLoad() { try { var raw = localStorage.getItem(LS_KEY); if (raw) { var arr = JSON.parse(raw); if (Array.isArray(arr)) return arr; } } catch (e) {} try { localStorage.setItem(LS_KEY, JSON.stringify(SEED)); } catch (e) {} return SEED.slice(); } function lsSave(list) { try { localStorage.setItem(LS_KEY, JSON.stringify(list)); } catch (e) {} } function apiList() { return fetch("api/participants", { headers: { Accept: "application/json" }, cache: "no-store" }) .then(function (r) { if (!r.ok) throw new Error("http " + r.status); return r.json(); }); } function apiCreate(rec) { return fetch("api/participants", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(rec) }).then(function (r) { if (!r.ok) throw new Error("http " + r.status); return r.json(); }); } var participants = []; var nextId = 1; function reindex() { nextId = 1; for (var i = 0; i < participants.length; i++) { if (typeof participants[i].id === "number" && participants[i].id >= nextId) nextId = participants[i].id + 1; } } function loadAll() { return apiList().then(function (arr) { if (!Array.isArray(arr)) throw new Error("bad payload"); participants = arr; reindex(); return participants; }).catch(function () { usingLocal = true; participants = lsLoad(); reindex(); return participants; }); } /* ── Хелперы ── */ function $(sel, root) { return (root || document).querySelector(sel); } function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function todayStr() { var d = new Date(); var m = ("0" + (d.getMonth() + 1)).slice(-2); var day = ("0" + d.getDate()).slice(-2); return day + "." + m + "." + d.getFullYear(); } var toastsRoot = $("#toasts"); function toast(text, tone) { var el = document.createElement("div"); el.className = "kt-ai-toast"; if (tone) el.setAttribute("data-tone", tone); el.textContent = text; toastsRoot.appendChild(el); setTimeout(function () { el.style.opacity = "0"; el.style.transition = "opacity 200ms"; setTimeout(function () { el.remove(); }, 220); }, 3200); } /* ── Переключение видов ── */ function showView(name) { var p = $("#view-participant"); var a = $("#view-admin"); if (name === "admin") { p.hidden = true; a.hidden = false; } else { a.hidden = true; p.hidden = false; } if (name === "admin") renderAdmin(); } /* ── Форма регистрации ── */ var form = $("#regform"); var done = $("#regdone"); var fName = $("#f-name"); var fLast = $("#f-lastname"); var fPhone = $("#f-phone"); function flashField(input, msg) { input.focus(); toast(msg, "warn"); } form.addEventListener("submit", function (ev) { ev.preventDefault(); var name = fName.value.trim(); var lastname = fLast.value.trim(); var phone = fPhone.value.trim(); if (!name) { flashField(fName, "Введите имя"); return; } if (!lastname) { flashField(fLast, "Введите фамилию"); return; } if (!/\+?[0-9](?:[0-9 ()\-]{6,14})/.test(phone)) { flashField(fPhone, "Укажите телефон в формате +7 XXX XXX XX XX"); return; } for (var i = 0; i < participants.length; i++) { var p = participants[i]; var key = (p.name + " " + p.lastname).trim().replace(/\s+/g, " ").toLowerCase(); if (key === (name + " " + lastname).replace(/\s+/g, " ").toLowerCase() && String(p.phone).replace(/\D/g, "") === phone.replace(/\D/g, "")) { toast("Этот участник уже зарегистрирован", "warn"); return; } } var rec = { name: name, lastname: lastname, phone: phone, status: "waiting", date: todayStr() }; function finished(savedRec) { rec = savedRec || rec; participants.unshift(rec); reindex(); form.hidden = true; done.hidden = false; $("#regdone-text").textContent = name + ", вы в списке участников. Администратор подтвердит участие до 18 сентября."; toast("Регистрация принята", "ok"); renderAdmin(); } if (usingLocal) { finished({ id: nextId, name: name, lastname: lastname, phone: phone, status: "waiting", date: todayStr() }); lsSave(participants); return; } apiCreate(rec).then(function (json) { finished(json && json.record ? json.record : { id: nextId, name: name, lastname: lastname, phone: phone, status: "waiting", date: todayStr() }); }).catch(function () { usingLocal = true; finished({ id: nextId, name: name, lastname: lastname, phone: phone, status: "waiting", date: todayStr() }); lsSave(participants); }); }); $("#regdone-again").addEventListener("click", function () { form.reset(); done.hidden = true; form.hidden = false; fName.focus(); }); /* ── Админка ── */ var statusFilter = "all"; var searchQuery = ""; var selectedId = null; function filtered() { return participants.filter(function (p) { if (statusFilter !== "all" && p.status !== statusFilter) return false; if (searchQuery) { var hay = (p.name + " " + p.lastname + " " + p.phone).toLowerCase(); if (hay.indexOf(searchQuery) === -1) return false; } return true; }); } function statusPill(status) { var s = STATUS[status] || STATUS.waiting; return '' + esc(s.label) + ""; } function renderAdmin() { $("#nav-count").textContent = participants.length; var total = participants.length; var confirmed = 0, waiting = 0; for (var i = 0; i < participants.length; i++) { if (participants[i].status === "confirmed") confirmed++; else waiting++; } $("#kpi-total").textContent = total; $("#kpi-confirmed").textContent = confirmed; $("#kpi-waiting").textContent = waiting; var chips = document.querySelectorAll(".kt-ai-filter-chip"); for (var j = 0; j < chips.length; j++) { chips[j].setAttribute("data-active", chips[j].getAttribute("data-status") === statusFilter ? "true" : "false"); } var list = filtered(); var html = ""; for (var k = 0; k < list.length; k++) { var p = list[k]; html += '" + '' + esc(p.lastname) + " " + esc(p.name) + "" + 'ДЦБ Астана' + '' + esc(p.phone) + "" + '' + esc(p.date) + "" + '' + statusPill(p.status) + "" + '' + ""; } $("#adm-tbody").innerHTML = html; var empty = $("#adm-empty"); if (list.length === 0) { empty.hidden = false; var hasFilter = statusFilter !== "all" || searchQuery !== ""; $("#adm-empty-title").textContent = hasFilter ? "Никого не нашлось" : "Пока никто не зарегистрировался"; $("#adm-empty-text").textContent = hasFilter ? "Попробуйте изменить запрос или фильтр." : "Регистрации участников появятся здесь, как только коллеги отправят форму."; } else { empty.hidden = true; } } $("#adm-tbody").addEventListener("click", function (ev) { var tr = ev.target.closest("tr[data-id]"); if (!tr) return; openDrawer(parseInt(tr.getAttribute("data-id"), 10)); }); $("#adm-search").addEventListener("input", function () { searchQuery = this.value.trim().toLowerCase(); renderAdmin(); }); document.querySelectorAll(".kt-ai-filter-chip").forEach(function (chip) { chip.addEventListener("click", function () { statusFilter = this.getAttribute("data-status"); renderAdmin(); }); }); /* ── Drawer ── */ var drawer = $("#drawer"); function findRec(id) { for (var i = 0; i < participants.length; i++) if (participants[i].id === id) return participants[i]; return null; } function openDrawer(id) { var p = findRec(id); if (!p) return; selectedId = id; $("#d-name").textContent = p.lastname + " " + p.name; var phoneEl = $("#d-phone"); phoneEl.textContent = p.phone; phoneEl.toggleAttribute("data-empty", !p.phone); $("#d-date").textContent = p.date; $("#d-status").innerHTML = statusPill(p.status); $("#d-confirm").hidden = p.status === "confirmed"; drawer.hidden = false; document.documentElement.classList.add("kt-ai-scroll-locked"); renderAdmin(); } function closeDrawer() { drawer.hidden = true; selectedId = null; document.documentElement.classList.remove("kt-ai-scroll-locked"); renderAdmin(); } $(".drawer-close").addEventListener("click", closeDrawer); $("#d-close").addEventListener("click", closeDrawer); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && !drawer.hidden) closeDrawer(); }); /* Статусы (подтверждение, удаление) — только для статической версии: на серверной стороне запись не теряется, статус «ждёт» сохранится. */ $("#d-confirm").addEventListener("click", function () { var p = findRec(selectedId); if (!p) return; p.status = "confirmed"; if (usingLocal) lsSave(participants); toast(p.lastname + " " + p.name + ": участие подтверждено", "ok"); openDrawer(p.id); }); $("#d-unlink").addEventListener("click", function () { var p = findRec(selectedId); if (!p) return; var idx = participants.indexOf(p); if (idx !== -1) participants.splice(idx, 1); if (usingLocal) lsSave(participants); closeDrawer(); toast(p.lastname + " " + p.name + " убран из списка"); }); /* ── Навигация ── */ document.querySelectorAll("[data-nav]").forEach(function (el) { el.addEventListener("click", function (ev) { ev.preventDefault(); showView("participant"); var target = this.getAttribute("data-nav"); if (target && target !== "reg") { setTimeout(function () { var sec = document.getElementById(target); if (sec) sec.scrollIntoView({ behavior: "smooth", block: "start" }); }, 60); } else { window.scrollTo({ top: 0, behavior: "smooth" }); } }); }); var adminLink = document.querySelector(".tb-admin-link-strong"); if (adminLink) adminLink.addEventListener("click", function () { showView("admin"); }); /* ── Старт: загрузить список (с сервера; при недоступности — локальный) ── */ loadAll().then(function () { renderAdmin(); if (location.hash === "#admin" || location.hash === "#view-admin") showView("admin"); else showView("participant"); }); })();