icon
This commit is contained in:
@@ -31,6 +31,67 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
} @else if (dashboard()) {
|
} @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">
|
<section class="dashboard-section" aria-labelledby="important-lists-title">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.important-list-card,
|
.important-list-card,
|
||||||
.suggestion-card {
|
.suggestion-card,
|
||||||
|
.current-tasks-card {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
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);
|
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-title,
|
||||||
.important-list-card mat-card-subtitle,
|
.important-list-card mat-card-subtitle,
|
||||||
.suggestion-card mat-card-title,
|
.suggestion-card mat-card-title,
|
||||||
@@ -53,6 +58,73 @@
|
|||||||
overflow-wrap: anywhere;
|
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 {
|
.dashboard-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -129,6 +201,14 @@ a mat-progress-spinner {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-task-row {
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-date-chip {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1040px) {
|
@media (min-width: 1040px) {
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { RouterLink } from '@angular/router';
|
|||||||
import { finalize } from 'rxjs';
|
import { finalize } from 'rxjs';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { getAuthErrorMessage } from '../auth/error-message';
|
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 { DashboardResponse, DashboardWeeklySuggestion } from './dashboard.models';
|
||||||
import { DashboardService } from './dashboard.service';
|
import { DashboardService } from './dashboard.service';
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ import { DashboardService } from './dashboard.service';
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
@@ -29,21 +33,43 @@ import { DashboardService } from './dashboard.service';
|
|||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent implements OnInit {
|
||||||
private readonly dashboardService = inject(DashboardService);
|
private readonly dashboardService = inject(DashboardService);
|
||||||
|
private readonly tasksService = inject(TasksService);
|
||||||
private readonly snackBar = inject(MatSnackBar);
|
private readonly snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
protected readonly dashboard = signal<DashboardResponse | null>(null);
|
protected readonly dashboard = signal<DashboardResponse | null>(null);
|
||||||
|
protected readonly tasks = signal<UserTask[]>([]);
|
||||||
protected readonly loading = signal(true);
|
protected readonly loading = signal(true);
|
||||||
|
protected readonly tasksLoading = signal(true);
|
||||||
protected readonly errorMessage = signal<string | null>(null);
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
protected readonly creatingSuggestionId = signal<string | null>(null);
|
protected readonly creatingSuggestionId = signal<string | null>(null);
|
||||||
|
protected readonly updatingTaskId = signal<string | null>(null);
|
||||||
protected readonly hasImportantLists = computed(
|
protected readonly hasImportantLists = computed(
|
||||||
() => (this.dashboard()?.importantLists.length ?? 0) > 0,
|
() => (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(
|
protected readonly suggestions = computed(
|
||||||
() => this.dashboard()?.weeklySuggestions.suggestions ?? [],
|
() => this.dashboard()?.weeklySuggestions.suggestions ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadDashboard();
|
this.loadDashboard();
|
||||||
|
this.loadTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadDashboard(): void {
|
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 {
|
protected createSuggestion(suggestion: DashboardWeeklySuggestion): void {
|
||||||
if (suggestion.createdListId || this.creatingSuggestionId()) {
|
if (suggestion.createdListId || this.creatingSuggestionId()) {
|
||||||
return;
|
return;
|
||||||
@@ -102,4 +170,29 @@ export class DashboardComponent implements OnInit {
|
|||||||
protected progressPercent(value: number): number {
|
protected progressPercent(value: number): number {
|
||||||
return Math.round(value * 100);
|
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="/" />
|
<base href="/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#6750a4" />
|
<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="manifest" href="manifest.webmanifest" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|||||||
Reference in New Issue
Block a user