registratsiya-na-timbilding/script.js

367 lines
14 KiB
JavaScript
Raw Permalink 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.

/* Тимбилдинг ДЦБ Астана — регистрация участников и список администратора.
Список участников хранится на сервере (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 { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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 '<span class="kt-ai-status-pill" data-status="' + s.tone + '">' + esc(s.label) + "</span>";
}
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 += '<tr data-id="' + p.id + '"' + (p.id === selectedId ? ' data-selected="true"' : "") + ">" +
'<td data-label="Участник"><span class="cell-name">' + esc(p.lastname) + " " + esc(p.name) + "</span>" +
'<span class="cell-sub">ДЦБ Астана</span></td>' +
'<td data-label="Телефон" class="num">' + esc(p.phone) + "</td>" +
'<td data-label="Зарегистрирован" class="num">' + esc(p.date) + "</td>" +
'<td data-label="Статус">' + statusPill(p.status) + "</td>" +
'<td><span class="row-act kt-ai-tooltip" data-tip="Подробнее" aria-hidden="true"><svg class="kt-icon"><use href="#chevronRight"></use></svg></span></td>' +
"</tr>";
}
$("#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");
});
})();