task
This commit is contained in:
@@ -4,25 +4,6 @@
|
|||||||
<h1>Tasks</h1>
|
<h1>Tasks</h1>
|
||||||
<p>Einfache Tagesaufgaben ohne Listenbezug.</p>
|
<p>Einfache Tagesaufgaben ohne Listenbezug.</p>
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
<mat-card class="task-create-card" appearance="outlined">
|
<mat-card class="task-create-card" appearance="outlined">
|
||||||
@@ -39,6 +20,16 @@
|
|||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</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-form-field appearance="outline">
|
||||||
<mat-label>Notiz</mat-label>
|
<mat-label>Notiz</mat-label>
|
||||||
<input
|
<input
|
||||||
@@ -66,6 +57,58 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</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()) {
|
@if (loading()) {
|
||||||
<mat-card class="state-card" appearance="outlined">
|
<mat-card class="state-card" appearance="outlined">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
@@ -97,105 +140,135 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-list">
|
@if (hasVisibleTasks()) {
|
||||||
@for (task of tasks(); track task.id) {
|
<p class="result-count">{{ visibleTasks().length }} von {{ tasks().length }} Tasks</p>
|
||||||
<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 (editingTaskId() === task.id) {
|
<div class="task-groups">
|
||||||
<div class="task-edit-form">
|
@for (group of taskGroups(); track group.date) {
|
||||||
<mat-form-field appearance="outline">
|
<section class="task-day-group" [class.today-group]="group.date === today()">
|
||||||
<mat-label>Titel</mat-label>
|
<header class="task-day-header">
|
||||||
<input
|
<div>
|
||||||
matInput
|
<h2>{{ group.label }}</h2>
|
||||||
type="text"
|
<p>{{ formatDay(group.date) }}</p>
|
||||||
[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>
|
</div>
|
||||||
|
<span>{{ group.tasks.length }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="task-actions">
|
<div class="task-list">
|
||||||
<button
|
@for (task of group.tasks; track task.id) {
|
||||||
mat-icon-button
|
<mat-card
|
||||||
type="button"
|
class="task-card"
|
||||||
aria-label="Task bearbeiten"
|
[class.completed]="task.completed"
|
||||||
(click)="startEdit(task)"
|
appearance="outlined"
|
||||||
>
|
>
|
||||||
<mat-icon aria-hidden="true">edit</mat-icon>
|
<mat-card-content>
|
||||||
</button>
|
<mat-checkbox
|
||||||
<button
|
[checked]="task.completed"
|
||||||
mat-icon-button
|
[disabled]="updatingTaskId() === task.id"
|
||||||
type="button"
|
(change)="toggleTask(task, $event.checked)"
|
||||||
aria-label="Task loeschen"
|
[attr.aria-label]="task.title + ' erledigt'"
|
||||||
[disabled]="deletingTaskId() === task.id"
|
/>
|
||||||
(click)="deleteTask(task)"
|
|
||||||
>
|
@if (editingTaskId() === task.id) {
|
||||||
@if (deletingTaskId() === task.id) {
|
<div class="task-edit-form">
|
||||||
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
<mat-form-field appearance="outline">
|
||||||
} @else {
|
<mat-label>Titel</mat-label>
|
||||||
<mat-icon aria-hidden="true">delete</mat-icon>
|
<input
|
||||||
}
|
matInput
|
||||||
</button>
|
type="text"
|
||||||
</div>
|
[value]="editTitle()"
|
||||||
}
|
(input)="editTitle.set($any($event.target).value)"
|
||||||
</mat-card-content>
|
/>
|
||||||
</mat-card>
|
</mat-form-field>
|
||||||
}
|
<mat-form-field appearance="outline">
|
||||||
</div>
|
<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 {
|
} @else {
|
||||||
<mat-card class="state-card" appearance="outlined">
|
<mat-card class="state-card" appearance="outlined">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<mat-icon aria-hidden="true">task_alt</mat-icon>
|
<mat-icon aria-hidden="true">task_alt</mat-icon>
|
||||||
<h2>Keine Tasks fuer diesen Tag</h2>
|
<h2>Noch keine Tasks</h2>
|
||||||
<p>Lege oben eine Aufgabe an, die an diesem Tag erledigt werden soll.</p>
|
<p>Lege oben eine Aufgabe an, die an einem bestimmten Tag erledigt werden soll.</p>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</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 {
|
.task-create-card {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -38,6 +24,48 @@
|
|||||||
justify-self: start;
|
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 {
|
.task-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -46,20 +74,78 @@
|
|||||||
color: var(--mat-sys-on-surface-variant);
|
color: var(--mat-sys-on-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-summary span,
|
.result-count {
|
||||||
.task-content span {
|
margin: 0 0 0.85rem;
|
||||||
|
color: var(--mat-sys-on-surface-variant);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-summary span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-summary mat-icon,
|
.task-summary mat-icon {
|
||||||
.task-content mat-icon {
|
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
font-size: 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 {
|
.task-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -90,14 +176,14 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-content h2 {
|
.task-content h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card.completed .task-content h2 {
|
.task-card.completed .task-content h3 {
|
||||||
color: var(--mat-sys-on-surface-variant);
|
color: var(--mat-sys-on-surface-variant);
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
@@ -108,12 +194,6 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-content span {
|
|
||||||
margin-top: 0.45rem;
|
|
||||||
color: var(--mat-sys-on-surface-variant);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-actions {
|
.task-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -134,15 +214,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@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 {
|
.task-card mat-card-content {
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@@ -153,12 +224,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 701px) {
|
@media (min-width: 701px) {
|
||||||
.day-controls {
|
|
||||||
width: min(100%, 440px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-card mat-card-content {
|
.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;
|
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 { FormsModule } from '@angular/forms';
|
||||||
import { finalize } from 'rxjs';
|
import { finalize } from 'rxjs';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
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.service';
|
import { TasksService } from './tasks.service';
|
||||||
import { UserTask } from './tasks.models';
|
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({
|
@Component({
|
||||||
selector: 'app-tasks',
|
selector: 'app-tasks',
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatButtonToggleModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
|
MatSelectModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
],
|
],
|
||||||
templateUrl: './tasks.component.html',
|
templateUrl: './tasks.component.html',
|
||||||
@@ -39,16 +52,58 @@ export class TasksComponent implements OnInit {
|
|||||||
protected readonly updatingTaskId = signal<string | null>(null);
|
protected readonly updatingTaskId = signal<string | null>(null);
|
||||||
protected readonly deletingTaskId = signal<string | null>(null);
|
protected readonly deletingTaskId = signal<string | null>(null);
|
||||||
protected readonly errorMessage = 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 newTitle = signal('');
|
||||||
protected readonly newNotes = signal('');
|
protected readonly newNotes = signal('');
|
||||||
protected readonly editingTaskId = signal<string | null>(null);
|
protected readonly editingTaskId = signal<string | null>(null);
|
||||||
protected readonly editTitle = signal('');
|
protected readonly editTitle = signal('');
|
||||||
protected readonly editNotes = signal('');
|
protected readonly editNotes = signal('');
|
||||||
protected readonly editDueDate = signal(this.todayKey());
|
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 openTasks = computed(() => this.tasks().filter((task) => !task.completed));
|
||||||
protected readonly completedTasks = 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 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 {
|
ngOnInit(): void {
|
||||||
this.loadTasks();
|
this.loadTasks();
|
||||||
@@ -58,7 +113,9 @@ export class TasksComponent implements OnInit {
|
|||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.errorMessage.set(null);
|
this.errorMessage.set(null);
|
||||||
|
|
||||||
this.tasksService.listTasks(this.selectedDate()).subscribe({
|
this.today.set(this.todayKey());
|
||||||
|
|
||||||
|
this.tasksService.listTasks().subscribe({
|
||||||
next: (tasks) => {
|
next: (tasks) => {
|
||||||
this.tasks.set(this.sortTasks(tasks));
|
this.tasks.set(this.sortTasks(tasks));
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
@@ -82,7 +139,7 @@ export class TasksComponent implements OnInit {
|
|||||||
.createTask({
|
.createTask({
|
||||||
title,
|
title,
|
||||||
notes: this.optionalText(this.newNotes()),
|
notes: this.optionalText(this.newNotes()),
|
||||||
dueDate: this.selectedDate(),
|
dueDate: this.newDueDate(),
|
||||||
})
|
})
|
||||||
.pipe(finalize(() => this.saving.set(false)))
|
.pipe(finalize(() => this.saving.set(false)))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@@ -129,7 +186,7 @@ export class TasksComponent implements OnInit {
|
|||||||
this.editingTaskId.set(null);
|
this.editingTaskId.set(null);
|
||||||
this.editTitle.set('');
|
this.editTitle.set('');
|
||||||
this.editNotes.set('');
|
this.editNotes.set('');
|
||||||
this.editDueDate.set(this.selectedDate());
|
this.editDueDate.set(this.newDueDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected saveEdit(task: UserTask): void {
|
protected saveEdit(task: UserTask): void {
|
||||||
@@ -149,13 +206,7 @@ export class TasksComponent implements OnInit {
|
|||||||
.pipe(finalize(() => this.updatingTaskId.set(null)))
|
.pipe(finalize(() => this.updatingTaskId.set(null)))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (updatedTask) => {
|
next: (updatedTask) => {
|
||||||
if (updatedTask.dueDate !== this.selectedDate()) {
|
this.replaceTask(updatedTask);
|
||||||
this.tasks.update((tasks) =>
|
|
||||||
tasks.filter((existingTask) => existingTask.id !== updatedTask.id),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.replaceTask(updatedTask);
|
|
||||||
}
|
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
},
|
},
|
||||||
error: (error: unknown) => {
|
error: (error: unknown) => {
|
||||||
@@ -187,28 +238,12 @@ export class TasksComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected previousDay(): void {
|
protected selectNewDueDate(value: string): void {
|
||||||
this.shiftSelectedDate(-1);
|
if (!value || value === this.newDueDate()) {
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedDate.set(value);
|
this.newDueDate.set(value);
|
||||||
this.cancelEdit();
|
|
||||||
this.loadTasks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected formatDay(value: string): string {
|
protected formatDay(value: string): string {
|
||||||
@@ -217,13 +252,26 @@ export class TasksComponent implements OnInit {
|
|||||||
return year && month && day ? `${day}.${month}.${year}` : value;
|
return year && month && day ? `${day}.${month}.${year}` : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private shiftSelectedDate(offsetDays: number): void {
|
protected dayLabel(value: string): string {
|
||||||
const date = new Date(`${this.selectedDate()}T00:00:00`);
|
if (value === this.today()) {
|
||||||
|
return 'Heute';
|
||||||
|
}
|
||||||
|
|
||||||
date.setDate(date.getDate() + offsetDays);
|
if (value === this.shiftDayKey(this.today(), 1)) {
|
||||||
this.selectedDate.set(this.toDateInputValue(date));
|
return 'Morgen';
|
||||||
this.cancelEdit();
|
}
|
||||||
this.loadTasks();
|
|
||||||
|
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 {
|
private replaceTask(task: UserTask): void {
|
||||||
@@ -240,10 +288,65 @@ export class TasksComponent implements OnInit {
|
|||||||
return left.completed ? 1 : -1;
|
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();
|
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 {
|
private optionalText(value: string): string | undefined {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
return trimmed ? trimmed : undefined;
|
return trimmed ? trimmed : undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user