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:
parent
cc05070714
commit
d6102f354a
50
app.js
50
app.js
@ -468,6 +468,56 @@ function startHomeWidgets() {
|
|||||||
|
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
bindCalendarNav();
|
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;
|
let calCursor = null;
|
||||||
|
|||||||
44
index.html
44
index.html
@ -11,7 +11,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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="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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@ -80,21 +80,33 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="card home-calendar">
|
<div class="home-row2">
|
||||||
<div class="cal-head">
|
<section class="card home-calendar">
|
||||||
<h2>Календарь</h2>
|
<div class="cal-head">
|
||||||
<div class="cal-nav">
|
<h2>Календарь</h2>
|
||||||
<button class="cal-btn" id="calPrev" type="button" aria-label="Предыдущий месяц">‹</button>
|
<div class="cal-nav">
|
||||||
<span class="cal-title" id="calTitle">—</span>
|
<button class="cal-btn" id="calPrev" type="button" aria-label="Предыдущий месяц">‹</button>
|
||||||
<button class="cal-btn" id="calNext" 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>
|
||||||
</div>
|
<div class="cal-weekdays" aria-hidden="true">
|
||||||
<div class="cal-weekdays" aria-hidden="true">
|
<span>Пн</span><span>Вт</span><span>Ср</span><span>Чт</span><span>Пт</span><span>Сб</span><span>Вс</span>
|
||||||
<span>Пн</span><span>Вт</span><span>Ср</span><span>Чт</span><span>Пт</span><span>Сб</span><span>Вс</span>
|
</div>
|
||||||
</div>
|
<div class="cal-grid" id="calGrid"></div>
|
||||||
<div class="cal-grid" id="calGrid"></div>
|
<p class="hint cal-hint">Точки — задачи с дедлайнами. Нажмите день, чтобы перейти в аналитику.</p>
|
||||||
<p class="hint cal-hint">Точки — задачи с дедлайнами. Нажмите день, чтобы перейти в аналитику.</p>
|
</section>
|
||||||
</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">
|
<section class="card">
|
||||||
<div class="news-header">
|
<div class="news-header">
|
||||||
@ -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/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/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="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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
98
styles.css
98
styles.css
@ -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; }
|
.news-empty-sub { font-size: 15px; color: var(--muted); line-height: 1.5; max-width: 480px; margin: 0 auto; }
|
||||||
|
|
||||||
/* ---------- Calendar widget ---------- */
|
/* ---------- Calendar widget ---------- */
|
||||||
|
.home-row2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
.home-calendar {
|
.home-calendar {
|
||||||
max-width: 50%;
|
|
||||||
padding: 24px 28px;
|
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 {
|
.cal-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -928,7 +1022,7 @@ input.flatpickr-input {
|
|||||||
.cal-hint { margin-top: 18px; margin-bottom: 0; }
|
.cal-hint { margin-top: 18px; margin-bottom: 0; }
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.home-calendar { max-width: 100%; }
|
.home-row2 { grid-template-columns: 1fr; }
|
||||||
.home-grid { grid-template-columns: 1fr; }
|
.home-grid { grid-template-columns: 1fr; }
|
||||||
.clock-time { font-size: 64px; }
|
.clock-time { font-size: 64px; }
|
||||||
.weather-temp { font-size: 64px; }
|
.weather-temp { font-size: 64px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user