367 lines
14 KiB
JavaScript
367 lines
14 KiB
JavaScript
/* Тимбилдинг ДЦБ Астана — регистрация участников и список администратора.
|
||
Список участников хранится на сервере (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 '<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");
|
||
});
|
||
})();
|