add edit delete and input forms
This commit is contained in:
parent
8d964947d3
commit
57f9acfcdb
@ -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": {
|
||||
|
||||
@ -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' },
|
||||
];
|
||||
|
||||
98
src/app/components/create/create.component.css
Normal file
98
src/app/components/create/create.component.css
Normal 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);
|
||||
}
|
||||
42
src/app/components/create/create.component.html
Normal file
42
src/app/components/create/create.component.html
Normal 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>
|
||||
51
src/app/components/create/create.component.ts
Normal file
51
src/app/components/create/create.component.ts
Normal 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']);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/app/components/list/list.component.css
Normal file
164
src/app/components/list/list.component.css
Normal 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);
|
||||
}
|
||||
66
src/app/components/list/list.component.html
Normal file
66
src/app/components/list/list.component.html
Normal 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>
|
||||
91
src/app/components/list/list.component.ts
Normal file
91
src/app/components/list/list.component.ts
Normal 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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
19
src/app/models/kantin.model.ts
Normal file
19
src/app/models/kantin.model.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user