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:
parent
17059591f6
commit
ba7369eeaf
95
app.js
95
app.js
@ -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();
|
||||
});
|
||||
|
||||
20
index.html
20
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=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>
|
||||
|
||||
95
styles.css
95
styles.css
@ -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 ---------- */
|
||||
|
||||
Reference in New Issue
Block a user