(function () { "use strict"; var STORAGE_KEY = "rezim-day-1"; var WEEKS = 4; var WD = ["вс", "пн", "вт", "ср", "чт", "пт", "сб"]; var DEFAULTS = { wake: "07:00", bedtime: "22:30", schedule: [ { time: "07:00", title: "Подъём и стакан воды" }, { time: "07:10", title: "Зарядка 10–15 минут" }, { time: "07:30", title: "Завтрак" }, { time: "08:00", title: "Дорога и начало дела" }, { time: "13:00", title: "Обед" }, { time: "13:30", title: "Короткий отдых 15 минут" }, { time: "14:00", title: "Работа / учёба" }, { time: "18:00", title: "Конец рабочего дня" }, { time: "19:00", title: "Ужин" }, { time: "20:00", title: "Спорт / прогулка" }, { time: "21:30", title: "Без экранов, вечер без спешки" }, { time: "22:30", title: "Сон" } ], habits: [ { id: "h1", name: "Спорт", goal: 3, note: "Хотя бы 30 минут движения", done: [] }, { id: "h2", name: "30 минут чтения", goal: 5, note: "Книга перед сном вместо ленты", done: [] }, { id: "h3", name: "Дневник 3 строк", goal: 7, note: "Что получилось, что улучшить", done: [] }, { id: "h4", name: "Готовить дома", goal: 5, note: "Завтрак или ужин", done: [] } ] }; var state = null; var serverLogReady = false; // ---------- даты ---------- function iso(d) { var p = function (n) { return String(n).padStart(2, "0"); }; return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()); } function daysAgo(n) { var d = new Date(); d.setDate(d.getDate() - n); return d; } function lastNDays() { var a = []; for (var i = WEEKS * 7 - 1; i >= 0; i--) a.push(iso(daysAgo(i))); return a; } function fmtDate(d, withYear) { var months = ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"]; var s = d.getDate() + " " + months[d.getMonth()]; if (withYear) s += " " + d.getFullYear(); return s; } function todayStr() { return iso(new Date()); } // ---------- данные ---------- function freshLog() { var log = {}; lastNDays().forEach(function (d) { log[d] = { done: [], habits: {} }; }); return log; } function loadLocal() { var data = null; try { data = JSON.parse(localStorage.getItem(STORAGE_KEY)); } catch (_) {} return data; } function saveLocal() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (_) {} } function todayEntry() { var t = todayStr(); if (!state.log[t]) { state.log[t] = { done: [], habits: {} }; } return state.log[t]; } function applyLocal() { var local = loadLocal(); if (local && local.log && Object.keys(local.log).length) { state.log = Object.assign(freshLog(), local.log); } else { state.log = seedLog(); } saveLocal(); } function seedLog() { var log = freshLog(); lastNDays().forEach(function (d, i) { var e = log[d]; if (d === todayStr()) return; if (i % 7 === 5) { e.done = []; e.sleep = 8.5; return; } e.done = state.schedule.slice(0, i % 6 === 0 ? state.schedule.length - 2 : state.schedule.length - 1).map(function (it) { return it.time; }); e.sleep = Math.round((7 + (i % 3) * 0.5) * 10) / 10; }); return log; } function seedDemoDays() { if (loadLocal()) return; state.log = seedLog(); saveLocal(); } // ---------- сервер ---------- function pushToServer() { var payload = { wake: state.wake, bedtime: state.bedtime, schedule: state.schedule, habits: state.habits, log: state.log }; fetch("api/state", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(pushOk).catch(function () { pushFail(); }); } var pushTimer = null; function pushOk() { if (pushTimer) { clearTimeout(pushTimer); pushTimer = null; } if ("serviceWorker" in navigator && !serverLogReady) { pushTimer = setTimeout(function () { pushTimer = null; if ("serviceWorker" in navigator) pushFail(); }, 600); } else { serverLogReady = true; } } function pushFail() { toast("Не удалось сохранить на сервере — изменения пока только на этом устройстве", "warn"); } // ---------- утилиты ---------- function toast(msg, tone) { var stack = document.getElementById("toasts"); var el = document.createElement("div"); el.className = "kt-ai-toast"; if (tone) el.setAttribute("data-tone", tone); el.textContent = msg; stack.appendChild(el); setTimeout(function () { el.remove(); }, 4500); } function sleepHours() { function parts(v) { var ab = String(v || "").split(":"); return [parseInt(ab[0], 10) || 0, parseInt(ab[1], 10) || 0]; } var b = parts(state.bedtime), w = parts(state.wake); var bm = b[0] * 60 + b[1], wm = w[0] * 60 + w[1]; if (bm >= wm) return 24 - (bm - wm) / 60; return (1440 - bm + wm) / 60; } // ---------- KPI и статусы ---------- function currentDayProgress(e) { var total = state.schedule.length; var done = (e && e.done ? e.done : []).filter(function (tm) { return state.schedule.some(function (it) { return it.time === tm; }); }).length; return { total: total, done: done, pct: total ? Math.round((done / total) * 100) : 0 }; } function weekAvgProgress() { var days = lastNDays().slice(-7); var sum = 0; days.forEach(function (d) { var e = state.log[d]; if (e) sum += currentDayProgress(e).pct; }); return Math.round(sum / 7); } function currentDayStreak() { var days = lastNDays(); var streak = 0; for (var i = days.length - 1; i >= 0; i--) { var e = state.log[days[i]]; if (e && e.done.length >= state.schedule.length && e.done.length > 0) streak++; else if (days[i] !== todayStr()) break; } return streak === 0 && (function () { var e = state.log[todayStr()]; return e && e.done.length > 0; })() ? 1 : streak; } // ---------- рендер ---------- function renderTopbar() { var now = new Date(); document.getElementById("dateChip").textContent = WD[now.getDay()] + " · " + fmtDate(now); } function renderKpi() { var t = todayEntry(); var cur = currentDayProgress(t); document.getElementById("kpiDone").textContent = cur.done + " из " + cur.total; var weekAvg = weekAvgProgress(); document.getElementById("kpiWeek").textContent = weekAvg + " %"; document.querySelector("#kWeek .pfill").style.width = weekAvg + "%"; document.getElementById("kpiStreak").textContent = currentDayStreak(); var st = sleepHours().toFixed(1).replace('.', ','); document.getElementById("kpiSleep").textContent = st + " ч"; } function renderDayList() { var t = todayEntry(); var rows = state.schedule.map(function (it, idx) { var ck = t.done.indexOf(it.time) !== -1; return '
' + '' + '' + it.time + ' ' + it.title + '' + '' + (ck ? 'готово' : '') + '' + '
'; }).join(""); var total = state.schedule.length; var done = t.done.filter(function (tm) { return state.schedule.some(function (it) { return it.time === tm; }); }).length; document.getElementById("dayList").innerHTML = '
' + '' + '' + '' + rows + '' + '
ЧасПункт
' + '
'; document.getElementById("progressBar").style.width = (total ? (done / total) * 100 : 0) + "%"; document.getElementById("progressText").textContent = done + " из " + total; } function renderWeekGrid() { var days = lastNDays().slice(-7); var head = days.map(function (d) { var dt = new Date(d + "T12:00:00"); var isToday = d === todayStr(); var p = currentDayProgress(state.log[d]); var on = d === todayStr() || p.pct > 0; return '' + '' + WD[dt.getDay()] + "" + '' + ""; }).join(""); var grid = '
' + head + "
"; state.habits.forEach(function (h) { var cells = days.map(function (d) { var on = h.done.indexOf(d) !== -1; var isToday = d === todayStr(); return ""; }).join(""); var weekDone = days.filter(function (d) { return h.done.indexOf(d) !== -1; }).length; grid += '
' + "" + h.name + "" + '
' + cells + "
" + "" + weekDone + " / " + h.goal + "" + "
"; }); document.getElementById("weekGrid").innerHTML = grid; } function renderSleep() { var b = document.querySelector('[data-sleepkey="bedtime"]'); var w = document.querySelector('[data-sleepkey="wake"]'); if (b) b.innerHTML = '' + state.bedtime + ""; if (w) w.innerHTML = '' + state.wake + ""; } function renderAll() { renderTopbar(); renderKpi(); renderDayList(); renderWeekGrid(); renderSleep(); } // ---------- действия ---------- function afterChange() { saveLocal(); pushToServer(); renderAll(); } function toggleItem(time) { var t = todayEntry(); var i = t.done.indexOf(time); if (i === -1) t.done.push(time); else t.done.splice(i, 1); if (t.done.length === state.schedule.length && typeof t.sleep !== "number") t.sleep = sleepHours(); afterChange(); } function toggleHabitDay(habId, day) { var h = state.habits.find(function (x) { return x.id === habId; }); if (!h) return; var i = h.done.indexOf(day); if (i === -1) h.done.push(day); else h.done.splice(i, 1); afterChange(); } function markAllToday() { var t = todayEntry(); var allDone = t.done.length >= state.schedule.length; state.schedule.forEach(function (it) { var i = t.done.indexOf(it.time); if (allDone && i !== -1) t.done.splice(i, 1); if (!allDone && i === -1) t.done.push(it.time); }); if (typeof t.sleep !== "number") t.sleep = sleepHours(); toast(allDone ? "День сброшен" : "Все пункты отмечены — отличный день", "ok"); afterChange(); } // ---------- drawer: день ---------- var drawerDay = null; function openDayDrawer(day) { drawerDay = day; var dt = new Date(day + "T12:00:00"); var e = state.log[day] || { done: [], habits: {} }; var cur = currentDayProgress(e); var isToday = day === todayStr(); var pill = document.getElementById("dayDrawerPill"); if (isToday) { pill.setAttribute("data-status", cur.done === 0 ? "info" : "warn"); pill.textContent = cur.done + " из " + cur.total + " — твой текущий день"; } else if (cur.pct === 100) { pill.setAttribute("data-status", "ok"); pill.textContent = "День по плану — 100%"; } else if (cur.done === 0) { pill.setAttribute("data-status", "risk"); pill.textContent = "День не выполнен"; } else { pill.setAttribute("data-status", "warn"); pill.textContent = "Выполнено на " + cur.pct + " %"; } document.getElementById("dayDrawerDate").textContent = WD[dt.getDay()] + ", " + fmtDate(dt, true); var rows = state.schedule.map(function (it) { var ck = (e.done || []).indexOf(it.time) !== -1; return '
' + '' + it.time + "" + it.title + "" + '' + (ck ? "готово" : "не сделано") + "
"; }).join(""); if (typeof e.sleep === "number") { rows += '
Сон за этот день' + '' + e.sleep.toFixed(1).replace(".", ",") + " ч
"; } document.getElementById("dayDrawerList").innerHTML = rows || '
Нет данных
'; document.getElementById("dayDrawer").style.display = "block"; } // ---------- drawer: привычка ---------- var drawerHabitId = null; function openHabitDrawer(habId) { var h = state.habits.find(function (x) { return x.id === habId; }); if (!h) return; drawerHabitId = habId; document.getElementById("habitDrawerTitle").textContent = h.name; document.getElementById("habitDrawerNote").textContent = h.note || ""; renderHabitDrawerCells(); document.getElementById("habitDrawer").style.display = "block"; } function renderHabitDrawerCells() { var h = state.habits.find(function (x) { return x.id === drawerHabitId; }); if (!h) return; var days = lastNDays().slice(-7); var wrap = document.getElementById("habitDrawerCells"); wrap.innerHTML = days.map(function (d) { var dt = new Date(d + "T12:00:00"); var on = h.done.indexOf(d) !== -1; return ""; }).join(""); } function deleteHabit() { if (!drawerHabitId) return; state.habits = state.habits.filter(function (x) { return x.id !== drawerHabitId; }); drawerHabitId = null; document.getElementById("habitDrawer").style.display = "none"; afterChange(); toast("Привычка удалена"); } // ---------- modal: новая привычка ---------- function openHabitModal() { document.getElementById("hName").value = ""; document.getElementById("hNote").value = ""; document.getElementById("hDays").value = "5"; document.getElementById("habitOverlay").style.display = "flex"; document.getElementById("habitModal").hidden = false; setTimeout(function () { document.getElementById("hName").focus(); }, 50); } function closeHabitModal() { document.getElementById("habitOverlay").style.display = "none"; document.getElementById("habitModal").hidden = true; } function saveHabit() { var name = document.getElementById("hName").value.trim(); if (!name) { toast("Дай привычке название", "warn"); return; } state.habits.push({ id: "h" + Date.now().toString(36), name: name, note: document.getElementById("hNote").value.trim(), goal: parseInt(document.getElementById("hDays").value, 10) || 5, done: [] }); closeHabitModal(); afterChange(); toast("Привычка добавлена", "ok"); } function closeDrawers() { document.getElementById("dayDrawer").style.display = "none"; document.getElementById("habitDrawer").style.display = "none"; drawerDay = null; drawerHabitId = null; } // ---------- события ---------- function bind() { var dayList = document.getElementById("dayList"); dayList.addEventListener("change", function (ev) { var cb = ev.target.closest("input[type=checkbox]"); if (!cb) return; var row = cb.closest("[data-idx]"); var it = state.schedule[parseInt(row.getAttribute("data-idx"), 10)]; if (it) toggleItem(it.time); }); dayList.addEventListener("click", function (ev) { if (ev.target.closest(".kt-ai-checkbox")) return; var name = ev.target.closest(".item-name"); if (!name) return; var row = name.closest("[data-idx]"); var it = state.schedule[parseInt(row.getAttribute("data-idx"), 10)]; if (it) toast(it.time + " · " + it.title + " — отметь галочкой, когда сделаешь", "info"); }); var grid = document.getElementById("weekGrid"); grid.addEventListener("click", function (ev) { var dcell = ev.target.closest("[data-dkeyday]"); if (dcell) { openDayDrawer(dcell.getAttribute("data-dkeyday")); return; } var hcell = ev.target.closest(".wcell[data-habid]"); if (hcell) openHabitDrawer(hcell.getAttribute("data-habid")); }); grid.addEventListener("keydown", function (ev) { if (ev.key !== "Enter" && ev.key !== " ") return; var cell = ev.target.closest(".wcell[data-habid]"); if (!cell) return; ev.preventDefault(); openHabitDrawer(cell.getAttribute("data-habid")); }); document.getElementById("habitDrawer").addEventListener("click", function (ev) { var c = ev.target.closest("i[data-habid2]"); if (c) toggleHabitDay(c.getAttribute("data-habid2"), c.getAttribute("data-day")); }); document.getElementById("btnToday").addEventListener("click", markAllToday); document.getElementById("btnAddHabit").addEventListener("click", openHabitModal); document.getElementById("btnHabitCancel").addEventListener("click", closeHabitModal); document.getElementById("btnHabitSave").addEventListener("click", saveHabit); document.getElementById("btnDeleteHabit").addEventListener("click", deleteHabit); document.getElementById("habitOverlay").addEventListener("click", closeHabitModal); document.addEventListener("keydown", function (ev) { if (ev.key !== "Escape") return; closeDrawers(); closeHabitModal(); }); ["#day", "#habits", "#sleep"].forEach(function (sel) { var a = document.querySelector('.kt-ai-nav-item[href="' + sel + '"]'); if (!a) return; a.addEventListener("click", function (ev) { ev.preventDefault(); document.querySelector(sel).scrollIntoView({ behavior: "smooth", block: "start" }); a.classList.add("is-active"); document.querySelectorAll(".kt-ai-nav-item").forEach(function (x) { if (x !== a) x.classList.remove("is-active"); }); }); }); } // ---------- старт ---------- (function init() { state = { wake: DEFAULTS.wake, bedtime: DEFAULTS.bedtime, schedule: DEFAULTS.schedule.map(function (x) { return Object.assign({}, x); }), habits: DEFAULTS.habits.map(function (x) { return { id: x.id, name: x.name, note: x.note, goal: x.goal, done: x.done.concat() }; }), log: {} }; seedDemoDays(); applyLocal(); bind(); renderAll(); fetch("api/state") .then(function (r) { return r.ok ? r.json() : null; }) .then(function (data) { if (data && data.log && Object.keys(data.log).length) { state.wake = data.wake || state.wake; state.bedtime = data.bedtime || state.bedtime; state.schedule = data.schedule && data.schedule.length ? data.schedule : state.schedule; state.habits = data.habits ? data.habits : state.habits; state.log = Object.assign(freshLog(), data.log); serverLogReady = true; saveLocal(); renderAll(); } }) .catch(function () {}); })(); })();