fix(dashboard): flatten header; replace greeting with date+time; split overdue/soon chips

Header shows current date and time instead of user name + separate date line.
urgentCount replaced by overdueCount (overdue tasks) and dueSoonCount (due today/soon),
each with a distinct chip color. formatDueDate updated to accept due_time and return
accurate overdue/soon states against the current moment.
dashboard grid expands to 4 columns at 1280px instead of 1440px.
This commit is contained in:
Konrad M.
2026-04-21 21:57:26 +02:00
parent 8f36c359aa
commit eede4a9708
2 changed files with 83 additions and 28 deletions
+59 -20
View File
@@ -66,20 +66,44 @@ function formatDateTime(isoString) {
return `${dateStr}, ${timeStr}${suffix ? ' ' + suffix : ''}`.trim(); return `${dateStr}, ${timeStr}${suffix ? ' ' + suffix : ''}`.trim();
} }
function formatDueDate(dateStr) { function formatDueDate(dateStr, timeStr) {
if (!dateStr) return null; if (!dateStr) return null;
const due = new Date(dateStr);
const dueDate = timeStr
? new Date(`${dateStr}T${timeStr}`)
: new Date(`${dateStr}T23:59:59`);
if (isNaN(dueDate)) return null;
const now = new Date(); const now = new Date();
const diffMs = due - now; const diffMs = dueDate - now;
const diffH = diffMs / (1000 * 60 * 60); const diffH = diffMs / (1000 * 60 * 60);
if (diffMs < 0) return { text: t('dashboard.overdue'), overdue: true }; const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
if (diffH < 24) return { text: t('dashboard.dueSoon'), overdue: false }; const dueDay = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate());
if (diffH < 48) return { text: t('dashboard.dueTomorrow'), overdue: false }; const calDayDiff = Math.round((dueDay - today) / (1000 * 60 * 60 * 24));
return {
text: formatDate(due), const fullLabel = timeStr
overdue: false, ? `${formatDate(dueDate)}, ${formatTime(dueDate)}` // beide aus i18n.js
}; : formatDate(dueDate);
if (diffMs < 0) {
return { text: `${t('dashboard.overdue')} ${fullLabel}`, overdue: true };
}
if (calDayDiff === 1 && dueDate.getHours() >= 22 && diffH < 24) {
return { text: `${t('dashboard.dueSoon')} ${fullLabel}`, overdue: false, soon: true };
}
if (calDayDiff === 0) {
return { text: `${t('dashboard.dueToday')} ${formatTime(dueDate)}`, overdue: false, soon: true };
}
if (calDayDiff === 1) {
return { text: `${t('dashboard.dueTomorrow')} ${formatTime(dueDate)}`, overdue: false };
}
return { text: fullLabel, overdue: false };
} }
const PRIORITY_LABELS = () => ({ const PRIORITY_LABELS = () => ({
@@ -147,13 +171,18 @@ function skeletonWidget(lines = 3) {
// -------------------------------------------------------- // --------------------------------------------------------
function renderGreeting(user, stats = {}) { function renderGreeting(user, stats = {}) {
const { urgentCount = 0, todayEventCount = 0, todayMealTitle = null } = stats; const { overdueCount = 0, dueSoonCount = 0, todayEventCount = 0, todayMealTitle = null } = stats;
const statChips = []; const statChips = [];
if (urgentCount > 0) if (overdueCount > 0)
statChips.push(`<span class="greeting-chip greeting-chip--warn"> statChips.push(`<span class="greeting-chip greeting-chip--warn">
<i data-lucide="alert-circle" class="icon-sm" style="flex-shrink:0" aria-hidden="true"></i> <i data-lucide="alert-circle" class="icon-sm" style="flex-shrink:0" aria-hidden="true"></i>
${urgentCount > 1 ? t('dashboard.urgentTasksChipPlural', { count: urgentCount }) : t('dashboard.urgentTasksChip', { count: urgentCount })} ${overdueCount > 1 ? t('dashboard.overdueTasksChipPlural', { count: overdueCount }) : t('dashboard.overdueTasksChip', { count: overdueCount })}
</span>`);
if (dueSoonCount > 0)
statChips.push(`<span class="greeting-chip greeting-chip--due">
<i data-lucide="clock" class="icon-sm" style="flex-shrink:0" aria-hidden="true"></i>
${dueSoonCount > 1 ? t('dashboard.urgentTasksChipPlural', { count: dueSoonCount }) : t('dashboard.urgentTasksChip', { count: dueSoonCount })}
</span>`); </span>`);
if (todayEventCount > 0) if (todayEventCount > 0)
statChips.push(`<span class="greeting-chip"> statChips.push(`<span class="greeting-chip">
@@ -166,12 +195,13 @@ function renderGreeting(user, stats = {}) {
${t('dashboard.todayMealChip', { title: esc(todayMealTitle) })} ${t('dashboard.todayMealChip', { title: esc(todayMealTitle) })}
</span>`); </span>`);
let time = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
return ` return `
<div class="widget-greeting"> <div class="widget-greeting">
<div class="widget-greeting__inner"> <div class="widget-greeting__inner">
<div class="widget-greeting__content"> <div class="widget-greeting__content">
<div class="widget-greeting__title">${greeting(user.display_name)}</div> <div class="widget-greeting__title">${formatDate(new Date())} - ${time}</div>
<div class="widget-greeting__date">${formatDate(new Date())}</div>
${statChips.length ? `<div class="widget-greeting__chips">${statChips.join('')}</div>` : ''} ${statChips.length ? `<div class="widget-greeting__chips">${statChips.join('')}</div>` : ''}
</div> </div>
<button class="widget-customize-btn" id="dashboard-customize-btn" <button class="widget-customize-btn" id="dashboard-customize-btn"
@@ -195,14 +225,14 @@ function renderUrgentTasks(tasks) {
} }
const items = tasks.map((t) => { const items = tasks.map((t) => {
const due = formatDueDate(t.due_date); const due = formatDueDate(t.due_date, t.due_time);
return ` return `
<div class="task-item" data-route="/tasks?open=${t.id}" role="button" tabindex="0"> <div class="task-item" data-task-id="${t.id}" data-task-title="${esc(t.title)}" role="button" tabindex="0">
${t.priority !== 'none' ? `<div class="task-item__priority task-item__priority--${t.priority}" aria-hidden="true"></div>` : ''} ${t.priority !== 'none' ? `<div class="task-item__priority task-item__priority--${t.priority}" aria-hidden="true"></div>` : ''}
<span class="sr-only">${PRIORITY_LABELS()[t.priority] ?? t.priority}</span> <span class="sr-only">${PRIORITY_LABELS()[t.priority] ?? t.priority}</span>
<div class="task-item__content"> <div class="task-item__content">
<div class="task-item__title">${esc(t.title)}</div> <div class="task-item__title">${esc(t.title)}</div>
${due ? `<div class="task-item__meta ${due.overdue ? 'task-item__meta--overdue' : ''}">${due.text}</div>` : ''} ${due ? `<div class="task-item__meta ${due.overdue ? 'task-item__meta--overdue' : ''} ${due.soon ? 'task-item__meta--soon' : ''}">${due.text}</div>` : ''}
</div> </div>
${t.assigned_color ? ` ${t.assigned_color ? `
<div class="task-item__avatar" style="background-color:${esc(t.assigned_color)}" <div class="task-item__avatar" style="background-color:${esc(t.assigned_color)}"
@@ -656,7 +686,14 @@ export async function render(container, { user }) {
const today = new Date().toDateString(); const today = new Date().toDateString();
const stats = { const stats = {
urgentCount: (data.urgentTasks ?? []).filter((t) => t.priority === 'urgent' || t.priority === 'high').length, overdueCount: (data.urgentTasks ?? []).filter((t) => {
const due = formatDueDate(t.due_date, t.due_time);
return due?.overdue === true;
}).length,
dueSoonCount: (data.urgentTasks ?? []).filter((t) => {
const due = formatDueDate(t.due_date, t.due_time);
return due?.soon === true;
}).length,
todayEventCount: (data.upcomingEvents ?? []).filter((e) => todayEventCount: (data.upcomingEvents ?? []).filter((e) =>
new Date(e.start_datetime).toDateString() === today new Date(e.start_datetime).toDateString() === today
).length, ).length,
@@ -665,13 +702,15 @@ export async function render(container, { user }) {
?? null, ?? null,
}; };
const rerender = () => render(container, { user });
function rebuildGrid(cfg) { function rebuildGrid(cfg) {
const grid = container.querySelector('.dashboard__grid'); const grid = container.querySelector('.dashboard__grid');
if (!grid) return; if (!grid) return;
const greeting = grid.querySelector('.widget-greeting'); const greeting = grid.querySelector('.widget-greeting');
grid.replaceChildren(...(greeting ? [greeting] : [])); grid.replaceChildren(...(greeting ? [greeting] : []));
grid.insertAdjacentHTML('beforeend', renderWidgets(cfg, data, weather)); grid.insertAdjacentHTML('beforeend', renderWidgets(cfg, data, weather));
wireLinks(container); wireLinks(container, rerender);
if (window.lucide) window.lucide.createIcons(); if (window.lucide) window.lucide.createIcons();
wireWeatherRefresh(container); wireWeatherRefresh(container);
} }
+24 -8
View File
@@ -69,7 +69,7 @@
} }
} }
@media (min-width: 1440px) { @media (min-width: 1280px) {
.dashboard__grid { .dashboard__grid {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
} }
@@ -107,10 +107,8 @@
.widget-greeting__content { .widget-greeting__content {
display: flex; display: flex;
flex-direction: column; gap: var(--space-3);
gap: var(--space-0h); align-items: center;
flex: 1;
min-width: 0;
} }
.widget-greeting__title { .widget-greeting__title {
@@ -120,7 +118,10 @@
@media (min-width: 1024px) { @media (min-width: 1024px) {
.widget-greeting__title { .widget-greeting__title {
font-size: var(--text-2xl); font-size: 1.4rem;
opacity: 0.85;
white-space: nowrap;
padding-top: 4px;
} }
} }
@@ -132,8 +133,9 @@
.widget-greeting__chips { .widget-greeting__chips {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-2); margin-top: 4px;
margin-top: var(--space-2); align-items: center;
gap: var(--space-2, 8px);
} }
.greeting-chip { .greeting-chip {
@@ -152,6 +154,10 @@
background: var(--color-danger-translucent); background: var(--color-danger-translucent);
} }
.greeting-chip--due {
background: rgba(245, 158, 11, 0.28);
}
/* -------------------------------------------------------- /* --------------------------------------------------------
* Basis-Widget (Card) * Basis-Widget (Card)
* *
@@ -330,6 +336,16 @@
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
} }
.task-item__meta--soon {
color: var(--color-soon);
font-weight: var(--font-weight-medium);
}
.task-item__meta--overdue {
color: var(--color-danger);
font-weight: var(--font-weight-medium);
}
.task-item__avatar { .task-item__avatar {
width: var(--space-5); width: var(--space-5);
height: var(--space-5); height: var(--space-5);