Add calendar widget on home view

Mini month calendar below clock/weather. Highlights today,
shows dots on dates with active task deadlines. Click a day
to filter analytics table by that deadline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sultan 2026-05-22 20:08:38 +05:00
parent 17059591f6
commit ba7369eeaf
3 changed files with 206 additions and 4 deletions

95
app.js
View File

@ -167,7 +167,7 @@ function fillSelect(el, items, includeAll = false, allLabel = 'Все') {
const state = {
currentUser: localStorage.getItem(USER_KEY) || 'Директор',
filters: { assignee: '', status: '', direction: '', priority: '' },
filters: { assignee: '', status: '', direction: '', priority: '', deadline: '' },
view: 'home',
};
@ -213,6 +213,7 @@ function renderTable(tasks) {
if (f.direction) visible = visible.filter((t) => t.direction === f.direction);
if (f.priority) visible = visible.filter((t) => t.priority === f.priority);
if (f.status) visible = visible.filter((t) => effectiveStatus(t) === f.status);
if (f.deadline) visible = visible.filter((t) => t.deadline === f.deadline);
visible.sort((a, b) => {
const order = { high: 0, medium: 1, low: 2 };
@ -464,6 +465,96 @@ function startHomeWidgets() {
const now = Date.now();
if (now - weatherLoadedAt > 10 * 60 * 1000) loadWeather();
if (now - newsLoadedAt > 15 * 60 * 1000) loadNews();
renderCalendar();
bindCalendarNav();
}
let calCursor = null;
const CAL_MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
function renderCalendar() {
const grid = document.getElementById('calGrid');
const title = document.getElementById('calTitle');
if (!grid || !title) return;
const now = new Date();
if (!calCursor) calCursor = { year: now.getFullYear(), month: now.getMonth() };
const { year, month } = calCursor;
title.textContent = `${CAL_MONTHS[month]} ${year}`;
const startDow = (new Date(year, month, 1).getDay() + 6) % 7;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const daysInPrev = new Date(year, month, 0).getDate();
const today = todayISO();
const tasks = getTasks();
const dueByDate = {};
tasks.forEach(t => {
if (!t.deadline || t.status === 'done') return;
dueByDate[t.deadline] = (dueByDate[t.deadline] || 0) + 1;
});
const cells = [];
for (let i = startDow - 1; i >= 0; i--) {
cells.push({ day: daysInPrev - i, muted: true });
}
for (let d = 1; d <= daysInMonth; d++) {
const iso = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
const dow = (new Date(year, month, d).getDay() + 6) % 7;
cells.push({
day: d,
iso,
today: iso === today,
hasTask: !!dueByDate[iso],
count: dueByDate[iso] || 0,
weekend: dow >= 5,
});
}
while (cells.length % 7 !== 0) cells.push({ day: '', muted: true, empty: true });
grid.innerHTML = cells.map(c => {
if (c.empty) return '<div class="cal-day muted"></div>';
const classes = ['cal-day'];
if (c.muted) classes.push('muted');
if (c.today) classes.push('today');
if (c.weekend) classes.push('weekend');
const dot = c.hasTask ? '<span class="cal-dot" aria-hidden="true"></span>' : '';
const titleAttr = c.hasTask ? ` title="${c.count} зад."` : '';
const isoAttr = c.iso ? ` data-iso="${c.iso}"` : '';
return `<div class="${classes.join(' ')}"${isoAttr}${titleAttr}><span>${c.day}</span>${dot}</div>`;
}).join('');
grid.querySelectorAll('.cal-day[data-iso]').forEach(el => {
el.addEventListener('click', () => {
const iso = el.getAttribute('data-iso');
if (!iso) return;
state.filters = state.filters || {};
state.filters.deadline = iso;
location.hash = '#/analytics';
});
});
}
function bindCalendarNav() {
const prev = document.getElementById('calPrev');
const next = document.getElementById('calNext');
if (!prev || !next) return;
if (prev.dataset.bound === '1') return;
prev.dataset.bound = '1';
next.dataset.bound = '1';
prev.addEventListener('click', () => {
if (!calCursor) return;
if (calCursor.month === 0) { calCursor.month = 11; calCursor.year--; }
else calCursor.month--;
renderCalendar();
});
next.addEventListener('click', () => {
if (!calCursor) return;
if (calCursor.month === 11) { calCursor.month = 0; calCursor.year++; }
else calCursor.month++;
renderCalendar();
});
}
function tickClock() {
@ -771,7 +862,7 @@ function init() {
});
});
$('#clearFilters').addEventListener('click', () => {
state.filters = { assignee: '', status: '', direction: '', priority: '' };
state.filters = { assignee: '', status: '', direction: '', priority: '', deadline: '' };
['fAssignee', 'fStatus', 'fDirection', 'fPriority'].forEach((id) => { $('#' + id).value = ''; });
renderAll();
});

View File

@ -11,7 +11,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
<link rel="stylesheet" href="styles.css?v=27" />
<link rel="stylesheet" href="styles.css?v=28" />
</head>
<body>
<header class="topbar">
@ -80,6 +80,22 @@
</section>
</div>
<section class="card home-calendar">
<div class="cal-head">
<h2>Календарь</h2>
<div class="cal-nav">
<button class="cal-btn" id="calPrev" type="button" aria-label="Предыдущий месяц">‹</button>
<span class="cal-title" id="calTitle">—</span>
<button class="cal-btn" id="calNext" type="button" aria-label="Следующий месяц">›</button>
</div>
</div>
<div class="cal-weekdays" aria-hidden="true">
<span>Пн</span><span>Вт</span><span>Ср</span><span>Чт</span><span>Пт</span><span>Сб</span><span>Вс</span>
</div>
<div class="cal-grid" id="calGrid"></div>
<p class="hint cal-hint">Точки — задачи с дедлайнами. Нажмите день, чтобы перейти в аналитику.</p>
</section>
<section class="card">
<div class="news-header">
<h2>Новости отрасли</h2>
@ -203,6 +219,6 @@
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/l10n/ru.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="app.js?v=24"></script>
<script src="app.js?v=25"></script>
</body>
</html>

View File

@ -831,12 +831,107 @@ input.flatpickr-input {
.news-empty-title { font-size: 19px; font-weight: 500; color: var(--ink); margin-bottom: 10px; letter-spacing: -0.01em; }
.news-empty-sub { font-size: 15px; color: var(--muted); line-height: 1.5; max-width: 480px; margin: 0 auto; }
/* ---------- Calendar widget ---------- */
.home-calendar { }
.cal-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 12px;
flex-wrap: wrap;
}
.cal-head h2 { margin: 0; }
.cal-nav { display: flex; align-items: center; gap: 10px; }
.cal-btn {
width: 34px; height: 34px;
border: none;
background: var(--surface-2);
border-radius: 50%;
font-size: 18px;
line-height: 1;
color: var(--ink);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.cal-btn:hover { background: rgba(0, 113, 227, 0.10); color: var(--kt-blue); }
.cal-btn:focus-visible { outline: 2px solid var(--kt-blue); outline-offset: 2px; }
.cal-title {
font-size: 16px;
font-weight: 500;
min-width: 170px;
text-align: center;
letter-spacing: -0.01em;
color: var(--ink);
}
.cal-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 8px;
}
.cal-weekdays span {
font-size: 11px;
font-weight: 600;
color: var(--muted);
text-align: center;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 6px 0;
}
.cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.cal-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--r-sm);
font-size: 14px;
font-weight: 500;
color: var(--ink);
cursor: pointer;
position: relative;
transition: background 0.15s ease, color 0.15s ease;
user-select: none;
}
.cal-day:hover { background: var(--surface-2); }
.cal-day.muted { color: var(--muted); opacity: 0.35; cursor: default; }
.cal-day.muted:hover { background: transparent; }
.cal-day.weekend { color: var(--ink-2); }
.cal-day.today {
background: var(--kt-blue);
color: #fff;
}
.cal-day.today:hover { background: var(--kt-blue); }
.cal-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: #FF453A;
position: absolute;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 0 0 2px var(--surface);
}
.cal-day.today .cal-dot { background: #fff; box-shadow: 0 0 0 2px var(--kt-blue); }
.cal-hint { margin-top: 18px; margin-bottom: 0; }
@media (max-width: 900px) {
.home-grid { grid-template-columns: 1fr; }
.clock-time { font-size: 64px; }
.weather-temp { font-size: 64px; }
.weather-icon { font-size: 56px; }
.home-clock, .home-weather { padding: 32px 24px; min-height: 200px; }
.cal-title { min-width: 0; flex: 1; font-size: 15px; }
.cal-day { font-size: 13px; }
}
/* ---------- Toast ---------- */