tasks
This commit is contained in:
@@ -84,6 +84,16 @@
|
||||
<mat-icon matListItemIcon aria-hidden="true">dashboard_customize</mat-icon>
|
||||
<span matListItemTitle>Templates</span>
|
||||
</a>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/tasks"
|
||||
routerLinkActive="active-nav-link"
|
||||
ariaCurrentWhenActive="page"
|
||||
(click)="closeSidebarOnCompact()"
|
||||
>
|
||||
<mat-icon matListItemIcon aria-hidden="true">task_alt</mat-icon>
|
||||
<span matListItemTitle>Tasks</span>
|
||||
</a>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/lists"
|
||||
@@ -148,6 +158,15 @@
|
||||
<mat-icon aria-hidden="true">dashboard_customize</mat-icon>
|
||||
<span>Templates</span>
|
||||
</a>
|
||||
<a
|
||||
class="bottom-nav-link"
|
||||
routerLink="/tasks"
|
||||
routerLinkActive="active-bottom-link"
|
||||
ariaCurrentWhenActive="page"
|
||||
>
|
||||
<mat-icon aria-hidden="true">task_alt</mat-icon>
|
||||
<span>Tasks</span>
|
||||
</a>
|
||||
<a
|
||||
class="bottom-nav-link"
|
||||
routerLink="/lists"
|
||||
|
||||
@@ -25,6 +25,12 @@ export const routes: Routes = [
|
||||
import('./dashboard/dashboard.component').then((module) => module.DashboardComponent),
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
loadComponent: () => import('./tasks/tasks.component').then((module) => module.TasksComponent),
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{ path: 'aufgaben', redirectTo: 'tasks' },
|
||||
{ path: 'templates', component: TemplatesComponent, canActivate: [authGuard] },
|
||||
{
|
||||
path: 'templates/new',
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.25rem;
|
||||
padding: 0.35rem 0.5rem calc(0.35rem + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||
|
||||
202
listify-client/src/app/tasks/tasks.component.html
Normal file
202
listify-client/src/app/tasks/tasks.component.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<section class="workspace-page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<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">
|
||||
<mat-card-content>
|
||||
<mat-form-field appearance="outline" class="task-title-field">
|
||||
<mat-label>Neuer Task</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
[value]="newTitle()"
|
||||
(input)="newTitle.set($any($event.target).value)"
|
||||
(keydown.enter)="createTask()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Notiz</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[value]="newNotes()"
|
||||
(input)="newNotes.set($any($event.target).value)"
|
||||
(keydown.enter)="createTask()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[disabled]="!newTitle().trim() || saving()"
|
||||
(click)="createTask()"
|
||||
>
|
||||
@if (saving()) {
|
||||
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
||||
} @else {
|
||||
<mat-icon aria-hidden="true">add</mat-icon>
|
||||
}
|
||||
Hinzufuegen
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@if (loading()) {
|
||||
<mat-card class="state-card" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-progress-spinner mode="indeterminate" diameter="40" />
|
||||
<h2>Tasks werden geladen</h2>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else if (errorMessage()) {
|
||||
<mat-card class="state-card error-state" appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-icon aria-hidden="true">error</mat-icon>
|
||||
<h2>Tasks konnten nicht geladen werden</h2>
|
||||
<p>{{ errorMessage() }}</p>
|
||||
<button mat-stroked-button type="button" (click)="loadTasks()">
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
Erneut laden
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else if (hasTasks()) {
|
||||
<div class="task-summary" aria-label="Task Zusammenfassung">
|
||||
<span>
|
||||
<mat-icon aria-hidden="true">radio_button_unchecked</mat-icon>
|
||||
{{ openTasks().length }} offen
|
||||
</span>
|
||||
<span>
|
||||
<mat-icon aria-hidden="true">check_circle</mat-icon>
|
||||
{{ completedTasks().length }} erledigt
|
||||
</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 (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>
|
||||
|
||||
<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>
|
||||
} @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>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</section>
|
||||
164
listify-client/src/app/tasks/tasks.component.scss
Normal file
164
listify-client/src/app/tasks/tasks.component.scss
Normal file
@@ -0,0 +1,164 @@
|
||||
.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;
|
||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||
border-radius: 8px;
|
||||
background: var(--mat-sys-surface);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.task-create-card mat-card-content {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.task-create-card mat-form-field,
|
||||
.task-edit-form mat-form-field {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-create-card button {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.task-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 0.85rem;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
}
|
||||
|
||||
.task-summary span,
|
||||
.task-content span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.task-summary mat-icon,
|
||||
.task-content mat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||
border-radius: 8px;
|
||||
background: var(--mat-sys-surface);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.task-card.completed {
|
||||
background: color-mix(in srgb, var(--mat-sys-surface-container) 72%, var(--mat-sys-surface));
|
||||
}
|
||||
|
||||
.task-card mat-card-content {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-content h2 {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-card.completed .task-content h2 {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.task-content p {
|
||||
margin: 0.3rem 0 0;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
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;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.task-edit-form {
|
||||
display: grid;
|
||||
grid-column: 2 / -1;
|
||||
gap: 0.7rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-edit-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
263
listify-client/src/app/tasks/tasks.component.ts
Normal file
263
listify-client/src/app/tasks/tasks.component.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
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 { 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 { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { getAuthErrorMessage } from '../auth/error-message';
|
||||
import { TasksService } from './tasks.service';
|
||||
import { UserTask } from './tasks.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
],
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['../workspace-page.scss', './tasks.component.scss'],
|
||||
})
|
||||
export class TasksComponent implements OnInit {
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly tasksService = inject(TasksService);
|
||||
|
||||
protected readonly tasks = signal<UserTask[]>([]);
|
||||
protected readonly loading = signal(true);
|
||||
protected readonly saving = signal(false);
|
||||
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 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 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);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
protected loadTasks(): void {
|
||||
this.loading.set(true);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
this.tasksService.listTasks(this.selectedDate()).subscribe({
|
||||
next: (tasks) => {
|
||||
this.tasks.set(this.sortTasks(tasks));
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.errorMessage.set(getAuthErrorMessage(error));
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected createTask(): void {
|
||||
const title = this.newTitle().trim();
|
||||
|
||||
if (!title || this.saving()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving.set(true);
|
||||
this.tasksService
|
||||
.createTask({
|
||||
title,
|
||||
notes: this.optionalText(this.newNotes()),
|
||||
dueDate: this.selectedDate(),
|
||||
})
|
||||
.pipe(finalize(() => this.saving.set(false)))
|
||||
.subscribe({
|
||||
next: (task) => {
|
||||
this.tasks.update((tasks) => this.sortTasks([...tasks, task]));
|
||||
this.newTitle.set('');
|
||||
this.newNotes.set('');
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected toggleTask(task: UserTask, completed: boolean): void {
|
||||
if (this.updatingTaskId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatingTaskId.set(task.id);
|
||||
this.tasksService
|
||||
.updateTask(task.id, { completed })
|
||||
.pipe(finalize(() => this.updatingTaskId.set(null)))
|
||||
.subscribe({
|
||||
next: (updatedTask) => this.replaceTask(updatedTask),
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected startEdit(task: UserTask): void {
|
||||
this.editingTaskId.set(task.id);
|
||||
this.editTitle.set(task.title);
|
||||
this.editNotes.set(task.notes ?? '');
|
||||
this.editDueDate.set(task.dueDate);
|
||||
}
|
||||
|
||||
protected cancelEdit(): void {
|
||||
this.editingTaskId.set(null);
|
||||
this.editTitle.set('');
|
||||
this.editNotes.set('');
|
||||
this.editDueDate.set(this.selectedDate());
|
||||
}
|
||||
|
||||
protected saveEdit(task: UserTask): void {
|
||||
const title = this.editTitle().trim();
|
||||
|
||||
if (!title || this.updatingTaskId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatingTaskId.set(task.id);
|
||||
this.tasksService
|
||||
.updateTask(task.id, {
|
||||
title,
|
||||
notes: this.optionalText(this.editNotes()) ?? null,
|
||||
dueDate: this.editDueDate(),
|
||||
})
|
||||
.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.cancelEdit();
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected deleteTask(task: UserTask): void {
|
||||
if (this.deletingTaskId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deletingTaskId.set(task.id);
|
||||
this.tasksService
|
||||
.deleteTask(task.id)
|
||||
.pipe(finalize(() => this.deletingTaskId.set(null)))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.tasks.update((tasks) => tasks.filter((existingTask) => existingTask.id !== task.id));
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedDate.set(value);
|
||||
this.cancelEdit();
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
protected formatDay(value: string): string {
|
||||
const [year, month, day] = value.split('-');
|
||||
|
||||
return year && month && day ? `${day}.${month}.${year}` : value;
|
||||
}
|
||||
|
||||
private shiftSelectedDate(offsetDays: number): void {
|
||||
const date = new Date(`${this.selectedDate()}T00:00:00`);
|
||||
|
||||
date.setDate(date.getDate() + offsetDays);
|
||||
this.selectedDate.set(this.toDateInputValue(date));
|
||||
this.cancelEdit();
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
private replaceTask(task: UserTask): void {
|
||||
this.tasks.update((tasks) =>
|
||||
this.sortTasks(
|
||||
tasks.map((existingTask) => (existingTask.id === task.id ? task : existingTask)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private sortTasks(tasks: UserTask[]): UserTask[] {
|
||||
return [...tasks].sort((left, right) => {
|
||||
if (left.completed !== right.completed) {
|
||||
return left.completed ? 1 : -1;
|
||||
}
|
||||
|
||||
return new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
private optionalText(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
private todayKey(): string {
|
||||
return this.toDateInputValue(new Date());
|
||||
}
|
||||
|
||||
private toDateInputValue(date: Date): string {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
24
listify-client/src/app/tasks/tasks.models.ts
Normal file
24
listify-client/src/app/tasks/tasks.models.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface UserTask {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
title: string;
|
||||
notes?: string;
|
||||
dueDate: string;
|
||||
completed: boolean;
|
||||
completedAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
title: string;
|
||||
notes?: string;
|
||||
dueDate: string;
|
||||
}
|
||||
|
||||
export interface UpdateTaskRequest {
|
||||
title?: string;
|
||||
notes?: string | null;
|
||||
dueDate?: string;
|
||||
completed?: boolean;
|
||||
}
|
||||
28
listify-client/src/app/tasks/tasks.service.ts
Normal file
28
listify-client/src/app/tasks/tasks.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CreateTaskRequest, UpdateTaskRequest, UserTask } from './tasks.models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TasksService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly apiUrl = '/api/tasks';
|
||||
|
||||
listTasks(date?: string): Observable<UserTask[]> {
|
||||
const params = date ? new HttpParams().set('date', date) : undefined;
|
||||
|
||||
return this.http.get<UserTask[]>(this.apiUrl, { params });
|
||||
}
|
||||
|
||||
createTask(data: CreateTaskRequest): Observable<UserTask> {
|
||||
return this.http.post<UserTask>(this.apiUrl, data);
|
||||
}
|
||||
|
||||
updateTask(taskId: string, data: UpdateTaskRequest): Observable<UserTask> {
|
||||
return this.http.patch<UserTask>(`${this.apiUrl}/${taskId}`, data);
|
||||
}
|
||||
|
||||
deleteTask(taskId: string): Observable<{ message: string }> {
|
||||
return this.http.delete<{ message: string }>(`${this.apiUrl}/${taskId}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user