This commit is contained in:
Tizian.Breuch
2025-10-24 14:35:07 +02:00
parent 1ec7ac6ccc
commit fd68b47414
16 changed files with 961 additions and 511 deletions

View File

@@ -0,0 +1,99 @@
/* /src/app/shared/components/table/generic-table/generic-table.component.css */
:host {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.table-container {
overflow-x: auto;
flex-grow: 1;
min-width: 0;
}
.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 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;
}
/* Spezifische Zell-Stile */
.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;
}
/* Verwendet die von dir definierte Klasse für die rechten Aktionen */
.actions-cell {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
/* Hilfsklasse für rechtsbündigen Text */
.text-right {
text-align: right;
}
.no-data-cell {
text-align: center;
padding: 2rem;
color: var(--color-text-light);
}

View File

@@ -0,0 +1,77 @@
<!-- /src/app/shared/components/table/generic-table/generic-table.component.html -->
<div class="table-container">
<table class="modern-table">
<thead>
<tr>
<th *ngFor="let col of columns" [ngClass]="col.cssClass">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of displayedData">
<td *ngFor="let col of columns" [ngClass]="col.cssClass">
<ng-container [ngSwitch]="col.type">
<ng-container *ngSwitchCase="'text'">
<div>
<span [class.mono]="col.key === 'id' || col.key === 'orderNumber' || col.key === 'sku'">
{{ getProperty(item, col.key) }}
</span>
<div *ngIf="col.subKey" class="user-email">
{{ getProperty(item, col.subKey) }}
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="'currency'">
<span class="amount">{{ getProperty(item, col.key) | currency:'EUR' }}</span>
</ng-container>
<!-- VEREINFACHT: Wir übergeben den Wert direkt, die Pille kümmert sich um den Rest. -->
<ng-container *ngSwitchCase="'status'">
<app-status-pill [status]="getProperty(item, col.key)"></app-status-pill>
</ng-container>
<ng-container *ngSwitchCase="'image-text'">
<div class="user-cell">
<img [src]="getProperty(item, col.imageKey!) || 'https://via.placeholder.com/40'"
[alt]="'Bild von ' + getProperty(item, col.key)" />
<div>
<div class="user-name">{{ getProperty(item, col.key) }}</div>
<div class="user-email">{{ getProperty(item, col.subKey!) }}</div>
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="'image'">
<img [src]="getProperty(item, col.key) || 'https://via.placeholder.com/50'"
alt="Bild"
style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
</ng-container>
<ng-container *ngSwitchCase="'actions'">
<div class="actions-cell">
<app-button buttonType="icon" tooltip="Details anzeigen" iconName="eye" (click)="view.emit(item)"></app-button>
<app-button buttonType="icon" tooltip="Bearbeiten" iconName="edit" (click)="edit.emit(item)"></app-button>
<app-button buttonType="icon-danger" tooltip="Löschen" iconName="delete" (click)="delete.emit(item)"></app-button>
</div>
</ng-container>
</ng-container>
</td>
</tr>
<tr *ngIf="!displayedData || displayedData.length === 0">
<td [attr.colspan]="columns.length" class="no-data-cell">Keine Daten gefunden.</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="data.length > itemsPerPage">
<app-paginator
[currentPage]="currentPage"
[totalItems]="data.length"
[itemsPerPage]="itemsPerPage"
(pageChange)="onPageChange($event)"
></app-paginator>
</div>

View File

@@ -0,0 +1,53 @@
// /src/app/shared/components/table/generic-table/generic-table.component.ts
import { Component, Input, Output, EventEmitter, SimpleChanges, OnChanges, OnInit } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { StatusPillComponent } from '../../ui/status-pill/status-pill.component';
import { ButtonComponent } from '../../ui/button/button.component';
import { PaginatorComponent } from '../paginator/paginator.component';
export type ColumnType = 'text' | 'currency' | 'status' | 'image-text' | 'image' | 'actions';
export interface ColumnConfig {
key: string;
title: string;
type: ColumnType;
imageKey?: string;
subKey?: string;
cssClass?: string;
}
@Component({
selector: 'app-generic-table',
standalone: true,
imports: [ CommonModule, CurrencyPipe, StatusPillComponent, ButtonComponent, PaginatorComponent ],
templateUrl: './generic-table.component.html',
styleUrl: './generic-table.component.css',
})
export class GenericTableComponent implements OnChanges, OnInit {
@Input() data: any[] = [];
@Input() columns: ColumnConfig[] = [];
@Input() itemsPerPage = 10;
@Output() view = new EventEmitter<any>();
@Output() edit = new EventEmitter<any>();
@Output() delete = new EventEmitter<any>();
public displayedData: any[] = [];
public currentPage = 1;
ngOnInit(): void { this.updatePagination(); }
ngOnChanges(changes: SimpleChanges): void { if (changes['data']) { this.currentPage = 1; this.updatePagination(); } }
onPageChange(newPage: number): void { this.currentPage = newPage; this.updatePagination(); }
private updatePagination(): void {
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
this.displayedData = this.data.slice(startIndex, endIndex);
}
getProperty(item: any, key: string): any {
if (!key) return '';
return key.split('.').reduce((obj, part) => obj && obj[part], item);
}
}

View File

@@ -1,57 +1,66 @@
// /src/app/shared/components/form/form-field/form-field.component.ts
import { Component, Input, forwardRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormsModule,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { AbstractControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-form-field',
standalone: true,
imports: [
CommonModule,
FormsModule, // Wichtig für [(ngModel)] im Template
FormsModule,
ReactiveFormsModule // <-- WICHTIG: Hinzufügen, um mit AbstractControl zu arbeiten
],
templateUrl: './form-field.component.html',
styleUrl: './form-field.component.css',
providers: [
{
// Stellt diese Komponente als "Value Accessor" zur Verfügung
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true,
},
],
})
// Die Komponente implementiert die ControlValueAccessor-Schnittstelle
export class FormFieldComponent implements ControlValueAccessor {
export class FormFieldComponent {
// --- KORREKTUR: Erweitere die erlaubten Typen ---
@Input() type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' = 'text';
@Input() label: string = '';
@Input() type: 'text' | 'email' | 'password' = 'text';
// Neuer Input, um das FormControl für die Fehleranzeige zu erhalten
@Input() control?: AbstractControl;
@Input() showErrors = true; // Standardmäßig Fehler anzeigen
controlId = `form-field-${Math.random().toString(36)}`;
controlId = `form-field-${Math.random().toString(36).substring(2, 9)}`;
// --- Eigenschaften für ControlValueAccessor ---
value: string = '';
// --- Eigenschaften & Methoden für ControlValueAccessor ---
value: string | number = '';
onChange: (value: any) => void = () => {};
onTouched: () => void = () => {};
disabled = false;
// --- Methoden, die von Angular Forms aufgerufen werden ---
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
// Hilfsfunktion für das Template, um Fehler zu finden
get errorMessage(): string | null {
if (!this.control || !this.control.errors || (!this.control.touched && !this.control.dirty)) {
return null;
}
if (this.control.hasError('required')) return 'Dieses Feld ist erforderlich.';
if (this.control.hasError('email')) return 'Bitte geben Sie eine gültige E-Mail-Adresse ein.';
if (this.control.hasError('min')) return `Der Wert muss mindestens ${this.control.errors['min'].min} sein.`;
// ... weitere Fehlermeldungen hier
return 'Ungültige Eingabe.';
}
}

View File

@@ -42,4 +42,10 @@
:host-context(body.dark-theme) .pill-danger { color: #fca5a5; background-color: #991b1b; border-color: #ef4444; }
.pill-info { color: #1d4ed8; background-color: #eff6ff; border-color: #bfdbfe; }
:host-context(body.dark-theme) .pill-info { color: #93c5fd; background-color: #1e40af; border-color: #3b82f6; }
:host-context(body.dark-theme) .pill-info { color: #93c5fd; background-color: #1e40af; border-color: #3b82f6; }
.pill-active { color: #15803d; background-color: #ecfdf5; border-color: #bbf7d0; }
:host-context(body.dark-theme) .pill-active { color: #a7f3d0; background-color: #166534; border-color: #22c55e; }
.pill-inactive { color: rgb(168, 168, 168); background-color: #ececec; border-color: #777777; }
:host-context(body.dark-theme) .pill-inactive { color: rgb(168, 168, 168); background-color: #ececec; border-color: #777777; }

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule, NgClass } from '@angular/common';
// import { OrderStatus } from '../../../../core/types/order';
@@ -11,7 +11,7 @@ import { CommonModule, NgClass } from '@angular/common';
})
export class StatusPillComponent implements OnChanges {
// Nimmt jetzt den neuen, sprechenden Status entgegen
@Input() status: any = 'info';
@Input() status: string | boolean = 'info';
// Diese Eigenschaften werden vom Template verwendet
public displayText = '';
@@ -22,13 +22,22 @@ export class StatusPillComponent implements OnChanges {
['completed', { text: 'Abgeschlossen', css: 'pill-success' }],
['processing', { text: 'In Bearbeitung', css: 'pill-warning' }],
['cancelled', { text: 'Storniert', css: 'pill-danger' }],
['info', { text: 'Info', css: 'pill-info' }]
['info', { text: 'Info', css: 'pill-info' }],
['active', { text: 'Ja', css: 'pill-active' }],
['inactive', { text: 'Nein', css: 'pill-inactive' }]
]);
ngOnChanges(): void {
// Wenn sich der Input-Status ändert, aktualisieren wir Text und Klasse
const details = this.statusMap.get(this.status) || this.statusMap.get('info')!;
this.displayText = details.text;
this.cssClass = details.css;
ngOnChanges(changes: SimpleChanges): void {
if (changes['status']) {
let statusKey = this.status;
if (typeof statusKey === 'boolean') {
statusKey = statusKey ? 'active' : 'inactive';
}
const details = this.statusMap.get(statusKey as string) || { text: statusKey.toString(), css: 'pill-secondary' };
this.displayText = details.text;
this.cssClass = details.css;
}
}
}