294 lines
13 KiB
HTML
294 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Расписание кружков</title>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; padding: 20px; }
|
||
.container { max-width: 1200px; margin: 0 auto; }
|
||
h1 { font-size: 28px; margin-bottom: 20px; color: #1d1d1f; }
|
||
|
||
.stats { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
|
||
.stat { background: white; padding: 12px 20px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||
.stat strong { font-size: 24px; color: #0071e3; }
|
||
|
||
.filters { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
|
||
.filter-btn { padding: 8px 16px; border: none; border-radius: 20px; background: white; cursor: pointer; font-size: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||
.filter-btn.active { background: #0071e3; color: white; }
|
||
|
||
.main { display: grid; grid-template-columns: 1fr 350px; gap: 24px; }
|
||
@media (max-width: 900px) { .main { grid-template-columns: 1fr; } }
|
||
|
||
.card { background: white; border-radius: 16px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||
.card h2 { font-size: 18px; margin-bottom: 16px; color: #1d1d1f; }
|
||
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e5e5; }
|
||
th { font-weight: 600; color: #6e6e73; font-size: 13px; text-transform: uppercase; }
|
||
tr:hover { background: #f5f5f7; }
|
||
|
||
.day-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||
.day-Пн { background: #e3f2fd; color: #1565c0; }
|
||
.day-Вт { background: #fff3e0; color: #e65100; }
|
||
.day-Ср { background: #e8f5e9; color: #2e7d32; }
|
||
.day-Чт { background: #fce4ec; color: #c2185b; }
|
||
.day-Пт { background: #f3e5f5; color: #7b1fa2; }
|
||
.day-Сб { background: #fff8e1; color: #f57f17; }
|
||
.day-Вс { background: #ffebee; color: #c62828; }
|
||
|
||
.actions { display: flex; gap: 8px; }
|
||
.btn { padding: 6px 12px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
||
.btn-edit { background: #0071e3; color: white; }
|
||
.btn-delete { background: #ff3b30; color: white; }
|
||
.btn-primary { background: #0071e3; color: white; padding: 10px 20px; font-size: 14px; }
|
||
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-group label { display: block; font-size: 13px; color: #6e6e73; margin-bottom: 6px; }
|
||
.form-group input, .form-group select { width: 100%; padding: 10px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
|
||
.form-group input:focus, .form-group select:focus { outline: none; border-color: #0071e3; }
|
||
|
||
.reminder-item { background: #fff9e6; border-left: 4px solid #ffcc00; padding: 12px; margin-bottom: 12px; border-radius: 8px; }
|
||
.reminder-item .time { font-size: 12px; color: #6e6e73; }
|
||
.reminder-item .text { font-size: 14px; margin: 4px 0; }
|
||
.reminder-item .delete { color: #ff3b30; cursor: pointer; font-size: 12px; }
|
||
|
||
.export-btn { margin-top: 16px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📅 Расписание кружков</h1>
|
||
|
||
<div class="stats">
|
||
<div class="stat"><strong id="total">0</strong> всего кружков</div>
|
||
<div class="stat"><strong id="byWeek">0</strong> на этой неделе</div>
|
||
</div>
|
||
|
||
<div class="filters" id="filters"></div>
|
||
|
||
<div class="main">
|
||
<div class="card">
|
||
<h2>Расписание</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>День</th>
|
||
<th>Время</th>
|
||
<th>Кружок</th>
|
||
<th>Место</th>
|
||
<th>Преподаватель</th>
|
||
<th>Такси</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="table"></tbody>
|
||
</table>
|
||
<button class="btn export-btn" onclick="exportCsv()">📥 Выгрузить в Excel</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2 id="formTitle">➕ Добавить кружок</h2>
|
||
<form id="form">
|
||
<input type="hidden" id="editId">
|
||
<div class="form-group">
|
||
<label>День недели</label>
|
||
<select id="day" required>
|
||
<option value="Пн">Понедельник</option>
|
||
<option value="Вт">Вторник</option>
|
||
<option value="Ср">Среда</option>
|
||
<option value="Чт">Четверг</option>
|
||
<option value="Пт">Пятница</option>
|
||
<option value="Сб">Суббота</option>
|
||
<option value="Вс">Воскресенье</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Время</label>
|
||
<input type="time" id="time" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Название кружка</label>
|
||
<input type="text" id="name" placeholder="Например: Футбол" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Место</label>
|
||
<input type="text" id="location" placeholder="Адрес или название">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Преподаватель</label>
|
||
<input type="text" id="teacher" placeholder="Имя преподавателя">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Телефон тренера</label>
|
||
<input type="tel" id="phone" placeholder="+7 777 123 45 67">
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="display:flex;align-items:center;gap:8px">
|
||
<input type="checkbox" id="taxi" style="width:auto">
|
||
Нужно такси
|
||
</label>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%">Сохранить</button>
|
||
<button type="button" class="btn" onclick="resetForm()" style="width:100%;margin-top:8px">Отмена</button>
|
||
</form>
|
||
|
||
<h2 style="margin-top:24px">⏰ Напоминания</h2>
|
||
<div id="reminders"></div>
|
||
<form id="reminderForm" style="margin-top:12px">
|
||
<div class="form-group">
|
||
<label>Текст напоминания</label>
|
||
<input type="text" id="reminderText" placeholder="Например: Не забыть форму!" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%">Добавить напоминание</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DAYS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||
let classes = [];
|
||
let reminders = [];
|
||
let filterDay = null;
|
||
|
||
async function load() {
|
||
classes = await fetch("/api/classes").then(r => r.json());
|
||
reminders = await fetch("/api/reminders").then(r => r.json());
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
const filtered = filterDay ? classes.filter(c => c.day === filterDay) : classes;
|
||
|
||
document.getElementById("table").innerHTML = filtered
|
||
.sort((a, b) => DAYS.indexOf(a.day) - DAYS.indexOf(b.day) || a.time.localeCompare(b.time))
|
||
.map(c => `
|
||
<tr>
|
||
<td><span class="day-badge day-${c.day}">${c.day}</span></td>
|
||
<td>${c.time}</td>
|
||
<td><strong>${c.name}</strong></td>
|
||
<td>${c.location || "—"}</td>
|
||
<td>${c.teacher || "—"}${c.phone ? `<br><a href="tel:${c.phone}" style="color:#0071e3">${c.phone}</a>` : ""}</td>
|
||
<td>${c.taxi ? "🚕" : "—"}</td>
|
||
<td class="actions">
|
||
<button class="btn btn-edit" onclick="editClass(${c.id})">✏️</button>
|
||
<button class="btn btn-delete" onclick="deleteClass(${c.id})">🗑️</button>
|
||
<button class="btn" style="background:#ff9500;color:white" onclick="notUrgent(${c.id})">⏰ Не успеваем</button>
|
||
</td>
|
||
</tr>
|
||
`).join("");
|
||
|
||
document.getElementById("total").textContent = classes.length;
|
||
document.getElementById("byWeek").textContent = classes.length;
|
||
|
||
document.getElementById("filters").innerHTML =
|
||
`<button class="filter-btn ${!filterDay ? "active" : ""}" onclick="setFilter(null)">Все</button>` +
|
||
DAYS.map(d => `<button class="filter-btn ${filterDay === d ? "active" : ""}" onclick="setFilter('${d}')">${d}</button>`).join("");
|
||
|
||
document.getElementById("reminders").innerHTML = reminders
|
||
.map(r => `
|
||
<div class="reminder-item">
|
||
<div class="time">${new Date(r.createdAt).toLocaleString("ru-RU")}</div>
|
||
<div class="text">${r.text}</div>
|
||
<div class="delete" onclick="deleteReminder(${r.id})">Удалить</div>
|
||
</div>
|
||
`).join("") || "<p style='color:#6e6e73;font-size:14px'>Нет напоминаний</p>";
|
||
}
|
||
|
||
function setFilter(day) {
|
||
filterDay = day;
|
||
render();
|
||
}
|
||
|
||
function resetForm() {
|
||
document.getElementById("form").reset();
|
||
document.getElementById("editId").value = "";
|
||
document.getElementById("formTitle").textContent = "➕ Добавить кружок";
|
||
}
|
||
|
||
function editClass(id) {
|
||
const c = classes.find(x => x.id === id);
|
||
if (!c) return;
|
||
document.getElementById("editId").value = c.id;
|
||
document.getElementById("day").value = c.day;
|
||
document.getElementById("time").value = c.time;
|
||
document.getElementById("name").value = c.name;
|
||
document.getElementById("location").value = c.location || "";
|
||
document.getElementById("teacher").value = c.teacher || "";
|
||
document.getElementById("phone").value = c.phone || "";
|
||
document.getElementById("taxi").checked = c.taxi || false;
|
||
document.getElementById("formTitle").textContent = "✏️ Редактировать";
|
||
}
|
||
|
||
async function deleteClass(id) {
|
||
if (!confirm("Удалить этот кружок?")) return;
|
||
await fetch("/api/classes", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }) });
|
||
load();
|
||
}
|
||
|
||
document.getElementById("form").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
const id = document.getElementById("editId").value;
|
||
const data = {
|
||
day: document.getElementById("day").value,
|
||
time: document.getElementById("time").value,
|
||
name: document.getElementById("name").value,
|
||
location: document.getElementById("location").value,
|
||
teacher: document.getElementById("teacher").value,
|
||
phone: document.getElementById("phone").value,
|
||
taxi: document.getElementById("taxi").checked
|
||
};
|
||
|
||
if (id) {
|
||
await fetch("/api/classes", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: Number(id), ...data }) });
|
||
} else {
|
||
await fetch("/api/classes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) });
|
||
}
|
||
resetForm();
|
||
load();
|
||
});
|
||
|
||
document.getElementById("reminderForm").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
await fetch("/api/reminders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: document.getElementById("reminderText").value, createdAt: new Date().toISOString() }) });
|
||
document.getElementById("reminderText").value = "";
|
||
load();
|
||
});
|
||
|
||
async function deleteReminder(id) {
|
||
await fetch("/api/reminders", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }) });
|
||
load();
|
||
}
|
||
|
||
async function notUrgent(id) {
|
||
const c = classes.find(x => x.id === id);
|
||
if (!c) return;
|
||
const msg = `⚠️ Не успеваем на ${c.name} (${c.day} ${c.time})!`;
|
||
if (c.phone && confirm(`Позвонить тренеру ${c.teacher || ""}?`)) {
|
||
window.location.href = "tel:" + c.phone;
|
||
} else {
|
||
await fetch("/api/reminders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: msg + " — позвоните тренеру: " + (c.phone || "нет телефона"), createdAt: new Date().toISOString() }) });
|
||
alert("Напоминание добавлено! Позвоните тренеру: " + (c.phone || "нет телефона"));
|
||
}
|
||
load();
|
||
}
|
||
|
||
function exportCsv() {
|
||
const head = ["День", "Время", "Кружок", "Место", "Преподаватель", "Телефон", "Такси"];
|
||
const body = classes.sort((a, b) => DAYS.indexOf(a.day) - DAYS.indexOf(b.day) || a.time.localeCompare(b.time))
|
||
.map(r => [r.day, r.time, r.name, r.location || "", r.teacher || "", r.phone || "", r.taxi ? "Да" : "Нет"]
|
||
.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 = "raspisanie.csv";
|
||
a.click();
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|