icon
This commit is contained in:
@@ -31,6 +31,67 @@
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else if (dashboard()) {
|
||||
<section class="dashboard-section" aria-labelledby="current-tasks-title">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2 id="current-tasks-title">Aktuelle Tasks</h2>
|
||||
<p>Heute faellige und ueberfaellige Aufgaben.</p>
|
||||
</div>
|
||||
<a mat-stroked-button routerLink="/tasks">
|
||||
<mat-icon aria-hidden="true">task_alt</mat-icon>
|
||||
Alle Tasks
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (tasksLoading()) {
|
||||
<mat-card class="state-card compact-state" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-progress-spinner mode="indeterminate" diameter="28" />
|
||||
<h2>Tasks werden geladen</h2>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else if (hasCurrentTasks()) {
|
||||
<mat-card class="current-tasks-card" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<div class="current-task-list">
|
||||
@for (task of currentTasks(); track task.id) {
|
||||
<div class="current-task-row" [class.overdue]="task.dueDate < dashboard()!.dateKey">
|
||||
<mat-checkbox
|
||||
[checked]="task.completed"
|
||||
[disabled]="updatingTaskId() === task.id"
|
||||
(change)="completeTask(task)"
|
||||
aria-label="Task erledigen"
|
||||
/>
|
||||
|
||||
<div class="current-task-main">
|
||||
<strong>{{ task.title }}</strong>
|
||||
@if (task.notes) {
|
||||
<span>{{ task.notes }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="task-date-chip">
|
||||
<mat-icon aria-hidden="true">
|
||||
{{ task.dueDate < dashboard()!.dateKey ? 'priority_high' : 'today' }}
|
||||
</mat-icon>
|
||||
{{ taskDateLabel(task) }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else {
|
||||
<mat-card class="state-card compact-state" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-icon aria-hidden="true">task_alt</mat-icon>
|
||||
<h2>Keine aktuellen Tasks</h2>
|
||||
<p>Heute ist nichts offen oder ueberfaellig.</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="dashboard-section" aria-labelledby="important-lists-title">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
}
|
||||
|
||||
.important-list-card,
|
||||
.suggestion-card {
|
||||
.suggestion-card,
|
||||
.current-tasks-card {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||
@@ -46,6 +47,10 @@
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.compact-state mat-card-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.important-list-card mat-card-title,
|
||||
.important-list-card mat-card-subtitle,
|
||||
.suggestion-card mat-card-title,
|
||||
@@ -53,6 +58,73 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.current-task-list {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.current-task-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.35rem 0.65rem;
|
||||
min-width: 0;
|
||||
padding: 0.55rem 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 64%, transparent);
|
||||
}
|
||||
|
||||
.current-task-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.current-task-main {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.current-task-main strong,
|
||||
.current-task-main span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.current-task-main strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-task-main span {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.task-date-chip {
|
||||
display: inline-flex;
|
||||
grid-column: 2;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--mat-sys-primary-container) 65%, transparent);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.current-task-row.overdue .task-date-chip {
|
||||
background: color-mix(in srgb, var(--mat-sys-error-container) 78%, transparent);
|
||||
color: var(--mat-sys-on-error-container);
|
||||
}
|
||||
|
||||
.task-date-chip mat-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dashboard-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -129,6 +201,14 @@ a mat-progress-spinner {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.current-task-row {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.task-date-chip {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1040px) {
|
||||
|
||||
@@ -4,11 +4,14 @@ import { RouterLink } from '@angular/router';
|
||||
import { finalize } from 'rxjs';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { getAuthErrorMessage } from '../auth/error-message';
|
||||
import { TasksService } from '../tasks/tasks.service';
|
||||
import { UserTask } from '../tasks/tasks.models';
|
||||
import { DashboardResponse, DashboardWeeklySuggestion } from './dashboard.models';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
@@ -19,6 +22,7 @@ import { DashboardService } from './dashboard.service';
|
||||
RouterLink,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatIconModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
@@ -29,21 +33,43 @@ import { DashboardService } from './dashboard.service';
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
private readonly dashboardService = inject(DashboardService);
|
||||
private readonly tasksService = inject(TasksService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
protected readonly dashboard = signal<DashboardResponse | null>(null);
|
||||
protected readonly tasks = signal<UserTask[]>([]);
|
||||
protected readonly loading = signal(true);
|
||||
protected readonly tasksLoading = signal(true);
|
||||
protected readonly errorMessage = signal<string | null>(null);
|
||||
protected readonly creatingSuggestionId = signal<string | null>(null);
|
||||
protected readonly updatingTaskId = signal<string | null>(null);
|
||||
protected readonly hasImportantLists = computed(
|
||||
() => (this.dashboard()?.importantLists.length ?? 0) > 0,
|
||||
);
|
||||
protected readonly currentTasks = computed(() => {
|
||||
const today = this.dashboard()?.dateKey ?? this.todayKey();
|
||||
|
||||
return this.tasks()
|
||||
.filter((task) => !task.completed && task.dueDate <= today)
|
||||
.sort((left, right) => {
|
||||
const dueDateComparison = left.dueDate.localeCompare(right.dueDate);
|
||||
|
||||
if (dueDateComparison !== 0) {
|
||||
return dueDateComparison;
|
||||
}
|
||||
|
||||
return new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime();
|
||||
})
|
||||
.slice(0, 6);
|
||||
});
|
||||
protected readonly hasCurrentTasks = computed(() => this.currentTasks().length > 0);
|
||||
protected readonly suggestions = computed(
|
||||
() => this.dashboard()?.weeklySuggestions.suggestions ?? [],
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadDashboard();
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
protected loadDashboard(): void {
|
||||
@@ -62,6 +88,48 @@ export class DashboardComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
protected loadTasks(): void {
|
||||
this.tasksLoading.set(true);
|
||||
|
||||
this.tasksService.listTasks().subscribe({
|
||||
next: (tasks) => {
|
||||
this.tasks.set(tasks);
|
||||
this.tasksLoading.set(false);
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.tasksLoading.set(false);
|
||||
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected completeTask(task: UserTask): void {
|
||||
if (this.updatingTaskId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatingTaskId.set(task.id);
|
||||
this.tasksService
|
||||
.updateTask(task.id, { completed: true })
|
||||
.pipe(finalize(() => this.updatingTaskId.set(null)))
|
||||
.subscribe({
|
||||
next: (updatedTask) => {
|
||||
this.tasks.update((tasks) =>
|
||||
tasks.map((existingTask) =>
|
||||
existingTask.id === updatedTask.id ? updatedTask : existingTask,
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected createSuggestion(suggestion: DashboardWeeklySuggestion): void {
|
||||
if (suggestion.createdListId || this.creatingSuggestionId()) {
|
||||
return;
|
||||
@@ -102,4 +170,29 @@ export class DashboardComponent implements OnInit {
|
||||
protected progressPercent(value: number): number {
|
||||
return Math.round(value * 100);
|
||||
}
|
||||
|
||||
protected taskDateLabel(task: UserTask): string {
|
||||
const today = this.dashboard()?.dateKey ?? this.todayKey();
|
||||
|
||||
if (task.dueDate === today) {
|
||||
return 'Heute';
|
||||
}
|
||||
|
||||
return `Ueberfaellig seit ${this.formatDay(task.dueDate)}`;
|
||||
}
|
||||
|
||||
private formatDay(value: string): string {
|
||||
const [year, month, day] = value.split('-');
|
||||
|
||||
return year && month && day ? `${day}.${month}.${year}` : value;
|
||||
}
|
||||
|
||||
private todayKey(): string {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#6750a4" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="pwa-icon.svg" />
|
||||
<link rel="alternate icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
Reference in New Issue
Block a user