312 lines
12 KiB
JavaScript
312 lines
12 KiB
JavaScript
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);
|