981 lines
26 KiB
HTML
981 lines
26 KiB
HTML
<!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: #D4A574;
|
||
--accent3: #C08B65;
|
||
--accent4: #A67B5B;
|
||
--accent5: #8B6F4E;
|
||
--accent6: #F7EDE2;
|
||
--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: 1400px;
|
||
margin: 0 auto;
|
||
display: grid;
|
||
grid-template-columns: 320px 1fr 280px;
|
||
gap: 24px;
|
||
}
|
||
|
||
/* Header */
|
||
.header {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
padding-bottom: 20px;
|
||
border-bottom: 1px solid var(--bg-border);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 42px;
|
||
color: var(--accent1);
|
||
font-style: italic;
|
||
}
|
||
|
||
.header-date {
|
||
color: var(--text-secondary);
|
||
font-size: 16px;
|
||
margin-top: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.header-date:hover { color: var(--accent1); }
|
||
|
||
.month-selector {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.month-btn {
|
||
background: transparent;
|
||
border: 1px solid var(--bg-border);
|
||
color: var(--text-secondary);
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s ease-out;
|
||
}
|
||
|
||
.month-btn:hover {
|
||
background: var(--bg-hover);
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.month-btn.active {
|
||
background: var(--accent1);
|
||
border-color: var(--accent1);
|
||
color: var(--bg-main);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Cards */
|
||
.card {
|
||
background: var(--bg-card);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 18px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 16px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* Schedule Section */
|
||
.schedule-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.schedule-title {
|
||
font-size: 16px;
|
||
color: var(--accent1);
|
||
margin-bottom: 12px;
|
||
font-style: italic;
|
||
}
|
||
|
||
.task-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.task-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
margin-bottom: 6px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.task-item:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
|
||
.task-text {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-main);
|
||
padding: 4px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.task-text:focus {
|
||
outline: none;
|
||
background: rgba(255,255,255,0.05);
|
||
}
|
||
|
||
.task-delete {
|
||
opacity: 0;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
padding: 0 4px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.task-item:hover .task-delete { opacity: 1; }
|
||
.task-delete:hover { color: var(--accent3); }
|
||
|
||
.add-task-btn {
|
||
width: 100%;
|
||
padding: 8px;
|
||
background: transparent;
|
||
border: 1px dashed var(--bg-border);
|
||
color: var(--text-secondary);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.add-task-btn:hover {
|
||
border-color: var(--accent1);
|
||
color: var(--accent1);
|
||
}
|
||
|
||
/* Weeks Grid */
|
||
.weeks-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.week-block {
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.week-header {
|
||
padding: 12px 16px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1.5px;
|
||
}
|
||
|
||
.week-1 .week-header { background: var(--accent1); color: var(--bg-main); }
|
||
.week-2 .week-header { background: var(--accent3); color: var(--bg-main); }
|
||
.week-3 .week-header { background: var(--accent5); color: var(--text-main); }
|
||
.week-4 .week-header { background: var(--accent2); color: var(--bg-main); }
|
||
|
||
.week-days {
|
||
display: grid;
|
||
grid-template-columns: 1.5fr repeat(7, 1fr);
|
||
background: var(--bg-card);
|
||
border-bottom: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.week-days div {
|
||
padding: 10px 6px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
border-right: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.week-days div:first-child {
|
||
text-align: left;
|
||
padding-left: 12px;
|
||
border-left: none;
|
||
}
|
||
|
||
.checkbox-grid {
|
||
background: var(--bg-card);
|
||
}
|
||
|
||
.checkbox-row {
|
||
display: grid;
|
||
grid-template-columns: 1.5fr repeat(7, 1fr);
|
||
align-items: center;
|
||
}
|
||
|
||
.checkbox-row:not(:last-child) {
|
||
border-bottom: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.checkbox-row .task-name {
|
||
padding: 12px;
|
||
font-size: 13px;
|
||
color: var(--text-main);
|
||
border-right: 1px solid var(--bg-border);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.checkbox-cell {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-right: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.custom-checkbox {
|
||
width: 22px;
|
||
height: 22px;
|
||
border: 2px solid var(--bg-border);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease-out;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
}
|
||
|
||
.custom-checkbox:hover {
|
||
transform: scale(1.1);
|
||
border-color: var(--accent1);
|
||
}
|
||
|
||
.custom-checkbox.checked {
|
||
background: var(--accent1);
|
||
border-color: var(--accent1);
|
||
}
|
||
|
||
.custom-checkbox.checked::after {
|
||
content: '✓';
|
||
color: var(--bg-main);
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.week-2 .custom-checkbox.checked { background: var(--accent3); border-color: var(--accent3); }
|
||
.week-3 .custom-checkbox.checked { background: var(--accent5); border-color: var(--accent5); }
|
||
.week-4 .custom-checkbox.checked { background: var(--accent2); border-color: var(--accent2); }
|
||
|
||
/* Bar Chart */
|
||
.chart-block {
|
||
margin-top: 16px;
|
||
padding: 16px;
|
||
background: rgba(0,0,0,0.2);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.bar-chart {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
height: 100px;
|
||
gap: 8px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.bar-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.bar {
|
||
width: 100%;
|
||
max-width: 40px;
|
||
border-radius: 4px 4px 0 0;
|
||
transition: height 0.3s ease-out;
|
||
min-height: 4px;
|
||
}
|
||
|
||
.bar-value {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.bar-label {
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.week-progress {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.progress-bar-bg {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: var(--bg-border);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar-fill {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.3s ease-out;
|
||
}
|
||
|
||
.week-1 .progress-bar-fill { background: var(--accent1); }
|
||
.week-2 .progress-bar-fill { background: var(--accent3); }
|
||
.week-3 .progress-bar-fill { background: var(--accent5); }
|
||
.week-4 .progress-bar-fill { background: var(--accent2); }
|
||
|
||
.progress-text {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
min-width: 60px;
|
||
text-align: right;
|
||
}
|
||
|
||
/* Progress Section */
|
||
.progress-title {
|
||
font-size: 16px;
|
||
color: var(--accent1);
|
||
margin-bottom: 16px;
|
||
font-style: italic;
|
||
}
|
||
|
||
.progress-count {
|
||
font-size: 28px;
|
||
color: var(--text-main);
|
||
font-weight: 600;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.task-progress-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.task-progress-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.task-progress-name {
|
||
width: 140px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.task-progress-bar-bg {
|
||
flex: 1;
|
||
height: 6px;
|
||
background: var(--bg-border);
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.task-progress-bar-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--accent3), var(--accent1));
|
||
border-radius: 3px;
|
||
transition: width 0.3s ease-out;
|
||
}
|
||
|
||
.task-progress-percent {
|
||
width: 35px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
text-align: right;
|
||
}
|
||
|
||
.donut-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.donut-chart {
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 50%;
|
||
background: conic-gradient(var(--accent1) 0deg 0deg, var(--bg-border) 0deg 360deg);
|
||
position: relative;
|
||
transition: background 0.3s ease-out;
|
||
}
|
||
|
||
.donut-inner {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 80px;
|
||
height: 80px;
|
||
background: var(--bg-card);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--accent1);
|
||
}
|
||
|
||
.donut-legend {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.legend-color {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* Overall Progress */
|
||
.overall-progress {
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid var(--bg-border);
|
||
}
|
||
|
||
.overall-title {
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.overall-count {
|
||
font-size: 36px;
|
||
color: var(--accent1);
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.overall-percent {
|
||
font-size: 18px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1200px) {
|
||
.container {
|
||
grid-template-columns: 280px 1fr;
|
||
}
|
||
.progress-section {
|
||
grid-column: 1 / -1;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.container {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.schedule-section, .progress-section {
|
||
grid-column: 1;
|
||
}
|
||
.weeks-container {
|
||
overflow-x: auto;
|
||
}
|
||
.week-days, .checkbox-row {
|
||
min-width: 600px;
|
||
}
|
||
.progress-section {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
body { padding: 12px; }
|
||
.header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
}
|
||
.header h1 { font-size: 28px; }
|
||
.month-selector {
|
||
justify-content: flex-start;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<div>
|
||
<h1>Планер</h1>
|
||
<div class="header-date" id="currentDate" title="Нажмите для редактирования">15 декабря 2025</div>
|
||
</div>
|
||
<div class="month-selector" id="monthSelector"></div>
|
||
</header>
|
||
|
||
<!-- Left: Schedule -->
|
||
<div class="schedule-section card">
|
||
<h2 class="card-title">Расписание дня</h2>
|
||
|
||
<div class="schedule-block" data-period="morning">
|
||
<h3 class="schedule-title">☀️ Утро</h3>
|
||
<ul class="task-list" id="morningTasks"></ul>
|
||
<button class="add-task-btn" onclick="addTask('morning')">+ Добавить задачу</button>
|
||
</div>
|
||
|
||
<div class="schedule-block" data-period="day" style="margin-top: 20px;">
|
||
<h3 class="schedule-title">🌤 День</h3>
|
||
<ul class="task-list" id="dayTasks"></ul>
|
||
<button class="add-task-btn" onclick="addTask('day')">+ Добавить задачу</button>
|
||
</div>
|
||
|
||
<div class="schedule-block" data-period="evening" style="margin-top: 20px;">
|
||
<h3 class="schedule-title">🌙 Вечер</h3>
|
||
<ul class="task-list" id="eveningTasks"></ul>
|
||
<button class="add-task-btn" onclick="addTask('evening')">+ Добавить задачу</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Center: Weeks Grid -->
|
||
<div class="weeks-container card" id="weeksContainer"></div>
|
||
|
||
<!-- Right: Progress -->
|
||
<div class="progress-section">
|
||
<div class="card">
|
||
<h2 class="progress-title">Прогресс за месяц</h2>
|
||
<div class="progress-count" id="monthCount">0 / 0</div>
|
||
|
||
<div class="task-progress-list" id="taskProgressList"></div>
|
||
|
||
<div class="donut-container">
|
||
<div class="donut-chart" id="donutChart">
|
||
<div class="donut-inner" id="donutInner">0%</div>
|
||
</div>
|
||
<div class="donut-legend">
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: var(--accent1)"></div>
|
||
<span>Выполнено</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: var(--bg-border)"></div>
|
||
<span>Осталось</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overall-progress">
|
||
<h3 class="overall-title">Общий прогресс</h3>
|
||
<div class="overall-count" id="overallCount">0 / 0</div>
|
||
<div class="overall-percent" id="overallPercent">0%</div>
|
||
<div class="overall-bar-bg">
|
||
<div class="overall-bar-fill" id="overallBar" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const MONTHS = [
|
||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
|
||
];
|
||
|
||
const WEEK_COLORS = ['var(--accent1)', 'var(--accent3)', 'var(--accent5)', 'var(--accent2)'];
|
||
|
||
let currentMonth = new Date().getMonth();
|
||
let currentYear = new Date().getFullYear();
|
||
let currentDate = new Date();
|
||
|
||
let data = {
|
||
morning: [],
|
||
day: [],
|
||
evening: []
|
||
};
|
||
|
||
let checkboxes = {};
|
||
|
||
function getStorageKey(month, year) {
|
||
return `habit-tracker-${year}-${String(month).padStart(2, '0')}`;
|
||
}
|
||
|
||
function loadAllData() {
|
||
const today = new Date();
|
||
currentDate = today;
|
||
|
||
for (let m = 0; m < 12; m++) {
|
||
const key = getStorageKey(m, currentYear);
|
||
const saved = localStorage.getItem(key);
|
||
if (saved) {
|
||
const parsed = JSON.parse(saved);
|
||
if (parsed.data) data = parsed.data;
|
||
if (parsed.checkboxes) checkboxes = parsed.checkboxes;
|
||
}
|
||
}
|
||
|
||
if (Object.keys(data).length === 0 || (!data.morning.length && !data.day.length && !data.evening.length)) {
|
||
data = {
|
||
morning: [],
|
||
day: [],
|
||
evening: []
|
||
};
|
||
}
|
||
}
|
||
|
||
function saveCurrentMonth() {
|
||
const key = getStorageKey(currentMonth, currentYear);
|
||
localStorage.setItem(key, JSON.stringify({ data, checkboxes }));
|
||
}
|
||
|
||
function getDaysInMonth(month, year) {
|
||
return new Date(year, month + 1, 0).getDate();
|
||
}
|
||
|
||
function formatDate(date) {
|
||
const options = { day: 'numeric', month: 'long', year: 'numeric' };
|
||
return date.toLocaleDateString('ru-RU', options);
|
||
}
|
||
|
||
function updateDateDisplay() {
|
||
document.getElementById('currentDate').textContent = formatDate(currentDate);
|
||
}
|
||
|
||
function renderMonthSelector() {
|
||
const container = document.getElementById('monthSelector');
|
||
container.innerHTML = MONTHS.map((month, i) => `
|
||
<button class="month-btn ${i === currentMonth ? 'active' : ''}" onclick="switchMonth(${i})">
|
||
${month.slice(0, 3)}
|
||
</button>
|
||
`).join('');
|
||
}
|
||
|
||
function switchMonth(month) {
|
||
currentMonth = month;
|
||
loadAllData();
|
||
renderAll();
|
||
}
|
||
|
||
function renderSchedule() {
|
||
['morning', 'day', 'evening'].forEach(period => {
|
||
const container = document.getElementById(`${period}Tasks`);
|
||
container.innerHTML = data[period].map((task, i) => `
|
||
<li class="task-item">
|
||
<input type="text" class="task-text" value="${task}"
|
||
onchange="updateTask('${period}', ${i}, this.value)">
|
||
<button class="task-delete" onclick="deleteTask('${period}', ${i})">×</button>
|
||
</li>
|
||
`).join('');
|
||
});
|
||
}
|
||
|
||
function updateTask(period, index, value) {
|
||
data[period][index] = value;
|
||
saveCurrentMonth();
|
||
renderWeeks();
|
||
renderProgress();
|
||
}
|
||
|
||
function deleteTask(period, index) {
|
||
if (confirm('Удалить эту задачу?')) {
|
||
data[period].splice(index, 1);
|
||
saveCurrentMonth();
|
||
renderSchedule();
|
||
renderWeeks();
|
||
renderProgress();
|
||
}
|
||
}
|
||
|
||
function addTask(period) {
|
||
const taskName = prompt('Название задачи:');
|
||
if (taskName && taskName.trim()) {
|
||
data[period].push(taskName.trim());
|
||
saveCurrentMonth();
|
||
renderSchedule();
|
||
renderWeeks();
|
||
renderProgress();
|
||
}
|
||
}
|
||
|
||
function getAllTasks() {
|
||
return [...data.morning, ...data.day, ...data.evening];
|
||
}
|
||
|
||
function renderWeeks() {
|
||
const container = document.getElementById('weeksContainer');
|
||
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
|
||
const tasks = getAllTasks();
|
||
|
||
let html = '';
|
||
const weeksCount = Math.ceil(daysInMonth / 7);
|
||
|
||
for (let w = 0; w < Math.min(weeksCount, 4); w++) {
|
||
const weekStart = w * 7 + 1;
|
||
const weekEnd = Math.min((w + 1) * 7, daysInMonth);
|
||
|
||
html += `<div class="week-block week-${w + 1}">`;
|
||
html += `<div class="week-header">Неделя ${w + 1}</div>`;
|
||
|
||
html += '<div class="week-days">';
|
||
html += '<div>Задача</div>';
|
||
|
||
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||
for (let d = 0; d < 7; d++) {
|
||
const dayNum = weekStart + d;
|
||
if (dayNum <= daysInMonth) {
|
||
html += `<div>${dayNames[d]} ${dayNum}</div>`;
|
||
} else {
|
||
html += '<div></div>';
|
||
}
|
||
}
|
||
html += '</div>';
|
||
|
||
html += '<div class="checkbox-grid">';
|
||
tasks.forEach((task, taskIdx) => {
|
||
html += '<div class="checkbox-row">';
|
||
html += `<div class="task-name">${task}</div>`;
|
||
|
||
for (let d = 0; d < 7; d++) {
|
||
const dayNum = weekStart + d;
|
||
if (dayNum <= daysInMonth) {
|
||
const dayIndex = (w * 7 + d);
|
||
const checkboxKey = `${taskIdx}-${dayIndex}`;
|
||
const isChecked = checkboxes[checkboxKey] || false;
|
||
html += `
|
||
<div class="checkbox-cell">
|
||
<div class="custom-checkbox ${isChecked ? 'checked' : ''}"
|
||
onclick="toggleCheckbox('${checkboxKey}')"></div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
html += '<div class="checkbox-cell"></div>';
|
||
}
|
||
}
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
|
||
html += renderChart(w, tasks.length, daysInMonth - weekStart + 1);
|
||
html += '</div>';
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderChart(weekIndex, tasksPerDay, daysToShow) {
|
||
const weekStart = weekIndex * 7;
|
||
const chartData = [];
|
||
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||
|
||
for (let d = 0; d < 7; d++) {
|
||
const dayIndex = weekStart + d;
|
||
let count = 0;
|
||
for (let t = 0; t < tasksPerDay; t++) {
|
||
if (checkboxes[`${t}-${dayIndex}`]) count++;
|
||
}
|
||
chartData.push(count);
|
||
}
|
||
|
||
const maxVal = Math.max(...chartData, 1);
|
||
const total = chartData.reduce((a, b) => a + b, 0);
|
||
const maxPossible = tasksPerDay * 7;
|
||
const percent = maxPossible > 0 ? Math.round((total / maxPossible) * 100) : 0;
|
||
|
||
let html = '<div class="chart-block">';
|
||
html += '<div class="bar-chart">';
|
||
chartData.forEach((val, i) => {
|
||
const height = (val / maxVal) * 80 + 4;
|
||
html += `
|
||
<div class="bar-container">
|
||
<div class="bar-value">${val}</div>
|
||
<div class="bar" style="height: ${height}px; background: ${WEEK_COLORS[weekIndex]}"></div>
|
||
<div class="bar-label">${dayNames[i]}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
|
||
html += `
|
||
<div class="week-progress">
|
||
<div class="progress-bar-bg">
|
||
<div class="progress-bar-fill" style="width: ${percent}%"></div>
|
||
</div>
|
||
<div class="progress-text">${total} / ${maxPossible} • ${percent}%</div>
|
||
</div>
|
||
`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function toggleCheckbox(key) {
|
||
checkboxes[key] = !checkboxes[key];
|
||
saveCurrentMonth();
|
||
renderWeeks();
|
||
renderProgress();
|
||
}
|
||
|
||
function renderProgress() {
|
||
const tasks = getAllTasks();
|
||
const totalSlots = tasks.length * getDaysInMonth(currentMonth, currentYear);
|
||
let totalChecked = 0;
|
||
|
||
const taskProgressHTML = tasks.map((task, taskIdx) => {
|
||
let checked = 0;
|
||
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
|
||
for (let d = 0; d < daysInMonth; d++) {
|
||
if (checkboxes[`${taskIdx}-${d}`]) checked++;
|
||
}
|
||
totalChecked += checked;
|
||
const percent = daysInMonth > 0 ? Math.round((checked / daysInMonth) * 100) : 0;
|
||
|
||
return `
|
||
<div class="task-progress-item">
|
||
<div class="task-progress-name">${task}</div>
|
||
<div class="task-progress-bar-bg">
|
||
<div class="task-progress-bar-fill" style="width: ${percent}%"></div>
|
||
</div>
|
||
<div class="task-progress-percent">${percent}%</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
document.getElementById('taskProgressList').innerHTML = taskProgressHTML;
|
||
document.getElementById('monthCount').textContent = `${totalChecked} / ${totalSlots}`;
|
||
|
||
const monthPercent = totalSlots > 0 ? Math.round((totalChecked / totalSlots) * 100) : 0;
|
||
const donut = document.getElementById('donutChart');
|
||
const donutInner = document.getElementById('donutInner');
|
||
const degrees = (monthPercent / 100) * 360;
|
||
donut.style.background = `conic-gradient(var(--accent1) 0deg ${degrees}deg, var(--bg-border) ${degrees}deg 360deg)`;
|
||
donutInner.textContent = `${monthPercent}%`;
|
||
|
||
document.getElementById('overallCount').textContent = `${totalChecked} / ${totalSlots}`;
|
||
document.getElementById('overallPercent').textContent = `${monthPercent}%`;
|
||
document.getElementById('overallBar').style.width = `${monthPercent}%`;
|
||
}
|
||
|
||
function renderAll() {
|
||
updateDateDisplay();
|
||
renderMonthSelector();
|
||
renderSchedule();
|
||
renderWeeks();
|
||
renderProgress();
|
||
}
|
||
|
||
document.getElementById('currentDate').addEventListener('click', () => {
|
||
const input = document.createElement('input');
|
||
input.type = 'date';
|
||
input.value = currentDate.toISOString().split('T')[0];
|
||
input.style.background = 'var(--bg-hover)';
|
||
input.style.border = '1px solid var(--bg-border)';
|
||
input.style.color = 'var(--text-main)';
|
||
input.style.padding = '4px 8px';
|
||
input.style.borderRadius = '4px';
|
||
input.style.fontFamily = 'inherit';
|
||
|
||
input.onchange = () => {
|
||
currentDate = new Date(input.value + 'T12:00:00');
|
||
saveCurrentMonth();
|
||
updateDateDisplay();
|
||
};
|
||
|
||
const dateEl = document.getElementById('currentDate');
|
||
dateEl.innerHTML = '';
|
||
dateEl.appendChild(input);
|
||
input.focus();
|
||
});
|
||
|
||
loadAllData();
|
||
renderAll();
|
||
</script>
|
||
</body>
|
||
</html>
|