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:
+59
-20
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user