task
This commit is contained in:
@@ -4,25 +4,6 @@
|
||||
<h1>Tasks</h1>
|
||||
<p>Einfache Tagesaufgaben ohne Listenbezug.</p>
|
||||
</div>
|
||||
|
||||
<div class="day-controls" aria-label="Tag auswaehlen">
|
||||
<button mat-icon-button type="button" aria-label="Vorheriger Tag" (click)="previousDay()">
|
||||
<mat-icon aria-hidden="true">chevron_left</mat-icon>
|
||||
</button>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tag</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="date"
|
||||
[value]="selectedDate()"
|
||||
(change)="selectDate($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-icon-button type="button" aria-label="Naechster Tag" (click)="nextDay()">
|
||||
<mat-icon aria-hidden="true">chevron_right</mat-icon>
|
||||
</button>
|
||||
<button mat-stroked-button type="button" (click)="selectToday()">Heute</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<mat-card class="task-create-card" appearance="outlined">
|
||||
@@ -39,6 +20,16 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="task-date-field">
|
||||
<mat-label>Faellig am</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="date"
|
||||
[value]="newDueDate()"
|
||||
(change)="selectNewDueDate($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Notiz</mat-label>
|
||||
<input
|
||||
@@ -66,6 +57,58 @@
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<section class="task-controls" aria-label="Tasks filtern">
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Tasks suchen</mat-label>
|
||||
<mat-icon matPrefix aria-hidden="true">search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="search"
|
||||
[value]="searchTerm()"
|
||||
(input)="searchTerm.set($any($event.target).value)"
|
||||
/>
|
||||
@if (searchTerm()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
type="button"
|
||||
aria-label="Suche loeschen"
|
||||
(click)="searchTerm.set('')"
|
||||
>
|
||||
<mat-icon aria-hidden="true">close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="date-filter-field">
|
||||
<mat-label>Zeitraum</mat-label>
|
||||
<mat-select [value]="dateFilter()" (selectionChange)="dateFilter.set($event.value)">
|
||||
@for (option of dateFilterOptions; track option.value) {
|
||||
<mat-option [value]="option.value">{{ option.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="status-filter-row">
|
||||
<mat-button-toggle-group
|
||||
[value]="statusFilter()"
|
||||
(change)="statusFilter.set($event.value)"
|
||||
aria-label="Task Status"
|
||||
>
|
||||
<mat-button-toggle value="open">Offen</mat-button-toggle>
|
||||
<mat-button-toggle value="all">Alle</mat-button-toggle>
|
||||
<mat-button-toggle value="completed">Erledigt</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
@if (activeFilterCount() > 0) {
|
||||
<button mat-button type="button" (click)="resetFilters()">
|
||||
<mat-icon aria-hidden="true">restart_alt</mat-icon>
|
||||
Zuruecksetzen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (loading()) {
|
||||
<mat-card class="state-card" appearance="outlined">
|
||||
<mat-card-content>
|
||||
@@ -97,105 +140,135 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-list">
|
||||
@for (task of tasks(); track task.id) {
|
||||
<mat-card class="task-card" [class.completed]="task.completed" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-checkbox
|
||||
[checked]="task.completed"
|
||||
[disabled]="updatingTaskId() === task.id"
|
||||
(change)="toggleTask(task, $event.checked)"
|
||||
[attr.aria-label]="task.title + ' erledigt'"
|
||||
/>
|
||||
@if (hasVisibleTasks()) {
|
||||
<p class="result-count">{{ visibleTasks().length }} von {{ tasks().length }} Tasks</p>
|
||||
|
||||
@if (editingTaskId() === task.id) {
|
||||
<div class="task-edit-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Titel</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[value]="editTitle()"
|
||||
(input)="editTitle.set($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Notiz</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[value]="editNotes()"
|
||||
(input)="editNotes.set($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tag</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="date"
|
||||
[value]="editDueDate()"
|
||||
(change)="editDueDate.set($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div class="task-edit-actions">
|
||||
<button
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[disabled]="!editTitle().trim() || updatingTaskId() === task.id"
|
||||
(click)="saveEdit(task)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">save</mat-icon>
|
||||
Speichern
|
||||
</button>
|
||||
<button mat-button type="button" (click)="cancelEdit()">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="task-content">
|
||||
<h2>{{ task.title }}</h2>
|
||||
@if (task.notes) {
|
||||
<p>{{ task.notes }}</p>
|
||||
}
|
||||
<span>
|
||||
<mat-icon aria-hidden="true">event</mat-icon>
|
||||
{{ formatDay(task.dueDate) }}
|
||||
</span>
|
||||
<div class="task-groups">
|
||||
@for (group of taskGroups(); track group.date) {
|
||||
<section class="task-day-group" [class.today-group]="group.date === today()">
|
||||
<header class="task-day-header">
|
||||
<div>
|
||||
<h2>{{ group.label }}</h2>
|
||||
<p>{{ formatDay(group.date) }}</p>
|
||||
</div>
|
||||
<span>{{ group.tasks.length }}</span>
|
||||
</header>
|
||||
|
||||
<div class="task-actions">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
aria-label="Task bearbeiten"
|
||||
(click)="startEdit(task)"
|
||||
<div class="task-list">
|
||||
@for (task of group.tasks; track task.id) {
|
||||
<mat-card
|
||||
class="task-card"
|
||||
[class.completed]="task.completed"
|
||||
appearance="outlined"
|
||||
>
|
||||
<mat-icon aria-hidden="true">edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
aria-label="Task loeschen"
|
||||
[disabled]="deletingTaskId() === task.id"
|
||||
(click)="deleteTask(task)"
|
||||
>
|
||||
@if (deletingTaskId() === task.id) {
|
||||
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
||||
} @else {
|
||||
<mat-icon aria-hidden="true">delete</mat-icon>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
<mat-card-content>
|
||||
<mat-checkbox
|
||||
[checked]="task.completed"
|
||||
[disabled]="updatingTaskId() === task.id"
|
||||
(change)="toggleTask(task, $event.checked)"
|
||||
[attr.aria-label]="task.title + ' erledigt'"
|
||||
/>
|
||||
|
||||
@if (editingTaskId() === task.id) {
|
||||
<div class="task-edit-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Titel</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[value]="editTitle()"
|
||||
(input)="editTitle.set($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Notiz</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[value]="editNotes()"
|
||||
(input)="editNotes.set($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tag</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="date"
|
||||
[value]="editDueDate()"
|
||||
(change)="editDueDate.set($any($event.target).value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div class="task-edit-actions">
|
||||
<button
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[disabled]="!editTitle().trim() || updatingTaskId() === task.id"
|
||||
(click)="saveEdit(task)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">save</mat-icon>
|
||||
Speichern
|
||||
</button>
|
||||
<button mat-button type="button" (click)="cancelEdit()">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="task-content">
|
||||
<h3>{{ task.title }}</h3>
|
||||
@if (task.notes) {
|
||||
<p>{{ task.notes }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="task-actions">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
aria-label="Task bearbeiten"
|
||||
(click)="startEdit(task)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
aria-label="Task loeschen"
|
||||
[disabled]="deletingTaskId() === task.id"
|
||||
(click)="deleteTask(task)"
|
||||
>
|
||||
@if (deletingTaskId() === task.id) {
|
||||
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
||||
} @else {
|
||||
<mat-icon aria-hidden="true">delete</mat-icon>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card class="state-card" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-icon aria-hidden="true">filter_alt_off</mat-icon>
|
||||
<h2>Keine Treffer</h2>
|
||||
<p>Mit den aktuellen Filtern wurden keine Tasks gefunden.</p>
|
||||
<button mat-stroked-button type="button" (click)="resetFilters()">
|
||||
<mat-icon aria-hidden="true">restart_alt</mat-icon>
|
||||
Filter zuruecksetzen
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
} @else {
|
||||
<mat-card class="state-card" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-icon aria-hidden="true">task_alt</mat-icon>
|
||||
<h2>Keine Tasks fuer diesen Tag</h2>
|
||||
<p>Lege oben eine Aufgabe an, die an diesem Tag erledigt werden soll.</p>
|
||||
<h2>Noch keine Tasks</h2>
|
||||
<p>Lege oben eine Aufgabe an, die an einem bestimmten Tag erledigt werden soll.</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
.day-controls {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.day-controls mat-form-field {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-create-card {
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
@@ -38,6 +24,48 @@
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.task-controls {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--mat-sys-surface) 88%, transparent);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.task-controls mat-form-field {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-filter-row {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-filter-row mat-button-toggle-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-color: color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status-filter-row mat-button-toggle,
|
||||
.status-filter-row button {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.status-filter-row button {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.task-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -46,20 +74,78 @@
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
}
|
||||
|
||||
.task-summary span,
|
||||
.task-content span {
|
||||
.result-count {
|
||||
margin: 0 0 0.85rem;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.task-summary span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.task-summary mat-icon,
|
||||
.task-content mat-icon {
|
||||
.task-summary mat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.task-groups {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-day-group {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-day-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
padding: 0.15rem 0.15rem 0;
|
||||
}
|
||||
|
||||
.task-day-header h2,
|
||||
.task-day-header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-day-header h2 {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.task-day-header p {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.task-day-header span {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding-inline: 0.45rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.today-group .task-day-header h2 {
|
||||
color: var(--mat-sys-primary);
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
@@ -90,14 +176,14 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-content h2 {
|
||||
.task-content h3 {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-card.completed .task-content h2 {
|
||||
.task-card.completed .task-content h3 {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -108,12 +194,6 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-content span {
|
||||
margin-top: 0.45rem;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
@@ -134,15 +214,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.day-controls {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.day-controls button[mat-stroked-button] {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.task-card mat-card-content {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
@@ -153,12 +224,22 @@
|
||||
}
|
||||
|
||||
@media (min-width: 701px) {
|
||||
.day-controls {
|
||||
width: min(100%, 440px);
|
||||
}
|
||||
|
||||
.task-create-card mat-card-content {
|
||||
grid-template-columns: minmax(220px, 1.2fr) minmax(180px, 1fr) auto;
|
||||
grid-template-columns: minmax(220px, 1.2fr) minmax(170px, 0.7fr) minmax(180px, 1fr) auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.task-controls {
|
||||
grid-template-columns: minmax(260px, 1.2fr) minmax(180px, 0.55fr);
|
||||
align-items: start;
|
||||
gap: 0.85rem 1rem;
|
||||
margin-bottom: 1.35rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.status-filter-row {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: minmax(300px, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,41 @@ import { Component, OnInit, computed, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { finalize } from 'rxjs';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { getAuthErrorMessage } from '../auth/error-message';
|
||||
import { TasksService } from './tasks.service';
|
||||
import { UserTask } from './tasks.models';
|
||||
|
||||
type TaskStatusFilter = 'open' | 'all' | 'completed';
|
||||
type TaskDateFilter = 'all' | 'today' | 'future' | 'overdue';
|
||||
|
||||
interface TaskDayGroup {
|
||||
date: string;
|
||||
label: string;
|
||||
tasks: UserTask[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
],
|
||||
templateUrl: './tasks.component.html',
|
||||
@@ -39,16 +52,58 @@ export class TasksComponent implements OnInit {
|
||||
protected readonly updatingTaskId = signal<string | null>(null);
|
||||
protected readonly deletingTaskId = signal<string | null>(null);
|
||||
protected readonly errorMessage = signal<string | null>(null);
|
||||
protected readonly selectedDate = signal(this.todayKey());
|
||||
protected readonly newDueDate = signal(this.todayKey());
|
||||
protected readonly newTitle = signal('');
|
||||
protected readonly newNotes = signal('');
|
||||
protected readonly editingTaskId = signal<string | null>(null);
|
||||
protected readonly editTitle = signal('');
|
||||
protected readonly editNotes = signal('');
|
||||
protected readonly editDueDate = signal(this.todayKey());
|
||||
protected readonly searchTerm = signal('');
|
||||
protected readonly statusFilter = signal<TaskStatusFilter>('open');
|
||||
protected readonly dateFilter = signal<TaskDateFilter>('all');
|
||||
protected readonly today = signal(this.todayKey());
|
||||
protected readonly openTasks = computed(() => this.tasks().filter((task) => !task.completed));
|
||||
protected readonly completedTasks = computed(() => this.tasks().filter((task) => task.completed));
|
||||
protected readonly hasTasks = computed(() => this.tasks().length > 0);
|
||||
protected readonly visibleTasks = computed(() => {
|
||||
const term = this.searchTerm().trim().toLowerCase();
|
||||
const today = this.today();
|
||||
|
||||
return this.tasks().filter((task) => {
|
||||
const matchesStatus =
|
||||
this.statusFilter() === 'all' ||
|
||||
(this.statusFilter() === 'open' && !task.completed) ||
|
||||
(this.statusFilter() === 'completed' && task.completed);
|
||||
const matchesDate =
|
||||
this.dateFilter() === 'all' ||
|
||||
(this.dateFilter() === 'today' && task.dueDate === today) ||
|
||||
(this.dateFilter() === 'future' && task.dueDate > today) ||
|
||||
(this.dateFilter() === 'overdue' && task.dueDate < today);
|
||||
const matchesSearch =
|
||||
!term ||
|
||||
[task.title, task.notes ?? '', task.dueDate].join(' ').toLowerCase().includes(term);
|
||||
|
||||
return matchesStatus && matchesDate && matchesSearch;
|
||||
});
|
||||
});
|
||||
protected readonly taskGroups = computed(() => this.groupTasksByDay(this.visibleTasks()));
|
||||
protected readonly hasVisibleTasks = computed(() => this.visibleTasks().length > 0);
|
||||
protected readonly activeFilterCount = computed(
|
||||
() =>
|
||||
Number(this.searchTerm().trim().length > 0) +
|
||||
Number(this.statusFilter() !== 'open') +
|
||||
Number(this.dateFilter() !== 'all'),
|
||||
);
|
||||
protected readonly dateFilterOptions: ReadonlyArray<{
|
||||
value: TaskDateFilter;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: 'all', label: 'Alle Tage' },
|
||||
{ value: 'today', label: 'Heute' },
|
||||
{ value: 'future', label: 'Zukunft' },
|
||||
{ value: 'overdue', label: 'Ueberfaellig' },
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
@@ -58,7 +113,9 @@ export class TasksComponent implements OnInit {
|
||||
this.loading.set(true);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
this.tasksService.listTasks(this.selectedDate()).subscribe({
|
||||
this.today.set(this.todayKey());
|
||||
|
||||
this.tasksService.listTasks().subscribe({
|
||||
next: (tasks) => {
|
||||
this.tasks.set(this.sortTasks(tasks));
|
||||
this.loading.set(false);
|
||||
@@ -82,7 +139,7 @@ export class TasksComponent implements OnInit {
|
||||
.createTask({
|
||||
title,
|
||||
notes: this.optionalText(this.newNotes()),
|
||||
dueDate: this.selectedDate(),
|
||||
dueDate: this.newDueDate(),
|
||||
})
|
||||
.pipe(finalize(() => this.saving.set(false)))
|
||||
.subscribe({
|
||||
@@ -129,7 +186,7 @@ export class TasksComponent implements OnInit {
|
||||
this.editingTaskId.set(null);
|
||||
this.editTitle.set('');
|
||||
this.editNotes.set('');
|
||||
this.editDueDate.set(this.selectedDate());
|
||||
this.editDueDate.set(this.newDueDate());
|
||||
}
|
||||
|
||||
protected saveEdit(task: UserTask): void {
|
||||
@@ -149,13 +206,7 @@ export class TasksComponent implements OnInit {
|
||||
.pipe(finalize(() => this.updatingTaskId.set(null)))
|
||||
.subscribe({
|
||||
next: (updatedTask) => {
|
||||
if (updatedTask.dueDate !== this.selectedDate()) {
|
||||
this.tasks.update((tasks) =>
|
||||
tasks.filter((existingTask) => existingTask.id !== updatedTask.id),
|
||||
);
|
||||
} else {
|
||||
this.replaceTask(updatedTask);
|
||||
}
|
||||
this.replaceTask(updatedTask);
|
||||
this.cancelEdit();
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
@@ -187,28 +238,12 @@ export class TasksComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
protected previousDay(): void {
|
||||
this.shiftSelectedDate(-1);
|
||||
}
|
||||
|
||||
protected nextDay(): void {
|
||||
this.shiftSelectedDate(1);
|
||||
}
|
||||
|
||||
protected selectToday(): void {
|
||||
this.selectedDate.set(this.todayKey());
|
||||
this.cancelEdit();
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
protected selectDate(value: string): void {
|
||||
if (!value || value === this.selectedDate()) {
|
||||
protected selectNewDueDate(value: string): void {
|
||||
if (!value || value === this.newDueDate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedDate.set(value);
|
||||
this.cancelEdit();
|
||||
this.loadTasks();
|
||||
this.newDueDate.set(value);
|
||||
}
|
||||
|
||||
protected formatDay(value: string): string {
|
||||
@@ -217,13 +252,26 @@ export class TasksComponent implements OnInit {
|
||||
return year && month && day ? `${day}.${month}.${year}` : value;
|
||||
}
|
||||
|
||||
private shiftSelectedDate(offsetDays: number): void {
|
||||
const date = new Date(`${this.selectedDate()}T00:00:00`);
|
||||
protected dayLabel(value: string): string {
|
||||
if (value === this.today()) {
|
||||
return 'Heute';
|
||||
}
|
||||
|
||||
date.setDate(date.getDate() + offsetDays);
|
||||
this.selectedDate.set(this.toDateInputValue(date));
|
||||
this.cancelEdit();
|
||||
this.loadTasks();
|
||||
if (value === this.shiftDayKey(this.today(), 1)) {
|
||||
return 'Morgen';
|
||||
}
|
||||
|
||||
if (value === this.shiftDayKey(this.today(), -1)) {
|
||||
return 'Gestern';
|
||||
}
|
||||
|
||||
return this.formatDay(value);
|
||||
}
|
||||
|
||||
protected resetFilters(): void {
|
||||
this.searchTerm.set('');
|
||||
this.statusFilter.set('open');
|
||||
this.dateFilter.set('all');
|
||||
}
|
||||
|
||||
private replaceTask(task: UserTask): void {
|
||||
@@ -240,10 +288,65 @@ export class TasksComponent implements OnInit {
|
||||
return left.completed ? 1 : -1;
|
||||
}
|
||||
|
||||
const dueDateComparison = this.compareDayKeys(left.dueDate, right.dueDate);
|
||||
|
||||
if (dueDateComparison !== 0) {
|
||||
return dueDateComparison;
|
||||
}
|
||||
|
||||
return new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
private groupTasksByDay(tasks: UserTask[]): TaskDayGroup[] {
|
||||
const groupedTasks = new Map<string, UserTask[]>();
|
||||
|
||||
for (const task of this.sortTasks(tasks)) {
|
||||
groupedTasks.set(task.dueDate, [...(groupedTasks.get(task.dueDate) ?? []), task]);
|
||||
}
|
||||
|
||||
return [...groupedTasks.entries()]
|
||||
.sort(([leftDate], [rightDate]) => this.compareDayKeys(leftDate, rightDate))
|
||||
.map(([date, dayTasks]) => ({
|
||||
date,
|
||||
label: this.dayLabel(date),
|
||||
tasks: dayTasks,
|
||||
}));
|
||||
}
|
||||
|
||||
private compareDayKeys(left: string, right: string): number {
|
||||
return this.dayRank(left) - this.dayRank(right) || left.localeCompare(right);
|
||||
}
|
||||
|
||||
private dayRank(value: string): number {
|
||||
const today = this.today();
|
||||
|
||||
if (value === today) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (value > today) {
|
||||
return 100000 + this.daysBetween(today, value);
|
||||
}
|
||||
|
||||
return 200000 + this.daysBetween(value, today);
|
||||
}
|
||||
|
||||
private daysBetween(startDate: string, endDate: string): number {
|
||||
const start = new Date(`${startDate}T00:00:00`).getTime();
|
||||
const end = new Date(`${endDate}T00:00:00`).getTime();
|
||||
|
||||
return Math.round((end - start) / 86_400_000);
|
||||
}
|
||||
|
||||
private shiftDayKey(value: string, offsetDays: number): string {
|
||||
const date = new Date(`${value}T00:00:00`);
|
||||
|
||||
date.setDate(date.getDate() + offsetDays);
|
||||
|
||||
return this.toDateInputValue(date);
|
||||
}
|
||||
|
||||
private optionalText(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
|
||||
Reference in New Issue
Block a user