hochu-vzlomat-sayt-kt/script.js

312 lines
12 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.

const STORAGE_KEY = "kindergarten_crm_children_v1";
const STATUS_LABELS = {
studying: "Учится",
sick: "Больничный",
vacation: "Отпуск"
};
const PAYMENT_LABELS = {
paid: "Оплачено",
current: "Актуально",
overdue: "Долг"
};
const DEMO_DATA = [
{ id: 1, name: "Айгерим Ахметов", group: "Солнышко", birth: "2021-04-12", parent: "Ахметова Л.К.", phone: "+7 700 111 22 33", status: "studying", payment: "paid", note: "Аллергия на цитрусовые" },
{ id: 2, name: "Бекзат Сауыт", group: "Солнышко", birth: "2021-07-30", parent: "Сауытов Д.Б.", phone: "+7 701 222 33 44", status: "sick", payment: "current", note: "Частые ОРВИ, нужен щадящий режим" },
{ id: 3, name: "Виктория Мельник", group: "Любимчики", birth: "2020-01-25", parent: "Мельник А.С.", phone: "+7 702 333 44 55", status: "studying", payment: "overdue", note: "Долг за сентябрь" },
{ id: 4, name: "Гульнара Сейитова", group: "Любимчики", birth: "2020-09-08", parent: "Сейитова Н.Ж.", phone: "+7 703 444 55 66", status: "vacation", payment: "paid", note: "Отпуск до 15 ноября" },
{ id: 5, name: "Дамир Тлеуов", group: "Ромашки", birth: "2019-11-17", parent: "Тлеуов Б.С.", phone: "+7 704 555 66 77", status: "studying", payment: "paid", note: "" },
{ id: 6, name: "Ева Соколова", group: "Ромашки", birth: "2019-03-04", parent: "Соколова И.А.", phone: "+7 705 666 77 88", status: "studying", payment: "current", note: "Нужен контроль приёма лекарств в 14:00" },
{ id: 7, name: "Жанибек Нурланов", group: "Солнышко", birth: "2021-12-21", parent: "Нурланова С.Б.", phone: "+7 706 777 88 99", status: "studying", payment: "paid", note: "" },
{ id: 8, name: "Катя Иванова", group: "Любимчики", birth: "2020-06-14", parent: "Иванов П.И.", phone: "+7 707 888 99 00", status: "sick", payment: "paid", note: "Больничный до 25 сентября" }
];
let children = [];
let activeStatusFilter = "all";
let searchQuery = "";
function loadChildren() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length > 0) {
children = parsed;
return;
}
}
} catch (e) {
console.warn("Не удалось загрузить данные, используется демо", e);
}
children = JSON.parse(JSON.stringify(DEMO_DATA));
saveChildren();
}
function saveChildren() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(children));
}
function getFilteredChildren() {
const q = searchQuery.toLowerCase().trim();
return children.filter((c) => {
const matchesStatus = activeStatusFilter === "all" || c.status === activeStatusFilter;
if (!matchesStatus) return false;
if (!q) return true;
const haystack = [c.name, c.group, c.parent, c.phone, c.note].filter(Boolean).join(" ").toLowerCase();
return haystack.includes(q);
});
}
function formatDate(iso) {
if (!iso) return "";
const [y, m, d] = iso.split("-");
return `${d}.${m}.${y}`;
}
function renderKpis() {
const total = children.length;
const studying = children.filter((c) => c.status === "studying").length;
const sick = children.filter((c) => c.status === "sick").length;
const overdue = children.filter((c) => c.payment === "overdue").length;
const attendancePct = total ? Math.round((studying / total) * 100) : 0;
document.getElementById("kpis").innerHTML = `
<div class="kt-ai-kpi">
<span class="kt-ai-kpi-label">Всего детей</span>
<span class="kt-ai-kpi-value">${total}</span>
<span class="kt-ai-kpi-sub">в саду</span>
</div>
<div class="kt-ai-kpi">
<span class="kt-ai-kpi-label">Сегодня в группе</span>
<span class="kt-ai-kpi-value">${studying}</span>
<span class="kt-ai-kpi-sub">${attendancePct}% посещаемость</span>
</div>
<div class="kt-ai-kpi">
<span class="kt-ai-kpi-label">На больничном</span>
<span class="kt-ai-kpi-value">${sick}</span>
<span class="kt-ai-kpi-sub">отсутствуют</span>
</div>
<div class="kt-ai-kpi">
<span class="kt-ai-kpi-label">Долги по оплате</span>
<span class="kt-ai-kpi-value">${overdue}</span>
<span class="kt-ai-kpi-sub">запросить родителю</span>
</div>
`;
}
function renderFilters() {
const statuses = ["all", "studying", "sick", "vacation"];
const labels = { all: "Все", studying: "Учатся", sick: "Больничный", vacation: "Отпуск" };
document.getElementById("status-filters").innerHTML = statuses
.map((s) => {
const count = s === "all" ? children.length : children.filter((c) => c.status === s).length;
const active = activeStatusFilter === s ? "active" : "";
return `<button type="button" class="kt-ai-chip ${active}" data-status="${s}">${labels[s]} · ${count}</button>`;
})
.join("");
document.getElementById("status-filters").querySelectorAll("[data-status]").forEach((btn) => {
btn.addEventListener("click", () => {
activeStatusFilter = btn.dataset.status;
renderAll();
});
});
}
function renderTable() {
const list = getFilteredChildren();
const tbody = document.getElementById("tbody");
const empty = document.getElementById("empty");
if (list.length === 0) {
tbody.innerHTML = "";
empty.hidden = false;
} else {
empty.hidden = true;
tbody.innerHTML = list
.map((c) => {
const birthDate = c.birth ? formatDate(c.birth) : "";
return `
<tr data-id="${c.id}">
<td>
<div class="kt-ai-child-name">${c.name}</div>
${c.birth ? `<div class="kt-ai-child-meta">р. ${birthDate}</div>` : ""}
</td>
<td>${c.group}</td>
<td>
<div>${c.parent || ""}</div>
${c.phone ? `<div class="kt-ai-child-meta">${c.phone}</div>` : ""}
</td>
<td><span class="kt-ai-status-pill is-${c.status}">${STATUS_LABELS[c.status] || c.status}</span></td>
<td><span class="kt-ai-status-pill is-${c.payment === "overdue" ? "sick" : c.payment === "paid" ? "studying" : "vacation"}">${PAYMENT_LABELS[c.payment] || c.payment}</span></td>
<td>
<div class="kt-ai-row-actions">
<button type="button" class="kt-ai-icon-button" data-action="edit" aria-label="Изменить">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="#pencil"></use></svg>
</button>
<button type="button" class="kt-ai-icon-button" data-action="delete" aria-label="Удалить">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="#trash"></use></svg>
</button>
</div>
</td>
</tr>
`;
})
.join("");
}
document.getElementById("count-label").textContent = `Показано ${list.length} из ${children.length}`;
tbody.querySelectorAll("[data-action]").forEach((btn) => {
const id = Number(btn.closest("tr").dataset.id);
btn.addEventListener("click", () => {
if (btn.dataset.action === "edit") openDrawer(id);
if (btn.dataset.action === "delete") removeChild(id);
});
});
}
function renderAll() {
renderKpis();
renderFilters();
renderTable();
}
function openDrawer(id = null) {
const drawer = document.getElementById("drawer");
const title = document.getElementById("drawer-title");
const form = document.getElementById("child-form");
form.reset();
document.getElementById("field-id").value = "";
if (id !== null) {
const child = children.find((c) => c.id === id);
if (!child) return;
title.textContent = "Ребёнок";
document.getElementById("field-id").value = child.id;
document.getElementById("field-name").value = child.name;
document.getElementById("field-group").value = child.group;
document.getElementById("field-birth").value = child.birth || "";
document.getElementById("field-parent").value = child.parent || "";
document.getElementById("field-phone").value = child.phone || "";
document.getElementById("field-status").value = child.status;
document.getElementById("field-payment").value = child.payment;
document.getElementById("field-note").value = child.note || "";
} else {
title.textContent = "Новый ребёнок";
}
drawer.hidden = false;
document.getElementById("field-name").focus();
}
function closeDrawer() {
document.getElementById("drawer").hidden = true;
}
function saveChild(e) {
e.preventDefault();
const id = document.getElementById("field-id").value;
const data = {
name: document.getElementById("field-name").value.trim(),
group: document.getElementById("field-group").value,
birth: document.getElementById("field-birth").value,
parent: document.getElementById("field-parent").value.trim(),
phone: document.getElementById("field-phone").value.trim(),
status: document.getElementById("field-status").value,
payment: document.getElementById("field-payment").value,
note: document.getElementById("field-note").value.trim()
};
if (!data.name) return;
if (id) {
const idx = children.findIndex((c) => c.id === Number(id));
if (idx !== -1) {
children[idx] = { ...children[idx], ...data };
showToast("Данные обновлены");
}
} else {
children.push({ id: Date.now(), ...data });
showToast("Ребёнок добавлен");
}
saveChildren();
renderAll();
closeDrawer();
}
function removeChild(id) {
const child = children.find((c) => c.id === id);
if (!child) return;
if (!confirm(`Удалить ${child.name}?`)) return;
children = children.filter((c) => c.id !== id);
saveChildren();
renderAll();
showToast("Запись удалена");
}
function exportCsv() {
const list = getFilteredChildren();
if (list.length === 0) {
showToast("Нет данных для выгрузки");
return;
}
const head = ["Имя ребёнка", "Группа", "Дата рождения", "Родитель", "Телефон", "Статус", "Оплата", "Заметка"];
const body = list.map((c) =>
[
c.name,
c.group,
c.birth ? formatDate(c.birth) : "",
c.parent,
c.phone,
STATUS_LABELS[c.status] || c.status,
PAYMENT_LABELS[c.payment] || c.payment,
c.note
]
.map((v) => '"' + String(v || "").replace(/"/g, '""') + '"')
.join(";")
);
const csv = "\uFEFF" + [head.join(";"), ...body].join("\n");
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = "deti-detsad.csv";
a.click();
showToast("Файл сформирован");
}
function showToast(message) {
const stack = document.getElementById("toasts");
const el = document.createElement("div");
el.className = "kt-ai-toast";
el.textContent = message;
stack.appendChild(el);
setTimeout(() => el.remove(), 2600);
}
function init() {
loadChildren();
renderAll();
document.getElementById("btn-new").addEventListener("click", () => openDrawer());
document.getElementById("btn-drawer-close").addEventListener("click", closeDrawer);
document.getElementById("btn-cancel").addEventListener("click", closeDrawer);
document.getElementById("child-form").addEventListener("submit", saveChild);
document.getElementById("btn-export").addEventListener("click", exportCsv);
document.getElementById("btn-clear-filters").addEventListener("click", () => {
activeStatusFilter = "all";
searchQuery = "";
document.getElementById("search").value = "";
renderAll();
});
document.getElementById("search").addEventListener("input", (e) => {
searchQuery = e.target.value;
renderTable();
});
}
document.addEventListener("DOMContentLoaded", init);