ok
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user