add edit delete and input forms

This commit is contained in:
Josef Seiringer 2025-10-28 22:37:36 +01:00
parent 8d964947d3
commit 57f9acfcdb
13 changed files with 538 additions and 64 deletions

View File

@ -1,5 +1,5 @@
{
"$schema": "@angular/cli/lib/config/schema.json",
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {

View File

@ -1,10 +1,14 @@
import { Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { RegisterComponent } from './components/register/register.component';
import { ListComponent } from './components/list/list.component';
import { CreateComponent } from './components/create/create.component';
export const routes: Routes = [
{ path: '', redirectTo: '/login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent},
{ path: 'register', component: RegisterComponent },
{ path: 'list', component: ListComponent },
{ path: 'create', component: CreateComponent },
{ path: '**', redirectTo: '/login' },
];

View File

@ -0,0 +1,98 @@
.create-card {
width: min(100% - 2rem, 700px);
}
.glass-card {
margin: 2rem auto;
padding: 1.25rem;
border-radius: 1rem;
background: rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #111;
}
.status {
padding: 0.75rem 1rem;
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.55);
margin-bottom: 0.75rem;
}
.status.error {
color: #8b0000;
background: rgba(255, 230, 230, 0.8);
}
.title {
margin: 0 0 1rem 0;
font-size: clamp(1.25rem, 1.5vw + 1rem, 2rem);
color: #000000b0;
font-weight: 700;
}
.form {
display: grid;
gap: 0.75rem;
}
.form-row {
display: grid;
gap: 0.25rem;
}
.form-row input {
padding: 0.6rem 0.75rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
font-size: 1rem;
transition:
background-color 120ms ease,
color 120ms ease,
border-color 120ms ease,
box-shadow 120ms ease;
}
/* Bessere Lesbarkeit beim Fokussieren */
.form-row input:focus,
.form-row input:focus-visible {
background: #ffffff;
color: #000000;
outline: none;
border-color: #000000;
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.08);
caret-color: #000000;
}
.form-row input::placeholder {
color: #666;
}
.form-row input:focus::placeholder {
color: #888;
}
.actions {
margin-top: 0.75rem;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.btn {
padding: 0.6rem 1rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
}
.btn.primary {
background: linear-gradient(135deg, #ff0048, #e03164);
color: #fff;
}
.btn.secondary {
background: rgba(255, 255, 255, 0.6);
}

View File

@ -0,0 +1,42 @@
<main class="login-container">
<div class="glass-card create-card">
<h2 class="title">Neuer Kantinen-Eintrag</h2>
<div *ngIf="errorMessage" class="status error">{{ errorMessage }}</div>
<form (ngSubmit)="save()" class="form">
<label class="form-row">
<span>Datum</span>
<input type="date" [(ngModel)]="byeDate" name="byeDate" required />
</label>
<label class="form-row">
<span>Betrag (€)</span>
<input
type="number"
step="0.01"
[(ngModel)]="betrag"
name="betrag"
required
/>
</label>
<label class="form-row">
<span>Belegname</span>
<input type="text" [(ngModel)]="belegName" name="belegName" />
</label>
<div class="actions">
<button
type="button"
class="btn secondary"
(click)="goToList()"
[disabled]="loading"
>
Abbrechen
</button>
<button type="submit" class="btn primary" [disabled]="loading">
{{ loading ? "Speichere…" : "Speichern" }}
</button>
</div>
</form>
</div>
</main>

View File

@ -0,0 +1,51 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { AppwriteService } from '../../services/appwrite.service';
@Component({
selector: 'app-create',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './create.component.html',
styleUrl: './create.component.css',
})
export class CreateComponent {
constructor(
private appwrite: AppwriteService,
private router: Router,
) {}
byeDate = '';
betrag: number | null = null;
belegName = '';
loading = false;
errorMessage: string | null = null;
async save(): Promise<void> {
if (!this.byeDate || this.betrag === null || Number.isNaN(this.betrag)) {
this.errorMessage = 'Bitte Datum und Betrag korrekt ausfüllen.';
return;
}
this.loading = true;
this.errorMessage = null;
try {
await this.appwrite.createDocument({
ByeDate: this.byeDate,
Betrag: this.betrag,
BelegName: this.belegName,
});
this.router.navigate(['/list']);
} catch (e: any) {
console.error(e);
this.errorMessage = e?.message ?? 'Fehler beim Speichern.';
} finally {
this.loading = false;
}
}
goToList(): void {
this.router.navigate(['/list']);
}
}

View File

@ -1,13 +0,0 @@
.headerText {
text-align: center;
color: rgb(42, 42, 88);
font-weight: bold;
font-size: 1.75rem;
font-family: "Libre Baskerville", serif;
}
i{
color: rgb(42, 42, 88);
cursor: pointer;
}

View File

@ -1,8 +0,0 @@
<div class="flex flex-row justify-between items-center bg-red-500/80 py-4 px-2">
<h2 class="headerText pl-4 ">Kegel Spielplan</h2>
<div>
<i class="fa-solid fa-file-lines fa-2xl pr-10" (click)="goToSummery()"></i>
<i class="fa-solid fa-file-circle-plus fa-2xl pr-10" (click)="goToInput()" ></i>
<i class="fa-solid fa-right-from-bracket fa-2xl pr-10" (click)="logout()"></i>
</div>
</div>

View File

@ -1,40 +0,0 @@
import { Component } from '@angular/core';
import { AppwriteService } from '../../services/appwrite.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-header',
standalone: true,
imports: [],
templateUrl: './header.component.html',
styleUrl: './header.component.css',
})
export class HeaderComponentComponent {
constructor(
private appwriteService: AppwriteService,
private routes: Router,
) {}
goToSummery() {
// this.routes.navigate(['/list']);
}
goToInput() {
// this.routes.navigate(['/input']);
}
async logout() {
try {
const accountService = await this.appwriteService.accountService;
await this.appwriteService.accountService.deleteSession(
(await accountService.getSession('current')).$id,
);
// Bei erfolgreicher Abmeldung
//this.toast.success('Logout erfolgreich!', 'Auf Wiedersehen!');
this.routes.navigate(['/login']);
} catch (err: any) {
console.error('Fehler beim Abmelden:', err.message);
}
}
}

View File

@ -0,0 +1,164 @@
/* Glas/Glassmorphism Card auf demselben Hintergrund wie Login (login-container liefert BG) */
.glass-card {
width: min(100% - 2rem, 1000px);
margin: 2rem auto;
padding: 1.25rem;
border-radius: 1rem;
background: rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #111;
}
.glass-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.glass-header h2 {
margin: 0;
font-size: clamp(1.25rem, 1.5vw + 1rem, 2rem);
color: #000000b0;
font-weight: 700;
}
/* Scrollbarer Bereich, der sich dynamisch anpasst */
.glass-scroll {
max-height: min(70vh, 800px);
overflow: auto;
}
/* Tabelle */
.list-table {
width: 100%;
border-collapse: collapse;
}
.list-table th,
.list-table td {
text-align: left;
padding: 0.75rem 0.5rem;
}
.list-table thead th {
position: sticky;
top: 0;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.list-table tbody tr:nth-child(even) {
background: rgba(255, 255, 255, 0.25);
}
.actions-col {
width: 1%;
white-space: nowrap;
}
.row-actions {
display: inline-flex;
gap: 0.35rem;
}
.icon-btn {
appearance: none;
border: none;
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
cursor: pointer;
background: rgba(255, 255, 255, 0.8);
color: #000;
transition:
transform 120ms ease,
background-color 120ms ease,
filter 120ms ease;
}
.icon-btn:hover {
transform: translateY(-1px);
filter: brightness(1.05);
}
.icon-btn.edit {
background: rgba(255, 255, 255, 0.85);
}
.icon-btn.delete {
background: #ff0048;
color: #fff;
}
.icon-btn.delete:hover {
background: #e03164;
}
.status {
padding: 0.75rem 1rem;
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.55);
}
.status.error {
color: #8b0000;
background: rgba(255, 230, 230, 0.8);
}
.empty {
padding: 1rem;
color: #333;
}
@media (max-width: 600px) {
.glass-card {
margin: 1rem auto;
padding: 1rem;
}
.list-table th,
.list-table td {
padding: 0.5rem;
}
}
/* Floating Action Button */
.fab {
position: fixed;
right: 1.25rem;
bottom: 1.25rem;
width: clamp(3rem, 4vw + 2rem, 4rem);
height: clamp(3rem, 4vw + 2rem, 4rem);
border-radius: 9999px;
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ff0048, #e03164);
color: #fff;
font-size: clamp(1.5rem, 2vw + 1rem, 2rem);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25);
cursor: pointer;
transition:
transform 0.15s ease,
box-shadow 0.15s ease,
filter 0.15s ease;
}
.fab:hover {
transform: translateY(-2px);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.3);
filter: brightness(1.05);
}
.fab:active {
transform: translateY(0);
}

View File

@ -0,0 +1,66 @@
<main class="login-container">
<div class="glass-card">
<div class="glass-header">
<h2>Kantinenliste</h2>
</div>
<div *ngIf="loading" class="status">Lade Daten…</div>
<div *ngIf="!loading && errorMessage" class="status error">
{{ errorMessage }}
</div>
<div *ngIf="!loading && !errorMessage" class="glass-scroll">
<table class="list-table" *ngIf="kantinList?.length; else noEntries">
<thead>
<tr>
<th>Datum</th>
<th>Betrag</th>
<th>Beleg</th>
<th class="actions-col">Aktionen</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let k of kantinList; trackBy: trackById">
<td>{{ k.szByeDate }}</td>
<td>{{ k.mnBetrag | number: "1.2-2" }} €</td>
<td>{{ k.szBelegName }}</td>
<td>
<div class="row-actions">
<button
type="button"
class="icon-btn edit"
(click)="onEdit(k)"
title="Bearbeiten"
aria-label="Bearbeiten"
>
<i class="fa-solid fa-pen"></i>
</button>
<button
type="button"
class="icon-btn delete"
(click)="onDelete(k)"
title="Löschen"
aria-label="Löschen"
>
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<ng-template #noEntries>
<div class="empty">Keine Einträge vorhanden.</div>
</ng-template>
</div>
</div>
<!-- Floating Action Button für neuen Eintrag -->
<button
class="fab"
(click)="goToCreate()"
aria-label="Neuen Eintrag hinzufügen"
title="Neuen Eintrag hinzufügen"
>
<span>+</span>
</button>
</main>

View File

@ -0,0 +1,91 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppwriteService } from '../../services/appwrite.service';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Kantin } from '../../models/kantin.model';
import { firstValueFrom } from 'rxjs';
import { Query } from 'appwrite';
@Component({
selector: 'app-list',
imports: [CommonModule, FormsModule],
templateUrl: './list.component.html',
styleUrl: './list.component.css',
})
export class ListComponent implements OnInit {
constructor(
private router: Router,
private appwriteService: AppwriteService,
) {}
public kantinList: Kantin[] = [];
public loading = false;
public errorMessage: string | null = null;
ngOnInit(): void {
// Async Logik in eigene Methode ausgelagert
void this.loadRecords();
}
private async loadRecords(): Promise<void> {
this.loading = true;
this.errorMessage = null;
try {
// getRecords() liefert ein Observable -> mit firstValueFrom einmalig auflösen
// Serverseitiges Sortieren: neueste Einträge zuerst nach ByeDate
const documents = await firstValueFrom(
this.appwriteService.getRecords([Query.orderDesc('ByeDate')]),
);
this.kantinList = (documents ?? []).map(
(doc: any) =>
new Kantin(
doc?.$id ?? '',
doc?.ByeDate ?? '',
Number(doc?.Betrag ?? 0),
doc?.BelegName ?? '',
),
);
} catch (error: any) {
console.error('Fehler beim Laden der Kantin-Einträge:', error);
this.errorMessage = error?.message ?? 'Unbekannter Fehler beim Laden.';
} finally {
this.loading = false;
}
}
// trackBy für *ngFor Performance
trackById(index: number, item: Kantin): string {
return item.szDocumentId ?? index.toString();
}
// Navigation zu einer (noch zu erstellenden) Create-Ansicht
goToCreate(): void {
this.router.navigate(['/create']);
}
onEdit(item: Kantin): void {
// TODO: Edit-Ansicht bauen. Bis dahin könnte Create mit Prefill genutzt werden.
this.router.navigate(['/create'], {
queryParams: {
id: item.szDocumentId,
date: item.szByeDate,
amount: item.mnBetrag,
name: item.szBelegName,
},
});
}
async onDelete(item: Kantin): Promise<void> {
const confirmed = window.confirm('Diesen Eintrag wirklich löschen?');
if (!confirmed) return;
try {
await this.appwriteService.deleteDocument(item.szDocumentId);
await this.loadRecords();
} catch (e) {
console.error('Löschen fehlgeschlagen', e);
this.errorMessage = 'Löschen fehlgeschlagen.';
}
}
}

View File

@ -52,7 +52,7 @@ detailHeight: number = 0;
this.sessionId = response.$id;
this.email = '';
this.password = '';
//this.router.navigate(['/input']);
this.router.navigate(['/list']);
}).catch((error) => {
console.error('Fehler beim Anmelden: ' + error.message);
alert('Fehler beim Anmelden: ' + error.message);

View File

@ -0,0 +1,19 @@
export class Kantin {
public szDocumentId: string;
public szByeDate: string;
public mnBetrag: number;
public szBelegName: string;
constructor(
documentId: string,
byeDate: string,
betrag: number,
belegName: string
) {
this.szDocumentId = documentId;
this.szByeDate = byeDate;
this.mnBetrag = betrag;
this.szBelegName = belegName;
}
}