promt-dlya-vibecoding-treker/index.html

1365 lines
37 KiB
HTML
Raw 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.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Планер</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-main: #2C1810;
--bg-card: #3E2418;
--bg-hover: #4A2C1A;
--bg-border: #5C3A28;
--text-main: #F5E6D3;
--text-secondary: #D9C4A8;
--text-muted: #A68B6F;
--accent1: #E8B598;
--accent2: #F7EDE2;
--accent3: #D4A574;
--accent4: #C08B65;
--accent5: #A67B5B;
--accent6: #8B6F4E;
--success: #B5A47A;
--shadow: 0 4px 20px rgba(44, 24, 16, 0.5);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Manrope', system-ui, sans-serif;
background: var(--bg-main);
color: var(--text-main);
min-height: 100vh;
padding: 24px;
line-height: 1.5;
}
h1, h2, h3 {
font-family: 'Cormorant Garamond', Georgia, serif;
font-weight: 600;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Header Navigation */
.header {
text-align: center;
margin-bottom: 24px;
}
.date-navigator {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 16px;
}
.nav-btn {
background: var(--bg-card);
border: 1px solid var(--bg-border);
color: var(--text-secondary);
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.nav-btn:hover {
background: var(--bg-hover);
color: var(--text-main);
}
.current-date {
font-size: 24px;
color: var(--text-main);
cursor: pointer;
padding: 8px 16px;
border-radius: 8px;
transition: background 0.2s;
}
.current-date:hover {
background: var(--bg-card);
}
.today-btn {
background: var(--accent1);
border: none;
color: var(--bg-main);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.today-btn:hover {
opacity: 0.9;
transform: scale(1.02);
}
.view-toggle {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
}
.view-btn {
background: transparent;
border: 1px solid var(--bg-border);
color: var(--text-secondary);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.view-btn.active {
background: var(--accent1);
border-color: var(--accent1);
color: var(--bg-main);
}
.title {
font-size: 36px;
color: var(--accent1);
font-style: italic;
margin-bottom: 8px;
}
.subtitle {
color: var(--text-secondary);
font-size: 16px;
}
/* Main Layout */
.main-layout {
display: grid;
grid-template-columns: 1fr 280px;
gap: 24px;
}
/* Task Sections */
.task-section {
background: var(--bg-card);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid;
box-shadow: var(--shadow);
}
.task-section.morning { border-color: var(--accent2); }
.task-section.day { border-color: var(--accent3); }
.task-section.evening { border-color: var(--accent4); }
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 18px;
font-style: italic;
}
.morning .section-header { color: var(--accent2); }
.day .section-header { color: var(--accent3); }
.evening .section-header { color: var(--accent4); }
.task-list {
list-style: none;
}
.task-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
transition: background 0.2s;
position: relative;
}
.task-item:hover {
background: var(--bg-hover);
}
.task-checkbox {
width: 22px;
height: 22px;
border: 2px solid var(--bg-border);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
margin-top: 2px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
}
.task-checkbox:hover {
transform: scale(1.1);
border-color: var(--accent1);
}
.task-checkbox.checked {
background: var(--accent1);
border-color: var(--accent1);
}
.task-checkbox.checked::after {
content: '✓';
color: var(--bg-main);
font-weight: bold;
font-size: 14px;
}
.task-content {
flex: 1;
min-width: 0;
}
.task-title {
font-size: 16px;
color: var(--text-main);
border: none;
background: transparent;
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.task-title:hover {
background: rgba(255,255,255,0.05);
}
.task-title.completed {
text-decoration: line-through;
opacity: 0.5;
}
.task-time {
font-size: 12px;
color: var(--text-muted);
margin-left: 8px;
}
.task-comment {
font-size: 13px;
color: var(--text-muted);
margin-top: 6px;
padding-left: 8px;
line-height: 1.4;
}
.task-menu {
opacity: 0;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
padding: 4px;
transition: all 0.2s;
}
.task-item:hover .task-menu { opacity: 1; }
.task-menu:hover { color: var(--accent1); }
.add-task-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bg-border);
color: var(--text-secondary);
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.add-task-btn:hover {
border-color: var(--accent1);
color: var(--accent1);
}
/* Week Overview */
.week-overview {
background: var(--bg-card);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow);
height: fit-content;
}
.week-title {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 13px;
}
.week-table {
width: 100%;
border-collapse: collapse;
}
.week-table td {
padding: 10px 8px;
border-bottom: 1px solid var(--bg-border);
cursor: pointer;
transition: background 0.2s;
}
.week-table tr:hover td {
background: var(--bg-hover);
}
.week-table .today {
background: var(--bg-hover);
}
.week-day {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
}
.week-date {
font-size: 14px;
color: var(--text-main);
font-weight: 500;
}
.week-progress {
text-align: right;
font-size: 12px;
color: var(--text-secondary);
}
.week-total {
padding-top: 16px;
font-size: 13px;
color: var(--text-secondary);
text-align: center;
}
/* Overall Progress */
.overall-progress {
margin-top: 24px;
padding: 20px;
background: var(--bg-card);
border-radius: 12px;
box-shadow: var(--shadow);
}
.overall-title {
font-size: 14px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
}
.overall-stats {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 12px;
}
.overall-count {
font-size: 28px;
color: var(--accent1);
font-weight: 600;
}
.overall-percent {
font-size: 18px;
color: var(--text-muted);
}
.overall-bar-bg {
height: 10px;
background: var(--bg-border);
border-radius: 5px;
overflow: hidden;
}
.overall-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent5), var(--accent1));
border-radius: 5px;
transition: width 0.3s ease-out;
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-card);
border-radius: 12px;
padding: 24px;
width: 100%;
max-width: 500px;
box-shadow: var(--shadow);
animation: modalFade 0.2s ease-out;
}
@keyframes modalFade {
from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
.modal-title {
font-size: 22px;
color: var(--text-main);
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.form-input, .form-textarea, .form-select {
width: 100%;
padding: 10px 12px;
background: var(--bg-main);
border: 1px solid var(--bg-border);
border-radius: 6px;
color: var(--text-main);
font-size: 14px;
font-family: inherit;
}
.form-input:focus, .form-textarea:focus, .form-select:focus {
outline: none;
border-color: var(--accent1);
}
.form-textarea {
min-height: 80px;
resize: vertical;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.radio-option {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.radio-option input[type="radio"] {
accent-color: var(--accent1);
}
.frequency-options {
margin-top: 10px;
padding-left: 24px;
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
}
.btn-primary {
background: var(--accent1);
border: none;
color: var(--bg-main);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
transform: scale(1.02);
}
.btn-secondary {
background: transparent;
border: 1px solid var(--bg-border);
color: var(--text-secondary);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--bg-hover);
color: var(--text-main);
}
/* Calendar Modal */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-top: 16px;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
background: var(--bg-main);
}
.calendar-day:hover {
background: var(--bg-hover);
}
.calendar-day.today {
border: 2px solid var(--accent1);
}
.calendar-day.selected {
background: var(--accent1);
color: var(--bg-main);
}
.calendar-day.has-tasks {
border: 1px solid var(--accent3);
}
.calendar-day.full-complete {
background: var(--success);
color: var(--bg-main);
}
.calendar-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 8px;
}
.calendar-header div {
text-align: center;
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
}
/* Month View */
.month-view {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.month-day {
aspect-ratio: 1;
background: var(--bg-card);
border-radius: 8px;
padding: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--bg-border);
}
.month-day:hover {
transform: scale(1.05);
border-color: var(--accent1);
}
.month-day .date {
font-size: 14px;
font-weight: 600;
color: var(--text-main);
margin-bottom: 4px;
}
.month-day .progress {
font-size: 11px;
color: var(--text-muted);
}
.month-day.has-tasks {
border-color: var(--accent3);
}
.month-day.full-complete {
background: var(--success);
}
.month-day.full-complete .date,
.month-day.full-complete .progress {
color: var(--bg-main);
}
/* Data buttons */
.data-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 8px;
}
.data-btn {
background: var(--bg-card);
border: 1px solid var(--bg-border);
color: var(--text-muted);
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.data-btn:hover {
color: var(--text-main);
border-color: var(--accent1);
}
/* Dropdown menu */
.dropdown {
position: absolute;
right: 0;
top: 100%;
background: var(--bg-card);
border: 1px solid var(--bg-border);
border-radius: 6px;
box-shadow: var(--shadow);
z-index: 100;
display: none;
min-width: 150px;
}
.dropdown.active {
display: block;
}
.dropdown-item {
padding: 8px 12px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: background 0.2s;
}
.dropdown-item:hover {
background: var(--bg-hover);
color: var(--text-main);
}
.dropdown-item.danger {
color: var(--accent4);
}
/* Responsive */
@media (max-width: 900px) {
.main-layout {
grid-template-columns: 1fr;
}
.week-overview {
overflow-x: auto;
}
}
@media (max-width: 600px) {
body { padding: 12px; }
.date-navigator {
flex-wrap: wrap;
gap: 8px;
}
.title { font-size: 24px; }
.current-date { font-size: 18px; }
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="date-navigator">
<button class="nav-btn" onclick="changeDay(-1)">←</button>
<div class="current-date" id="currentDate" onclick="showCalendarModal()">Понедельник, 15 декабря 2025</div>
<button class="nav-btn" onclick="changeDay(1)">→</button>
<button class="today-btn" id="todayBtn" onclick="goToToday()">Сегодня</button>
</div>
<div class="view-toggle">
<button class="view-btn active" onclick="setView('day')">День</button>
<button class="view-btn" onclick="setView('month')">Месяц</button>
</div>
</header>
<!-- Day View -->
<div id="dayView">
<h1 class="title">Ты хозяин своих привычек</h1>
<p class="subtitle" id="daySubtitle">Выполнено 0 из 0 задач</p>
<div class="main-layout" style="margin-top: 24px;">
<div>
<div class="task-section morning">
<h2 class="section-header">☀️ Утро</h2>
<ul class="task-list" id="morningTasks"></ul>
<button class="add-task-btn" onclick="openTaskModal('morning')">+ Добавить задачу</button>
</div>
<div class="task-section day">
<h2 class="section-header">☕ День</h2>
<ul class="task-list" id="dayTasks"></ul>
<button class="add-task-btn" onclick="openTaskModal('day')">+ Добавить задачу</button>
</div>
<div class="task-section evening">
<h2 class="section-header">🌙 Вечер</h2>
<ul class="task-list" id="eveningTasks"></ul>
<button class="add-task-btn" onclick="openTaskModal('evening')">+ Добавить задачу</button>
</div>
</div>
<div class="week-overview">
<h3 class="week-title">Обзор недели</h3>
<table class="week-table" id="weekTable"></table>
<div class="week-total" id="weekTotal"></div>
</div>
</div>
<div class="overall-progress">
<h3 class="overall-title">Общий прогресс за месяц</h3>
<div class="overall-stats">
<div class="overall-count" id="overallCount">0 / 0</div>
<div class="overall-percent" id="overallPercent">0%</div>
</div>
<div class="overall-bar-bg">
<div class="overall-bar-fill" id="overallBar" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Month View -->
<div id="monthView" style="display: none;">
<h1 class="title" id="monthTitle">Декабрь 2025</h1>
<div class="month-view" id="monthGrid" style="margin-top: 24px;"></div>
</div>
</div>
<!-- Task Modal -->
<div class="modal-overlay" id="taskModal">
<div class="modal">
<h2 class="modal-title" id="taskModalTitle">Новая задача</h2>
<input type="hidden" id="taskId">
<div class="form-group">
<label class="form-label">Название задачи *</label>
<input type="text" class="form-input" id="taskTitle" placeholder="Например: Пробежка 1 км">
</div>
<div class="form-group">
<label class="form-label">Время выполнения</label>
<input type="time" class="form-input" id="taskTime">
</div>
<div class="form-group">
<label class="form-label">Комментарий</label>
<textarea class="form-textarea" id="taskComment" placeholder="Заметки, детали, напоминания..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Период</label>
<div class="radio-group">
<label class="radio-option">
<input type="radio" name="recurrence" value="once" onchange="toggleRecurrence(false)">
Одноразовая задача
</label>
<label class="radio-option">
<input type="radio" name="recurrence" value="recurring" onchange="toggleRecurrence(true)">
Повторять до...
</label>
</div>
<div class="frequency-options" id="recurrenceOptions" style="display: none;">
<input type="date" class="form-input" id="endDate" style="margin-bottom: 10px;">
<select class="form-select" id="frequency">
<option value="daily">Каждый день</option>
<option value="weekdays">По будням</option>
<option value="weekends">По выходным</option>
<option value="weekly">Раз в неделю</option>
</select>
</div>
</div>
<div class="modal-buttons">
<button class="btn-secondary" onclick="closeTaskModal()">Отмена</button>
<button class="btn-primary" onclick="saveTask()">Сохранить</button>
</div>
</div>
</div>
<!-- Calendar Modal -->
<div class="modal-overlay" id="calendarModal">
<div class="modal">
<h2 class="modal-title">Выберите дату</h2>
<div class="calendar-header">
<div>Пн</div><div>Вт</div><div>Ср</div><div>Чт</div><div>Пт</div><div>Сб</div><div>Вс</div>
</div>
<div class="calendar-grid" id="calendarGrid"></div>
<div class="modal-buttons">
<button class="btn-secondary" onclick="closeCalendarModal()">Закрыть</button>
</div>
</div>
</div>
<!-- Data buttons -->
<div class="data-buttons">
<button class="data-btn" onclick="exportData()">Экспорт JSON</button>
<button class="data-btn" onclick="document.getElementById('importFile').click()">Импорт JSON</button>
<input type="file" id="importFile" accept=".json" style="display: none;" onchange="importData(event)">
</div>
<script>
// State
let currentDate = new Date();
let viewMode = 'day';
let selectedSection = 'morning';
let editingTaskId = null;
let activeDropdown = null;
let appData = {
tasks: [],
completions: {}
};
// Storage
const STORAGE_KEY = 'daily-planner-v1';
function loadData() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
appData = JSON.parse(saved);
}
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(appData));
}
// Date helpers
function formatDateKey(date) {
return date.toISOString().split('T')[0];
}
function formatDisplayDate(date) {
const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' };
return date.toLocaleDateString('ru-RU', options);
}
function getWeekDays(date) {
const day = date.getDay() || 7;
const start = new Date(date);
start.setDate(date.getDate() - day + 1);
const days = [];
for (let i = 0; i < 7; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
days.push(d);
}
return days;
}
function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6;
}
function isSameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
function getDaysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
// Task filtering
function getTasksForDate(date) {
const dateKey = formatDateKey(date);
return appData.tasks.filter(task => {
const taskDate = task.recurrence.startDate;
if (task.recurrence.type === 'once') {
return taskDate === dateKey;
}
if (!task.recurrence.endDate || dateKey > task.recurrence.endDate) {
return false;
}
if (dateKey < taskDate) {
return false;
}
const freq = task.recurrence.frequency;
if (freq === 'daily') return true;
if (freq === 'weekdays' && !isWeekend(date)) return true;
if (freq === 'weekends' && isWeekend(date)) return true;
if (freq === 'weekly' && date.getDay() === new Date(taskDate).getDay()) return true;
return false;
});
}
function isTaskCompleted(taskId, dateKey) {
return appData.completions[dateKey]?.[taskId] || false;
}
// Render
function render() {
document.getElementById('currentDate').textContent = formatDisplayDate(currentDate);
const today = new Date();
const todayBtn = document.getElementById('todayBtn');
todayBtn.style.display = isSameDay(currentDate, today) ? 'none' : 'block';
if (viewMode === 'day') {
renderDayView();
} else {
renderMonthView();
}
}
function renderDayView() {
const dateKey = formatDateKey(currentDate);
const tasks = getTasksForDate(currentDate);
['morning', 'day', 'evening'].forEach(section => {
const container = document.getElementById(`${section}Tasks`);
const sectionTasks = tasks.filter(t => t.section === section);
container.innerHTML = sectionTasks.map(task => {
const completed = isTaskCompleted(task.id, dateKey);
return `
<li class="task-item">
<div class="task-checkbox ${completed ? 'checked' : ''}"
onclick="toggleTask('${task.id}')"></div>
<div class="task-content">
<span class="task-title ${completed ? 'completed' : ''}"
onclick="editTaskInline('${task.id}')">${task.title}</span>
${task.time ? `<span class="task-time">${task.time}</span>` : ''}
${task.comment ? `<div class="task-comment">${task.comment}</div>` : ''}
</div>
<button class="task-menu" onclick="toggleDropdown('${task.id}')">⋯</button>
<div class="dropdown" id="dropdown-${task.id}">
<div class="dropdown-item" onclick="openTaskModal('${task.section}', '${task.id}')">Редактировать</div>
<div class="dropdown-item danger" onclick="deleteTask('${task.id}')">Удалить</div>
</div>
</li>
`;
}).join('');
});
const completedCount = tasks.filter(t => isTaskCompleted(t.id, dateKey)).length;
document.getElementById('daySubtitle').textContent = `Выполнено ${completedCount} из ${tasks.length} задач`;
renderWeekOverview();
renderOverallProgress();
}
function renderWeekOverview() {
const weekDays = getWeekDays(currentDate);
const dateKey = formatDateKey(currentDate);
let html = '';
let totalTasks = 0;
let totalCompleted = 0;
weekDays.forEach(day => {
const dayKey = formatDateKey(day);
const dayTasks = getTasksForDate(day);
const completed = dayTasks.filter(t => isTaskCompleted(t.id, dayKey)).length;
const percent = dayTasks.length > 0 ? Math.round((completed / dayTasks.length) * 100) : 0;
totalTasks += dayTasks.length;
totalCompleted += completed;
const dayNames = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
const isToday = isSameDay(day, currentDate);
html += `
<tr onclick="selectDate('${dayKey}')" class="${isToday ? 'today' : ''}">
<td>
<div class="week-day">${dayNames[day.getDay()]}</div>
<div class="week-date">${day.getDate()}.${day.getMonth() + 1}</div>
</td>
<td class="week-progress">${completed}/${dayTasks.length} · ${percent}%</td>
</tr>
`;
});
document.getElementById('weekTable').innerHTML = html;
document.getElementById('weekTotal').textContent =
`Итого за неделю: ${totalCompleted} / ${totalTasks} · ${totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0}%`;
}
function renderOverallProgress() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
let totalTasks = 0;
let totalCompleted = 0;
for (let d = 1; d <= daysInMonth; d++) {
const date = new Date(year, month, d);
const dateKey = formatDateKey(date);
const tasks = getTasksForDate(date);
tasks.forEach(task => {
totalTasks++;
if (isTaskCompleted(task.id, dateKey)) {
totalCompleted++;
}
});
}
const percent = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
document.getElementById('overallCount').textContent = `${totalCompleted} / ${totalTasks}`;
document.getElementById('overallPercent').textContent = `${percent}%`;
document.getElementById('overallBar').style.width = `${percent}%`;
}
function renderMonthView() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
document.getElementById('monthTitle').textContent =
currentDate.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' });
let html = '';
for (let d = 1; d <= daysInMonth; d++) {
const date = new Date(year, month, d);
const dateKey = formatDateKey(date);
const tasks = getTasksForDate(date);
const completed = tasks.filter(t => isTaskCompleted(t.id, dateKey)).length;
const percent = tasks.length > 0 ? Math.round((completed / tasks.length) * 100) : 0;
const hasTasks = tasks.length > 0;
const fullComplete = tasks.length > 0 && completed === tasks.length;
html += `
<div class="month-day ${hasTasks ? 'has-tasks' : ''} ${fullComplete ? 'full-complete' : ''}"
onclick="selectDate('${dateKey}')">
<div class="date">${d}</div>
<div class="progress">${tasks.length > 0 ? percent + '%' : '-'}</div>
</div>
`;
}
document.getElementById('monthGrid').innerHTML = html;
}
// Actions
function changeDay(delta) {
currentDate.setDate(currentDate.getDate() + delta);
render();
}
function selectDate(dateKey) {
currentDate = new Date(dateKey + 'T12:00:00');
viewMode = 'day';
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.view-btn:first-child').classList.add('active');
closeCalendarModal();
render();
}
function goToToday() {
currentDate = new Date();
render();
}
function setView(mode) {
viewMode = mode;
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.textContent.toLowerCase() === mode));
document.getElementById('dayView').style.display = mode === 'day' ? 'block' : 'none';
document.getElementById('monthView').style.display = mode === 'month' ? 'block' : 'none';
if (mode === 'month') renderMonthView();
}
function toggleTask(taskId) {
const dateKey = formatDateKey(currentDate);
if (!appData.completions[dateKey]) {
appData.completions[dateKey] = {};
}
appData.completions[dateKey][taskId] = !appData.completions[dateKey][taskId];
saveData();
render();
}
function toggleDropdown(taskId) {
if (activeDropdown && activeDropdown !== taskId) {
document.getElementById(`dropdown-${activeDropdown}`).classList.remove('active');
}
const dropdown = document.getElementById(`dropdown-${taskId}`);
dropdown.classList.toggle('active');
activeDropdown = dropdown.classList.contains('active') ? taskId : null;
}
function closeAllDropdowns() {
if (activeDropdown) {
document.getElementById(`dropdown-${activeDropdown}`).classList.remove('active');
activeDropdown = null;
}
}
function openTaskModal(section, taskId = null) {
closeAllDropdowns();
selectedSection = section;
editingTaskId = taskId;
const modal = document.getElementById('taskModal');
const title = document.getElementById('taskModalTitle');
if (taskId) {
const task = appData.tasks.find(t => t.id === taskId);
title.textContent = 'Редактировать задачу';
document.getElementById('taskId').value = task.id;
document.getElementById('taskTitle').value = task.title;
document.getElementById('taskTime').value = task.time || '';
document.getElementById('taskComment').value = task.comment || '';
const isRecurring = task.recurrence.type === 'recurring';
document.querySelector(`input[name="recurrence"][value="${isRecurring ? 'recurring' : 'once'}"]`).checked = true;
toggleRecurrence(isRecurring);
if (isRecurring) {
document.getElementById('endDate').value = task.recurrence.endDate || '';
document.getElementById('frequency').value = task.recurrence.frequency || 'daily';
}
} else {
title.textContent = 'Новая задача';
document.getElementById('taskId').value = '';
document.getElementById('taskTitle').value = '';
document.getElementById('taskTime').value = '';
document.getElementById('taskComment').value = '';
document.querySelector('input[name="recurrence"][value="once"]').checked = true;
toggleRecurrence(false);
}
modal.classList.add('active');
}
function closeTaskModal() {
document.getElementById('taskModal').classList.remove('active');
editingTaskId = null;
}
function toggleRecurrence(show) {
document.getElementById('recurrenceOptions').style.display = show ? 'block' : 'none';
}
function saveTask() {
const title = document.getElementById('taskTitle').value.trim();
if (!title) {
alert('Введите название задачи');
return;
}
const taskId = document.getElementById('taskId').value || 'task-' + Date.now();
const isRecurring = document.querySelector('input[name="recurrence"]:checked').value === 'recurring';
const taskData = {
id: taskId,
title,
time: document.getElementById('taskTime').value,
comment: document.getElementById('taskComment').value,
section: selectedSection,
recurrence: {
type: isRecurring ? 'recurring' : 'once',
startDate: formatDateKey(currentDate),
endDate: isRecurring ? document.getElementById('endDate').value : null,
frequency: document.getElementById('frequency').value
}
};
const existingIndex = appData.tasks.findIndex(t => t.id === taskId);
if (existingIndex >= 0) {
appData.tasks[existingIndex] = taskData;
} else {
appData.tasks.push(taskData);
}
saveData();
closeTaskModal();
render();
}
function editTaskInline(taskId) {
const task = appData.tasks.find(t => t.id === taskId);
if (task) {
selectedSection = task.section;
openTaskModal(task.section, taskId);
}
}
function deleteTask(taskId) {
closeAllDropdowns();
if (confirm('Удалить эту задачу?')) {
appData.tasks = appData.tasks.filter(t => t.id !== taskId);
saveData();
render();
}
}
// Calendar Modal
function showCalendarModal() {
const modal = document.getElementById('calendarModal');
const grid = document.getElementById('calendarGrid');
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
const firstDay = new Date(year, month, 1).getDay() || 7;
const today = new Date();
let html = '';
for (let i = 1; i < firstDay; i++) {
html += '<div></div>';
}
for (let d = 1; d <= daysInMonth; d++) {
const date = new Date(year, month, d);
const dateKey = formatDateKey(date);
const tasks = getTasksForDate(date);
const completed = tasks.filter(t => isTaskCompleted(t.id, dateKey)).length;
const isToday = isSameDay(date, today);
const isSelected = isSameDay(date, currentDate);
const hasTasks = tasks.length > 0;
const fullComplete = tasks.length > 0 && completed === tasks.length;
let classes = '';
if (isToday) classes += ' today';
if (isSelected) classes += ' selected';
if (hasTasks) classes += ' has-tasks';
if (fullComplete) classes += ' full-complete';
html += `<div class="calendar-day${classes}" onclick="selectDate('${dateKey}')">${d}</div>`;
}
grid.innerHTML = html;
modal.classList.add('active');
}
function closeCalendarModal() {
document.getElementById('calendarModal').classList.remove('active');
}
// Data export/import
function exportData() {
const blob = new Blob([JSON.stringify(appData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'planner-backup.json';
a.click();
URL.revokeObjectURL(url);
}
function importData(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
appData = JSON.parse(e.target.result);
saveData();
render();
alert('Данные успешно импортированы');
} catch (err) {
alert('Ошибка импорта: неверный формат файла');
}
};
reader.readAsText(file);
event.target.value = '';
}
// Close dropdowns on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('.task-menu') && !e.target.closest('.dropdown')) {
closeAllDropdowns();
}
});
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.classList.remove('active');
}
});
});
// Init
loadData();
render();
</script>
</body>
</html>