(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 () {});
})();
})();