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

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

View File

@@ -1,15 +1,16 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
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({
selector: 'app-kpi-card',
standalone: true,
imports: [CommonModule, IconComponent],
imports: [CommonModule, IconComponent,CardComponent],
templateUrl: './kpi-card.component.html',
styleUrl: '../data-display.css',
styleUrl: './kpi-card.component.css',
})
export class KpiCardComponent {
@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">
<thead>
<tr>
<th class="sortable">
<span>Kunde</span>
<app-icon name="chevron-down"></app-icon>
</th>
<th>Kunde</th>
<th>Bestell-ID</th>
<th>Status</th>
<th class="text-right">Betrag</th>
@@ -14,46 +11,64 @@
</thead>
<tbody>
<!-- Schleife, die über die übergebenen Bestelldaten iteriert -->
<tr *ngFor="let order of orders">
<tr *ngFor="let order of displayedOrders">
<td>
<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 class="user-name">{{ order.user.name }}</div>
<div class="user-email">{{ order.user.email }}</div>
</div>
</div>
</td>
<td><span class="mono">#{{ order.id }}</span></td>
<td>
<!-- <app-status-pill [status]="order.status">{{ order.statusText }}</app-status-pill> -->
<span class="mono">#{{ order.id }}</span>
</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">
<app-button
buttonType="icon"
tooltip="Details anzeigen"
iconName="eye"
(click)="viewDetails.emit(order.id)">
<app-button
buttonType="icon"
tooltip="Details anzeigen"
iconName="placeholder"
(click)="view.emit(order.id)"
>
</app-button>
<app-button
buttonType="icon"
tooltip="Bearbeiten"
iconName="edit"
(click)="editOrder.emit(order.id)">
<app-button
buttonType="icon"
tooltip="Bearbeiten"
iconName="placeholder"
(click)="edit.emit(order.id)"
>
</app-button>
<app-button
buttonType="icon-danger"
tooltip="Löschen"
iconName="trash-2"
(click)="deleteOrder.emit(order.id)">
<app-button
buttonType="icon-danger"
tooltip="Löschen"
iconName="placeholder"
(click)="delete.emit(order.id)"
>
</app-button>
</td>
</tr>
<!-- Fallback, wenn keine Daten vorhanden sind -->
<tr *ngIf="!orders || orders.length === 0">
<tr *ngIf="!displayedOrders || displayedOrders.length === 0">
<td colspan="5" class="no-data-cell">Keine Bestellungen gefunden.</td>
</tr>
</tbody>
</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 { StatusPillComponent } from '../../ui/status-pill/status-pill.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
export interface OrderUser {
@@ -24,19 +24,44 @@ export interface Order {
standalone: true,
imports: [
CommonModule,
StatusPillComponent,
ButtonComponent,
IconComponent
PaginatorComponent
],
templateUrl: './orders-table.component.html',
styleUrl: '../data-display.css'
styleUrl: './orders-table.component.css'
})
export class OrdersTableComponent {
// 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
@Output() viewDetails = new EventEmitter<string>();
@Output() editOrder = new EventEmitter<string>();
@Output() deleteOrder = new EventEmitter<string>();
@Output() view = new EventEmitter<string>();
@Output() edit = 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,
imports: [CommonModule, ButtonComponent],
templateUrl: './paginator.component.html',
styleUrl: '../data-display.css'
styleUrl: './paginator.component.css',
})
export class PaginatorComponent {
// --- EINGABEN von außen ---
@@ -26,7 +26,7 @@ export class PaginatorComponent {
get rangeStart(): number {
return (this.currentPage - 1) * this.itemsPerPage + 1;
}
get rangeEnd(): number {
const end = this.currentPage * this.itemsPerPage;
return end > this.totalItems ? this.totalItems : end;
@@ -43,4 +43,4 @@ export class PaginatorComponent {
this.pageChange.emit(this.currentPage + 1);
}
}
}
}

View File

@@ -10,6 +10,6 @@
[class.btn-icon-danger]="buttonType === 'icon-danger'"
[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>
</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'
})
export class IconComponent implements OnChanges {
@Input() name: string = '';
// --- HIER IST DIE KORREKTUR: Umbenennung zu svgColor ---
@Input() svgColor: string | null = null; // Farbe des SVG-Inhalts (Füllung/Linie)
@Input() iconName: string = '';
@Input() svgColor: string | null = null;
constructor(
private http: HttpClient,
@@ -41,13 +40,13 @@ export class IconComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
// Ü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();
}
}
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);
this.el.nativeElement.innerHTML = '';
@@ -55,7 +54,7 @@ export class IconComponent implements OnChanges {
if (cachedSvg) {
this.setSvg(cachedSvg);
} else {
this.http.get(`icons/${this.name}.svg`, { responseType: 'text' })
this.http.get(`icons/${this.iconName}.svg`, { responseType: 'text' })
.pipe(
tap(svg => {
if (!isPlatformBrowser(this.platformId)) {