data display

This commit is contained in:
Tizian.Breuch
2025-09-10 13:03:13 +02:00
parent 37bafcddf0
commit df7e249a62
18 changed files with 474 additions and 1486 deletions

View File

@@ -34,10 +34,7 @@ export const routes: Routes = [
title: 'Zugriff verweigert', title: 'Zugriff verweigert',
}, },
// Regel 3: Fallback-Route (Wildcard)
// JEDE ANDERE URL, die nicht von den obigen Regeln abgefangen wurde,
// wird als ungültig betrachtet und ebenfalls zum Auth-Feature (und von dort zum Login) umgeleitet.
// Das stellt sicher, dass Benutzer immer auf einer validen Seite landen.
{ {
path: '**', path: '**',
component: NotFoundComponent, component: NotFoundComponent,

View File

@@ -1,4 +1,4 @@
<div class="not-found-container"> <!-- <div class="not-found-container">
<div class="not-found-box"> <div class="not-found-box">
<div class="status-code">404</div> <div class="status-code">404</div>
@@ -13,11 +13,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
<span>Zurück</span> <span>Zurück</span>
</button> </button>
<button class="btn btn-primary" routerLink="/"> <!-- Link zur Startseite der App --> <button class="btn btn-primary" routerLink="/">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span>Zur Startseite</span> <span>Zur Startseite</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div> -->
<p>404</p>

View File

@@ -1,265 +1,19 @@
<!-- ================================================================================= <div class="dashboard-grid">
FINALES & VOLLSTÄNDIGES DEMO-LAYOUT (VERSION 5.0) <app-kpi-card
Aufgebaut aus wiederverwendbaren Komponenten *ngFor="let kpi of kpiData"
================================================================================== --> [value]="kpi.value"
<div class="dashboard-container"> [label]="kpi.label"
<!-- =================================================================== --> [color]="kpi.color"
<!-- 1. SIDEBAR (Statisch, da sehr spezifisch für dieses Layout) --> [iconName]="kpi.iconName"
<!-- =================================================================== -->
<app-sidebar></app-sidebar>
<!-- =================================================================== -->
<!-- 2. HAUPTINHALT -->
<!-- =================================================================== -->
<main class="main-content">
<app-page-header></app-page-header>
<div class="dashboard-grid">
<app-kpi-card
value="€ 14.750"
label="Umsatz"
color="green"
iconName="money"
></app-kpi-card>
<app-kpi-card
value="1.284"
label="Neue Nutzer"
color="blue"
iconName="group"
></app-kpi-card>
<app-kpi-card
value="312"
label="Bestellungen"
color="orange"
iconName="shopping_bag"
></app-kpi-card>
<app-kpi-card
value="99.8%"
label="Verfügbarkeit"
color="purple"
iconName="bolt"
></app-kpi-card>
<app-card class="grid-col-span-2">
<h3 class="card-header">Umsatzentwicklung</h3>
<div class="chart-container">
<div class="chart-bar" style="height: 60%"><span>Jan</span></div>
<div class="chart-bar" style="height: 75%"><span>Feb</span></div>
<div class="chart-bar" style="height: 50%"><span>Mär</span></div>
<div class="chart-bar" style="height: 85%"><span>Apr</span></div>
<div class="chart-bar" style="height: 90%"><span>Mai</span></div>
<div class="chart-bar" style="height: 65%"><span>Jun</span></div>
</div>
</app-card>
<app-card class="grid-col-span-2">
<h3 class="card-header">Letzte Bestellungen</h3>
<app-orders-table
[orders]="ordersData"
(deleteOrder)="handleDeleteOrder($event)"
>
</app-orders-table>
<app-paginator
[currentPage]="currentPage"
[totalItems]="totalItems"
[itemsPerPage]="itemsPerPage"
(pageChange)="onPageChange($event)"
></app-paginator>
</app-card>
<!-- UI Komponenten Showcase -->
<app-card class="grid-col-span-2">
<h3 class="card-header">Moderne Formular-Elemente</h3>
<div class="component-grid">
<app-form-field label="Name" type="text"></app-form-field>
<app-form-select label="Stadt" [options]="cityOptions" ></app-form-select>
<app-form-textarea label="Nachricht"></app-form-textarea>
<app-slide-toggle></app-slide-toggle>
</div>
</app-card>
<app-card class="grid-col-span-2">
<h3 class="card-header">Buttons, Chips & Interaktion</h3>
<div class="component-grid">
<div class="button-group">
<app-button buttonType="primary" tooltip="Primary"
>Primary</app-button
>
<app-button buttonType="secondary">Secondary</app-button>
<app-button buttonType="stroked">Stroked</app-button>
<app-button buttonType="flat">Flat</app-button>
<app-button
buttonType="icon"
tooltip="Favorit hinzufügen"
iconName="favorite"
svgColor="red"
></app-button>
<app-button
buttonType="icon"
tooltip="Details anzeigen"
iconName="eye"
></app-button>
</div>
<div class="chip-set">
<app-chip label="Technologie" [removable]="true"></app-chip>
<app-chip
label="Angular"
[active]="true"
[removable]="true"
></app-chip>
</div>
<div class="badge-showcase">
<div class="badge-container">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg
><span class="badge-dot">3</span>
</div>
</div>
<app-menu [items]="actionMenuItems"></app-menu>
</div>
</app-card>
<app-card class="grid-col-span-2">
<h3 class="card-header">Indikatoren & Feedback</h3>
<div class="card-body component-grid">
<app-button buttonType="secondary" (click)="triggerSnackbar()"
>Snackbar anzeigen</app-button
>
<app-alert type="success"
><strong>Erfolg!</strong> Ihre Änderungen wurden
gespeichert.</app-alert
>
<app-alert type="danger"
><strong>Fehler!</strong> Bitte füllen Sie alle Felder
aus.</app-alert
>
</div>
</app-card>
<app-card class="grid-col-span-2">
<h3 class="card-header">Navigation & Layout</h3>
<div class="component-grid">
<div class="tab-group">
<button class="tab-link active">Profil</button
><button class="tab-link">Konto</button>
</div>
<div class="form-group-inline">
<label>Lautstärke</label
><input
type="range"
class="form-slider"
min="0"
max="100"
value="50"
/>
</div>
<app-expansion-panel title="Weitere Details anzeigen">
Dieser Inhalt wird dynamisch ein- und ausgeblendet. Lorem ipsum
dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada
lacus ex, sit amet blandit leo lobortis eget.
</app-expansion-panel>
<div class="stepper">
<div class="step step-complete">
<div class="step-icon"></div>
<span>Profil</span>
</div>
<div class="step-connector"></div>
<div class="step step-active">
<div class="step-icon">2</div>
<span>Konto</span>
</div>
<div class="step-connector"></div>
<div class="step">
<div class="step-icon">3</div>
<span>Abschluss</span>
</div>
</div>
</div>
</app-card>
<app-card class="grid-col-span-2">
<h3 class="card-header">Ladezustände (Skeleton)</h3>
<div *ngIf="isLoading" class="component-grid">
<div class="skeleton-card card">
<app-skeleton type="avatar"></app-skeleton>
<div class="skeleton-content">
<app-skeleton type="line"></app-skeleton
><app-skeleton type="line" width="70%"></app-skeleton>
</div>
</div>
<div class="skeleton-card card">
<app-skeleton type="avatar"></app-skeleton>
<div class="skeleton-content">
<app-skeleton type="line" width="50%"></app-skeleton
><app-skeleton type="line" width="80%"></app-skeleton>
</div>
</div>
</div>
<div class="card-body" *ngIf="!isLoading">
<app-alert type="success"
><strong>Inhalt erfolgreich geladen!</strong></app-alert
>
</div>
</app-card>
</div>
</main>
<!-- 3. OVERLAYS -->
<app-dialog
[isOpen]="isDialogOpen"
(close)="closeDialog()"
title="Bestellung löschen?"
> >
<div dialog-content> </app-kpi-card>
<div class="dialog-icon-container">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<p>Sind Sie sicher? Diese Aktion kann nicht rückgängig gemacht werden.</p>
</div>
<div dialog-actions>
<app-button buttonType="stroked" (click)="closeDialog()"
>Abbrechen</app-button
>
<app-button buttonType="primary" (click)="closeDialog()"
>Ja, endgültig löschen</app-button
>
</div>
</app-dialog>
</div> </div>
<app-orders-table
[data]="ordersData"
[itemsPerPage]="5"
(view)="handleViewDetails($event)"
(edit)="handleEditOrder($event)"
(delete)="handleDeleteOrder($event)"
>
</app-orders-table>

View File

@@ -1,147 +1,157 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SnackbarService } from '../../../../shared/services/snackbar.service';
// Importieren ALLER wiederverwendbaren Komponenten, die im Template genutzt werden
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { CardComponent } from '../../../../shared/components/ui/card/card.component'; import { CardComponent } from '../../../../shared/components/ui/card/card.component';
import { ChipComponent } from '../../../../shared/components/ui/chip/chip.component'; // Wir müssen KpiColor hier importieren, um es als Typ verwenden zu können
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
import { KpiCardComponent } from '../../../../shared/components/data-display/kpi-card/kpi-card.component';
import { PageHeaderComponent } from '../../../../shared/components/layout/page-header/page-header.component';
import { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component';
import { StatusPillComponent } from '../../../../shared/components/ui/status-pill/status-pill.component';
import { DialogComponent } from '../../../../shared/components/overlays/dialog/dialog.component';
import { import {
MenuComponent, KpiCardComponent,
MenuItem, KpiColor,
} from '../../../../shared/components/overlays/menu/menu.component'; } from '../../../../shared/components/data-display/kpi-card/kpi-card.component';
import { AlertComponent } from '../../../../shared/components/ui/alert/alert.component'; import {
OrdersTableComponent,
Order,
} from '../../../../shared/components/data-display/orders-table/orders-table.component';
import { PaginatorComponent } from '../../../../shared/components/data-display/paginator/paginator.component'; import { PaginatorComponent } from '../../../../shared/components/data-display/paginator/paginator.component';
import { SkeletonComponent } from '../../../../shared/components/ui/skeleton/skeleton.component';
import { ExpansionPanelComponent } from '../../../../shared/components/layout/expansion-panel/expansion-panel.component';
import { SidebarComponent } from '../../../../shared/components/layout/sidebar/sidebar.component';
import { OrdersTableComponent, Order } from '../../../../shared/components/data-display/orders-table/orders-table.component'; // Wir definieren ein Interface für unsere KPI-Daten für Typsicherheit
import { FormSelectComponent, SelectOption } from '../../../../shared/components/form/form-select/form-select.component'; interface Kpi {
import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component'; value: string;
label: string;
color: KpiColor; // <-- Hier verwenden wir den importierten Typ
iconName: string;
}
@Component({ @Component({
selector: 'app-demo2', selector: 'app-demo2',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
ButtonComponent,
CardComponent, CardComponent,
ChipComponent,
FormFieldComponent,
KpiCardComponent, KpiCardComponent,
PageHeaderComponent,
SlideToggleComponent,
StatusPillComponent,
DialogComponent,
MenuComponent,
AlertComponent,
PaginatorComponent,
SkeletonComponent,
ExpansionPanelComponent,
SidebarComponent,
OrdersTableComponent, OrdersTableComponent,
FormSelectComponent, PaginatorComponent,
FormTextareaComponent
], ],
templateUrl: './demo2.component.html', templateUrl: './demo2.component.html',
styleUrl: './demo2.component.css',
}) })
export class Demo2Component implements OnInit { export class Demo2Component {
// Zustands-Variablen für diese Demo-Seite kpiData: Kpi[] = [
isLoading = true;
isDialogOpen = false;
// isComponentMenuOpen = false; // <--- DIESE ZEILE ENTFERNEN, DA SIE NICHT MEHR BENÖTIGT WIRD
// Eigenschaften für den Paginator
currentPage = 1;
totalItems = 123;
itemsPerPage = 10;
actionMenuItems: MenuItem[] = [
{ {
label: 'Bearbeiten', value: '€ 14.750',
action: () => this.snackbarService.show('Bearbeiten geklickt!'), label: 'Umsatz',
color: 'green',
iconName: 'placeholder',
}, },
{ {
label: 'Kopieren', value: '1.284',
action: () => console.log('Kopieren geklickt!'), label: 'Neue Nutzer',
color: 'blue',
iconName: 'placeholder',
}, },
{ {
dividerBefore: true, value: '312',
label: 'Löschen', label: 'Bestellungen',
action: () => this.openDialog(), // Öffnet z.B. den Bestätigungs-Dialog color: 'orange',
isDanger: true, iconName: 'placeholder',
},
{
value: '99.8%',
label: 'Verfügbarkeit',
color: 'purple',
iconName: 'placeholder',
}, },
]; ];
ordersData: Order[] = [ ordersData: Order[] = [
{ {
id: '10543', id: '10543',
user: { name: 'Max Mustermann', email: 'max.mustermann@example.com', avatarUrl: 'https://i.pravatar.cc/40?u=max' }, user: {
name: 'Max Mustermann',
email: 'max.m@example.com',
avatarUrl: 'https://i.pravatar.cc/40?u=max',
},
amount: '€ 129,99', amount: '€ 129,99',
status: 'success', status: 'success',
statusText: 'Abgeschlossen' statusText: 'Abgeschlossen',
}, },
{ {
id: '10542', id: '10542',
user: { name: 'Erika Mustermann', email: 'erika.m@example.com', avatarUrl: 'https://i.pravatar.cc/40?u=erika' }, user: {
name: 'Erika Mustermann',
email: 'erika.m@example.com',
avatarUrl: 'https://i.pravatar.cc/40?u=erika',
},
amount: '€ 49,50', amount: '€ 49,50',
status: 'warning', status: 'warning',
statusText: 'In Bearbeitung' statusText: 'In Bearbeitung',
}, },
{ {
id: '10541', id: '10541',
user: { name: 'Peter Pan', email: 'peter.pan@example.com', avatarUrl: 'https://i.pravatar.cc/40?u=peter' }, user: {
name: 'Peter Pan',
email: 'peter.p@example.com',
avatarUrl: 'https://i.pravatar.cc/40?u=peter',
},
amount: '€ 87,00', amount: '€ 87,00',
status: 'danger', status: 'danger',
statusText: 'Storniert' statusText: 'Storniert',
} },
{
id: '10543',
user: {
name: 'Max Mustermann',
email: 'max.m@example.com',
avatarUrl: 'https://i.pravatar.cc/40?u=max',
},
amount: '€ 129,99',
status: 'success',
statusText: 'Abgeschlossen',
},
{
id: '10542',
user: {
name: 'Erika Mustermann',
email: 'erika.m@example.com',
avatarUrl: 'https://i.pravatar.cc/40?u=erika',
},
amount: '€ 49,50',
status: 'warning',
statusText: 'In Bearbeitung',
},
{
id: '10541',
user: {
name: 'Peter Pan',
email: 'peter.p@example.com',
avatarUrl: 'https://i.pravatar.cc/40?u=peter',
},
amount: '€ 87,00',
status: 'danger',
statusText: 'Storniert',
},
]; ];
cityOptions: SelectOption[] = [ // Eigenschaften für den Paginator
{ value: 'berlin', label: 'Berlin' }, currentPage = 1;
{ value: 'munich', label: 'München' }, totalItems = this.ordersData.length;
{ value: 'hamburg', label: 'Hamburg' } itemsPerPage = 10;
];
// --- EVENT-HANDLER FÜR DIE KIND-KOMPONENTEN ---
constructor(private snackbarService: SnackbarService) {}
ngOnInit(): void {
setTimeout(() => {
this.isLoading = false;
}, 2000);
}
// Methoden für den Dialog
openDialog(): void {
this.isDialogOpen = true;
}
closeDialog(): void {
this.isDialogOpen = false;
}
// Methode für den Snackbar-Trigger
triggerSnackbar(): void {
const time = new Date().toLocaleTimeString();
this.snackbarService.show('Neues Ereignis um ' + time);
}
// Methode für den Paginator
onPageChange(newPage: number): void { onPageChange(newPage: number): void {
this.currentPage = newPage; this.currentPage = newPage;
console.log('Wechsle zu Seite:', newPage); console.log('Seite gewechselt zu:', newPage);
// Hier würde die Logik zum Neuladen der Tabellendaten folgen // In einer echten Anwendung würden Sie hier die Daten neu laden
} }
handleDeleteOrder(orderId: string) { handleDeleteOrder(orderId: string): void {
console.log('Lösche Bestellung mit ID:', orderId); console.log('Lösche Bestellung mit ID:', orderId);
this.openDialog(); // Öffnet z.B. den globalen Dialog // Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen
}
handleViewDetails(orderId: string): void {
console.log('View Bestellung mit ID:', orderId);
// Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen
}
handleEditOrder(orderId: string): void {
console.log('Edit Bestellung mit ID:', orderId);
// Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen
} }
} }

View File

@@ -1,166 +0,0 @@
/* WICHTIG: Diese Stile sollten aus der globalen styles.css HIERHER VERSCHOBEN werden. */
:host {
display: block;
}
.kpi-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
}
.kpi-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: grid;
place-items: center;
flex-shrink: 0;
background-color: var(--color-surface);
}
.kpi-icon app-icon {
width: 24px;
height: 24px;
color: #fff;
}
.icon-blue {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.icon-green {
background: linear-gradient(135deg, #2ecc71, #27ae60);
}
.icon-orange {
background: linear-gradient(135deg, #f39c12, #f1c40f);
}
.icon-purple {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
}
.kpi-content {
display: flex;
flex-direction: column;
}
.kpi-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
}
.kpi-label {
font-size: 0.9rem;
color: var(--color-text-light);
}
/* Verschieben Sie alle Tabellen-Stile aus styles.css hierher */
:host {
display: block;
}
.table-container {
width: 100%;
overflow-x: auto;
}
.modern-table {
width: 100%;
border-collapse: collapse;
white-space: nowrap;
}
.modern-table thead th {
padding: 0.75rem 1.5rem;
color: var(--color-text-light);
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: left;
border-bottom: 2px solid var(--color-border);
}
.modern-table th.sortable {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.modern-table th.sortable:hover {
color: var(--color-text);
}
.modern-table .sort-icon {
opacity: 0.5;
transition: all var(--transition-speed);
}
.modern-table th.sortable:hover .sort-icon {
opacity: 1;
}
.modern-table tbody tr {
transition: background-color var(--transition-speed);
border-bottom: 1px solid var(--color-border);
}
.modern-table tbody tr:last-of-type {
border-bottom: none;
}
.modern-table tbody tr:hover {
background-color: var(--color-body-bg);
}
.modern-table tbody td {
padding: 1rem 1.5rem;
vertical-align: middle;
}
.user-cell {
display: flex;
align-items: center;
gap: 1rem;
}
.user-cell img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-weight: 600;
color: var(--color-text);
}
.user-email {
font-size: 0.9rem;
color: var(--color-text-light);
}
.amount { font-weight: 600; }
.mono { font-family: 'Courier New', Courier, monospace; }
.actions-cell {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.no-data-cell {
text-align: center;
padding: 2rem;
color: var(--color-text-light);
}
/* Verschieben Sie diese Stile aus der globalen styles.css hierher */
:host {
display: block;
}
.paginator {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 1.5rem;
padding: 1rem;
border-top: 1px solid var(--color-border);
}
.paginator-info {
font-size: 0.9rem;
color: var(--color-text-light);
font-weight: 500;
}
.paginator-controls {
display: flex;
gap: 0.5rem;
}

View File

@@ -0,0 +1,56 @@
:host {
display: block;
}
/* Die .kpi-card-Regel wird jetzt auf den Host angewendet, da das Template nur den Inhalt enthält */
:host .kpi-card-content {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
}
.kpi-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: grid;
place-items: center;
flex-shrink: 0;
}
.kpi-icon app-icon {
width: 24px;
height: 24px;
color: #fff; /* Standardfarbe für das Icon (currentColor) */
}
/* Farbvarianten für den Hintergrund des Icons */
.icon-blue {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.icon-green {
background: linear-gradient(135deg, #2ecc71, #27ae60);
}
.icon-orange {
background: linear-gradient(135deg, #f39c12, #f1c40f);
}
.icon-purple {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
}
.kpi-content {
display: flex;
flex-direction: column;
}
.kpi-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
}
.kpi-label {
font-size: 0.9rem;
color: var(--color-text-light);
}

View File

@@ -1,4 +1,4 @@
<div class="card kpi-card"> <app-card>
<div <div
class="kpi-icon" class="kpi-icon"
[class.icon-blue]="color === 'blue'" [class.icon-blue]="color === 'blue'"
@@ -8,7 +8,7 @@
> >
<app-icon <app-icon
*ngIf="iconName" *ngIf="iconName"
[name]="iconName" [iconName]="iconName"
[svgColor]="svgColor" [svgColor]="svgColor"
></app-icon> ></app-icon>
</div> </div>
@@ -16,4 +16,4 @@
<span class="kpi-value">{{ value }}</span> <span class="kpi-value">{{ value }}</span>
<span class="kpi-label">{{ label }}</span> <span class="kpi-label">{{ label }}</span>
</div> </div>
</div> </app-card>

View File

@@ -1,15 +1,16 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { IconComponent } from '../../ui/icon/icon.component'; import { IconComponent } from '../../ui/icon/icon.component';
import { CardComponent } from '../../ui/card/card.component';
type KpiColor = 'blue' | 'green' | 'orange' | 'purple'; export type KpiColor = 'blue' | 'green' | 'orange' | 'purple';
@Component({ @Component({
selector: 'app-kpi-card', selector: 'app-kpi-card',
standalone: true, standalone: true,
imports: [CommonModule, IconComponent], imports: [CommonModule, IconComponent,CardComponent],
templateUrl: './kpi-card.component.html', templateUrl: './kpi-card.component.html',
styleUrl: '../data-display.css', styleUrl: './kpi-card.component.css',
}) })
export class KpiCardComponent { export class KpiCardComponent {
@Input() value: string = ''; @Input() value: string = '';

View File

@@ -0,0 +1,104 @@
/* Das Host-Element der Komponente wird zum Flex-Container, um das Layout zu steuern */
:host {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden; /* Verhindert, dass die Komponente selbst überläuft */
}
/* Der Container für die Tabelle wird zum scrollbaren Bereich */
.table-container {
overflow-x: auto;
flex-grow: 1;
min-width: 0; /* Erlaubt dem Flex-Item, kleiner als sein Inhalt zu werden */
}
.modern-table {
width: 100%;
border-collapse: collapse;
white-space: nowrap;
}
.modern-table thead th {
padding: 0.75rem 1.5rem;
color: var(--color-text-light);
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: left;
border-bottom: 2px solid var(--color-border);
}
.modern-table th.sortable {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.modern-table th.sortable:hover {
color: var(--color-text);
}
.modern-table tbody tr {
transition: background-color var(--transition-speed);
border-bottom: 1px solid var(--color-border);
}
.modern-table tbody tr:last-of-type {
border-bottom: none;
}
.modern-table tbody tr:hover {
background-color: var(--color-body-bg-hover);
}
.modern-table tbody td {
padding: 1rem 1.5rem;
vertical-align: middle;
}
.user-cell {
display: flex;
align-items: center;
gap: 1rem;
}
.user-cell img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-weight: 600;
color: var(--color-text);
}
.user-email {
font-size: 0.9rem;
color: var(--color-text-light);
}
.amount {
font-weight: 600;
text-align: right;
}
.mono {
font-family: 'Courier New', Courier, monospace;
}
.actions-cell {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.no-data-cell {
text-align: center;
padding: 2rem;
color: var(--color-text-light);
}

View File

@@ -2,10 +2,7 @@
<table class="modern-table"> <table class="modern-table">
<thead> <thead>
<tr> <tr>
<th class="sortable"> <th>Kunde</th>
<span>Kunde</span>
<app-icon name="chevron-down"></app-icon>
</th>
<th>Bestell-ID</th> <th>Bestell-ID</th>
<th>Status</th> <th>Status</th>
<th class="text-right">Betrag</th> <th class="text-right">Betrag</th>
@@ -14,46 +11,64 @@
</thead> </thead>
<tbody> <tbody>
<!-- Schleife, die über die übergebenen Bestelldaten iteriert --> <!-- Schleife, die über die übergebenen Bestelldaten iteriert -->
<tr *ngFor="let order of orders"> <tr *ngFor="let order of displayedOrders">
<td> <td>
<div class="user-cell"> <div class="user-cell">
<img [src]="order.user.avatarUrl" [alt]="'Avatar von ' + order.user.name" /> <img
[src]="order.user.avatarUrl"
[alt]="'Avatar von ' + order.user.name"
/>
<div> <div>
<div class="user-name">{{ order.user.name }}</div> <div class="user-name">{{ order.user.name }}</div>
<div class="user-email">{{ order.user.email }}</div> <div class="user-email">{{ order.user.email }}</div>
</div> </div>
</div> </div>
</td> </td>
<td><span class="mono">#{{ order.id }}</span></td>
<td> <td>
<!-- <app-status-pill [status]="order.status">{{ order.statusText }}</app-status-pill> --> <span class="mono">#{{ order.id }}</span>
</td> </td>
<td class="text-right amount">{{ order.amount }}</td> <td>
<app-status-pill [status]="order.status">{{
order.statusText
}}</app-status-pill>
</td>
<td class="amount">{{ order.amount }}</td>
<td class="actions-cell"> <td class="actions-cell">
<app-button <app-button
buttonType="icon" buttonType="icon"
tooltip="Details anzeigen" tooltip="Details anzeigen"
iconName="eye" iconName="placeholder"
(click)="viewDetails.emit(order.id)"> (click)="view.emit(order.id)"
>
</app-button> </app-button>
<app-button <app-button
buttonType="icon" buttonType="icon"
tooltip="Bearbeiten" tooltip="Bearbeiten"
iconName="edit" iconName="placeholder"
(click)="editOrder.emit(order.id)"> (click)="edit.emit(order.id)"
>
</app-button> </app-button>
<app-button <app-button
buttonType="icon-danger" buttonType="icon-danger"
tooltip="Löschen" tooltip="Löschen"
iconName="trash-2" iconName="placeholder"
(click)="deleteOrder.emit(order.id)"> (click)="delete.emit(order.id)"
>
</app-button> </app-button>
</td> </td>
</tr> </tr>
<!-- Fallback, wenn keine Daten vorhanden sind --> <tr *ngIf="!displayedOrders || displayedOrders.length === 0">
<tr *ngIf="!orders || orders.length === 0">
<td colspan="5" class="no-data-cell">Keine Bestellungen gefunden.</td> <td colspan="5" class="no-data-cell">Keine Bestellungen gefunden.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div>
<app-paginator
[currentPage]="currentPage"
[totalItems]="data.length"
[itemsPerPage]="itemsPerPage"
(pageChange)="onPageChange($event)"
>
</app-paginator>
</div>

View File

@@ -1,8 +1,8 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { StatusPillComponent } from '../../ui/status-pill/status-pill.component'; import { StatusPillComponent } from '../../ui/status-pill/status-pill.component';
import { ButtonComponent } from '../../ui/button/button.component'; import { ButtonComponent } from '../../ui/button/button.component';
import { IconComponent } from '../../ui/icon/icon.component'; import { PaginatorComponent } from '../paginator/paginator.component';
// Interfaces für die Datenstruktur // Interfaces für die Datenstruktur
export interface OrderUser { export interface OrderUser {
@@ -24,19 +24,44 @@ export interface Order {
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
StatusPillComponent,
ButtonComponent, ButtonComponent,
IconComponent PaginatorComponent
], ],
templateUrl: './orders-table.component.html', templateUrl: './orders-table.component.html',
styleUrl: '../data-display.css' styleUrl: './orders-table.component.css'
}) })
export class OrdersTableComponent { export class OrdersTableComponent {
// Nimmt die anzuzeigenden Bestelldaten entgegen // Nimmt die anzuzeigenden Bestelldaten entgegen
@Input() orders: Order[] = []; @Input() data: Order[] = [];
@Input() itemsPerPage = 5;
currentPage = 1;
displayedOrders: Order[] = [];
// Gibt Events für die Aktionen aus, mit der ID der betroffenen Bestellung // Gibt Events für die Aktionen aus, mit der ID der betroffenen Bestellung
@Output() viewDetails = new EventEmitter<string>(); @Output() view = new EventEmitter<string>();
@Output() editOrder = new EventEmitter<string>(); @Output() edit = new EventEmitter<string>();
@Output() deleteOrder = new EventEmitter<string>(); @Output() delete = new EventEmitter<string>();
ngOnChanges(changes: SimpleChanges): void {
// Wenn sich die Eingabedaten (allOrders) ändern, aktualisieren wir die Ansicht
if (changes['data']) {
this.updateDisplayedOrders();
}
}
// Wird aufgerufen, wenn der Paginator die Seite wechselt
onPageChange(newPage: number): void {
this.currentPage = newPage;
this.updateDisplayedOrders();
}
// Diese Methode berechnet, welcher Teil der Daten angezeigt werden soll
private updateDisplayedOrders(): void {
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
this.displayedOrders = this.data.slice(startIndex, endIndex);
}
} }

View File

@@ -0,0 +1,26 @@
:host {
display: block;
/* Sorgt dafür, dass der Paginator im Flex-Layout der Tabelle nicht schrumpft */
flex-shrink: 0;
}
.paginator {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1rem;
border-top: 1px solid var(--color-border);
}
.paginator-info {
font-size: 0.9rem;
color: var(--color-text-light);
font-weight: 500;
/* Schiebt diesen Text und die Controls garantiert nach rechts */
margin-left: auto;
}
.paginator-controls {
display: flex;
gap: 0.5rem;
}

View File

@@ -7,7 +7,7 @@ import { ButtonComponent } from '../../ui/button/button.component';
standalone: true, standalone: true,
imports: [CommonModule, ButtonComponent], imports: [CommonModule, ButtonComponent],
templateUrl: './paginator.component.html', templateUrl: './paginator.component.html',
styleUrl: '../data-display.css' styleUrl: './paginator.component.css',
}) })
export class PaginatorComponent { export class PaginatorComponent {
// --- EINGABEN von außen --- // --- EINGABEN von außen ---
@@ -26,7 +26,7 @@ export class PaginatorComponent {
get rangeStart(): number { get rangeStart(): number {
return (this.currentPage - 1) * this.itemsPerPage + 1; return (this.currentPage - 1) * this.itemsPerPage + 1;
} }
get rangeEnd(): number { get rangeEnd(): number {
const end = this.currentPage * this.itemsPerPage; const end = this.currentPage * this.itemsPerPage;
return end > this.totalItems ? this.totalItems : end; return end > this.totalItems ? this.totalItems : end;
@@ -43,4 +43,4 @@ export class PaginatorComponent {
this.pageChange.emit(this.currentPage + 1); this.pageChange.emit(this.currentPage + 1);
} }
} }
} }

View File

@@ -10,6 +10,6 @@
[class.btn-icon-danger]="buttonType === 'icon-danger'" [class.btn-icon-danger]="buttonType === 'icon-danger'"
[class.btn-full-width]="fullWidth" [class.btn-full-width]="fullWidth"
> >
<app-icon *ngIf="iconName" [name]="iconName" [svgColor]="svgColor"></app-icon> <app-icon *ngIf="iconName" [iconName]="iconName" [svgColor]="svgColor"></app-icon>
<ng-content></ng-content> <ng-content></ng-content>
</button> </button>

View File

@@ -0,0 +1,20 @@
.card {
background-color: var(--color-surface);
border-radius: var(--border-radius-md);
box-shadow: var(--box-shadow-sm);
border: 1px solid var(--color-border);
transition: all var(--transition-speed);
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--box-shadow-md);
}
.card-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
font-size: 1.1rem;
font-weight: 700;
}
.card-body {
padding: 1.5rem;
}

View File

@@ -27,9 +27,8 @@ const ICON_SVG_KEY_PREFIX = 'icon-svg-';
styleUrl: './icon.component.css' styleUrl: './icon.component.css'
}) })
export class IconComponent implements OnChanges { export class IconComponent implements OnChanges {
@Input() name: string = ''; @Input() iconName: string = '';
// --- HIER IST DIE KORREKTUR: Umbenennung zu svgColor --- @Input() svgColor: string | null = null;
@Input() svgColor: string | null = null; // Farbe des SVG-Inhalts (Füllung/Linie)
constructor( constructor(
private http: HttpClient, private http: HttpClient,
@@ -41,13 +40,13 @@ export class IconComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
// Überprüfe Änderungen an 'name' ODER 'svgColor' // Überprüfe Änderungen an 'name' ODER 'svgColor'
if ((changes['name'] && this.name) || (changes['svgColor'] && this.name)) { if ((changes['name'] && this.iconName) || (changes['svgColor'] && this.iconName)) {
this.loadSvg(); this.loadSvg();
} }
} }
private loadSvg(): void { private loadSvg(): void {
const svgStateKey: StateKey<string> = makeStateKey<string>(ICON_SVG_KEY_PREFIX + this.name); const svgStateKey: StateKey<string> = makeStateKey<string>(ICON_SVG_KEY_PREFIX + this.iconName);
const cachedSvg = this.transferState.get(svgStateKey, null); const cachedSvg = this.transferState.get(svgStateKey, null);
this.el.nativeElement.innerHTML = ''; this.el.nativeElement.innerHTML = '';
@@ -55,7 +54,7 @@ export class IconComponent implements OnChanges {
if (cachedSvg) { if (cachedSvg) {
this.setSvg(cachedSvg); this.setSvg(cachedSvg);
} else { } else {
this.http.get(`icons/${this.name}.svg`, { responseType: 'text' }) this.http.get(`icons/${this.iconName}.svg`, { responseType: 'text' })
.pipe( .pipe(
tap(svg => { tap(svg => {
if (!isPlatformBrowser(this.platformId)) { if (!isPlatformBrowser(this.platformId)) {

File diff suppressed because it is too large Load Diff