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();
|
||||
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;
|
||||
|
||||
16
index.html
16
index.html
@ -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>
|
||||
|
||||
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; }
|
||||
|
||||
/* ---------- 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; }
|
||||
|
||||
Reference in New Issue
Block a user