Add upcoming tasks widget right of calendar

Two-column row: calendar | upcoming tasks (top 6 by deadline).
Color-coded by overdue / due soon / normal. Priority dot on right.
Click an item to filter analytics 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:16:52 +05:00
parent cc05070714
commit d6102f354a
3 changed files with 174 additions and 18 deletions

50
app.js
View File

@ -468,6 +468,56 @@ function startHomeWidgets() {
renderCalendar();
bindCalendarNav();
renderUpcoming();
}
const RU_MON_SHORT = ['янв','фев','мар','апр','май','июн','июл','авг','сен','окт','ноя','дек'];
function renderUpcoming() {
const list = document.getElementById('upcomingList');
if (!list) return;
const today = todayISO();
const tasks = getTasks()
.filter(t => t.deadline && effectiveStatus(t) !== 'done')
.sort((a, b) => a.deadline.localeCompare(b.deadline))
.slice(0, 6);
if (!tasks.length) {
list.innerHTML = '<li class="upcoming-empty">Нет активных задач с дедлайнами.</li>';
return;
}
list.innerHTML = tasks.map(t => {
const [y, m, d] = t.deadline.split('-').map(Number);
const day = String(d).padStart(2, '0');
const mon = RU_MON_SHORT[m - 1];
const diff = daysBetween(t.deadline, today);
let cls = 'upcoming-item';
if (t.deadline < today) cls += ' is-overdue';
else if (diff <= 3) cls += ' is-soon';
return `
<li class="${cls}" data-id="${t.id}" data-iso="${t.deadline}">
<div class="upcoming-date">
<span class="ud-day">${day}</span>
<span class="ud-mon">${mon}</span>
</div>
<div class="upcoming-body">
<div class="upcoming-title">${escapeHtml(t.title)}</div>
<div class="upcoming-meta">${escapeHtml(t.assignee || '—')}</div>
</div>
<span class="upcoming-prio ${t.priority}" title="Приоритет: ${t.priority}"></span>
</li>
`;
}).join('');
list.querySelectorAll('.upcoming-item').forEach(el => {
el.addEventListener('click', () => {
const iso = el.getAttribute('data-iso');
state.filters = state.filters || {};
state.filters.deadline = iso;
location.hash = '#/analytics';
});
});
}
let calCursor = null;

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=29" />
<link rel="stylesheet" href="styles.css?v=30" />
</head>
<body>
<header class="topbar">
@ -80,6 +80,7 @@
</section>
</div>
<div class="home-row2">
<section class="card home-calendar">
<div class="cal-head">
<h2>Календарь</h2>
@ -96,6 +97,17 @@
<p class="hint cal-hint">Точки — задачи с дедлайнами. Нажмите день, чтобы перейти в аналитику.</p>
</section>
<section class="card home-tasks">
<div class="tasks-head">
<h2>Ближайшие задачи</h2>
<a href="#/analytics" class="tasks-all">Все →</a>
</div>
<ul id="upcomingList" class="upcoming-list">
<li class="upcoming-empty">Загружаем…</li>
</ul>
</section>
</div>
<section class="card">
<div class="news-header">
<h2>Новости отрасли</h2>
@ -219,6 +231,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=25"></script>
<script src="app.js?v=26"></script>
</body>
</html>

View File

@ -832,10 +832,104 @@ input.flatpickr-input {
.news-empty-sub { font-size: 15px; color: var(--muted); line-height: 1.5; max-width: 480px; margin: 0 auto; }
/* ---------- Calendar widget ---------- */
.home-row2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 28px;
}
.home-calendar {
max-width: 50%;
padding: 24px 28px;
}
.home-tasks {
padding: 24px 28px;
display: flex;
flex-direction: column;
}
.tasks-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
gap: 12px;
}
.tasks-head h2 { margin: 0; font-size: 18px; }
.tasks-all {
font-size: 13px;
font-weight: 500;
color: var(--kt-blue);
text-decoration: none;
}
.tasks-all:hover { text-decoration: underline; }
.upcoming-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.upcoming-empty {
padding: 20px 0;
color: var(--muted);
font-size: 14px;
text-align: center;
}
.upcoming-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 14px;
align-items: center;
padding: 14px 0;
border-top: 1px solid var(--hairline);
cursor: pointer;
transition: color 0.15s ease;
}
.upcoming-item:first-child { border-top: none; }
.upcoming-item:hover { color: var(--kt-blue); }
.upcoming-date {
font-size: 12px;
font-weight: 600;
color: var(--muted);
font-variant-numeric: tabular-nums;
min-width: 44px;
text-align: center;
padding: 6px 8px;
background: var(--surface-2);
border-radius: var(--r-sm);
line-height: 1.15;
display: flex;
flex-direction: column;
}
.upcoming-date .ud-day { font-size: 16px; color: var(--ink); font-weight: 600; }
.upcoming-date .ud-mon { text-transform: uppercase; letter-spacing: 0.04em; }
.upcoming-item.is-overdue .upcoming-date { background: rgba(255, 69, 58, 0.12); color: #FF453A; }
.upcoming-item.is-overdue .ud-day { color: #FF453A; }
.upcoming-item.is-soon .upcoming-date { background: rgba(0, 113, 227, 0.10); color: var(--kt-blue); }
.upcoming-item.is-soon .ud-day { color: var(--kt-blue); }
.upcoming-body { min-width: 0; }
.upcoming-title {
font-size: 14px;
font-weight: 500;
color: var(--ink);
letter-spacing: -0.005em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.upcoming-meta {
font-size: 12px;
color: var(--muted);
margin-top: 2px;
}
.upcoming-prio {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
flex-shrink: 0;
}
.upcoming-prio.high { background: #FF453A; }
.upcoming-prio.medium { background: #FF9F0A; }
.upcoming-prio.low { background: #30D158; }
.cal-head {
display: flex;
justify-content: space-between;
@ -928,7 +1022,7 @@ input.flatpickr-input {
.cal-hint { margin-top: 18px; margin-bottom: 0; }
@media (max-width: 900px) {
.home-calendar { max-width: 100%; }
.home-row2 { grid-template-columns: 1fr; }
.home-grid { grid-template-columns: 1fr; }
.clock-time { font-size: 64px; }
.weather-temp { font-size: 64px; }