-
+
+
+
\ No newline at end of file
diff --git a/src/app/features/components/products/product-list/product-list.component.ts b/src/app/features/components/products/product-list/product-list.component.ts
index c35b518..87ddd7c 100644
--- a/src/app/features/components/products/product-list/product-list.component.ts
+++ b/src/app/features/components/products/product-list/product-list.component.ts
@@ -1,282 +1,68 @@
-import { Component, OnInit, OnDestroy, inject } from '@angular/core';
+// /src/app/features/admin/components/products/product-list/product-list.component.ts
+
+import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
-import {
- FormBuilder,
- FormGroup,
- FormArray,
- FormControl,
- ReactiveFormsModule,
- Validators,
-} from '@angular/forms';
-import { Observable, Subscription } from 'rxjs';
-import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
-
-// Models
-import {
- AdminProduct,
- ProductImage,
-} from '../../../../core/models/product.model';
-import { Category } from '../../../../core/models/category.model';
-import { Supplier } from '../../../../core/models/supplier.model';
-
-// Services
-import { ProductService } from '../../../services/product.service';
-import { CategoryService } from '../../../services/category.service';
-import { SupplierService } from '../../../services/supplier.service';
+import { AdminProduct } from '../../../../core/models/product.model';
+import { GenericTableComponent, ColumnConfig } from '../../../../shared/components/data-display/generic-table/generic-table.component';
+import { SearchBarComponent } from '../../../../shared/components/layout/search-bar/search-bar.component';
+import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
+import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
@Component({
selector: 'app-product-list',
standalone: true,
- imports: [CommonModule, ReactiveFormsModule],
+ imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
templateUrl: './product-list.component.html',
+ styleUrl: './product-list.component.css'
})
-export class ProductListComponent implements OnInit, OnDestroy {
- private productService = inject(ProductService);
- private categoryService = inject(CategoryService);
- private supplierService = inject(SupplierService);
- private fb = inject(FormBuilder);
+export class ProductListComponent implements OnChanges {
+ // --- Inputs & Outputs ---
+ @Input() products: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
+ @Input() allColumns: ColumnConfig[] = [];
+ @Input() visibleColumns: ColumnConfig[] = [];
+
+ @Output() addNew = new EventEmitter
();
+ @Output() editProduct = new EventEmitter();
+ @Output() deleteProduct = new EventEmitter();
+ @Output() columnsChange = new EventEmitter();
+ @Output() searchChange = new EventEmitter();
- products$!: Observable;
- allCategories$!: Observable;
- allSuppliers$!: Observable;
+ filteredProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
+ isColumnFilterVisible = false;
- productForm: FormGroup;
- selectedProductId: string | null = null;
- private nameChangeSubscription?: Subscription;
+ constructor() {}
- // Eigenschaften für das Bild-Management
- existingImages: ProductImage[] = [];
- mainImageFile: File | null = null;
- additionalImageFiles: File[] = [];
-
- constructor() {
- this.productForm = this.fb.group({
- name: ['', Validators.required],
- slug: ['', Validators.required],
- sku: ['', Validators.required],
- description: [''],
- price: [0, [Validators.required, Validators.min(0)]],
- oldPrice: [null, [Validators.min(0)]],
- purchasePrice: [null, [Validators.min(0)]],
- stockQuantity: [0, [Validators.required, Validators.min(0)]],
- weight: [null, [Validators.min(0)]],
- isActive: [true],
- isFeatured: [false],
- featuredDisplayOrder: [0],
- supplierId: [null],
- categorieIds: this.fb.array([]),
- imagesToDelete: this.fb.array([]), // FormArray für die IDs der zu löschenden Bilder
- });
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['products']) {
+ this.filteredProducts = this.products ? [...this.products] : [];
+ }
}
- // Getter für einfachen Zugriff auf FormArrays
- get categorieIds(): FormArray {
- return this.productForm.get('categorieIds') as FormArray;
+ onSearch(term: string): void {
+ this.searchChange.emit(term);
}
- get imagesToDelete(): FormArray {
- return this.productForm.get('imagesToDelete') as FormArray;
+ toggleColumnFilter(): void {
+ this.isColumnFilterVisible = !this.isColumnFilterVisible;
}
- ngOnInit(): void {
- this.loadInitialData();
- this.subscribeToNameChanges();
+ isColumnVisible(columnKey: string): boolean {
+ return this.visibleColumns.some(c => c.key === columnKey);
}
- ngOnDestroy(): void {
- this.nameChangeSubscription?.unsubscribe();
- }
-
- loadInitialData(): void {
- this.products$ = this.productService.getAll();
- this.allCategories$ = this.categoryService.getAll();
- this.allSuppliers$ = this.supplierService.getAll();
- }
-
- selectProduct(product: AdminProduct): void {
- this.selectedProductId = product.id;
- this.productForm.patchValue(product);
-
- this.categorieIds.clear();
- product.categorieIds?.forEach((id) =>
- this.categorieIds.push(this.fb.control(id))
- );
-
- this.existingImages = product.images || [];
- }
-
- clearSelection(): void {
- this.selectedProductId = null;
- this.productForm.reset({
- name: '',
- slug: '',
- sku: '',
- description: '',
- price: 0,
- oldPrice: null,
- purchasePrice: null,
- stockQuantity: 0,
- weight: null,
- isActive: true,
- isFeatured: false,
- featuredDisplayOrder: 0,
- supplierId: null,
- });
- this.categorieIds.clear();
- this.imagesToDelete.clear();
- this.existingImages = [];
- this.mainImageFile = null;
- this.additionalImageFiles = [];
- }
-
- onMainFileChange(event: Event): void {
- const file = (event.target as HTMLInputElement).files?.[0];
- if (file) this.mainImageFile = file;
- }
-
- onAdditionalFilesChange(event: Event): void {
- const files = (event.target as HTMLInputElement).files;
- if (files) this.additionalImageFiles = Array.from(files);
- }
-
- deleteExistingImage(imageId: string, event: Event): void {
- event.preventDefault();
- this.imagesToDelete.push(this.fb.control(imageId));
- this.existingImages = this.existingImages.filter(
- (img) => img.id !== imageId
- );
- }
-
- onCategoryChange(event: Event): void {
+ onColumnToggle(column: ColumnConfig, event: Event): void {
const checkbox = event.target as HTMLInputElement;
- const categoryId = checkbox.value;
+ let newVisibleColumns: ColumnConfig[];
+
if (checkbox.checked) {
- if (!this.categorieIds.value.includes(categoryId)) {
- this.categorieIds.push(new FormControl(categoryId));
- }
- } else {
- const index = this.categorieIds.controls.findIndex(
- (x) => x.value === categoryId
+ newVisibleColumns = [...this.visibleColumns, column];
+ newVisibleColumns.sort((a, b) =>
+ this.allColumns.findIndex(c => c.key === a.key) -
+ this.allColumns.findIndex(c => c.key === b.key)
);
- if (index !== -1) {
- this.categorieIds.removeAt(index);
- }
- }
- }
-
- isCategorySelected(categoryId: string): boolean {
- return this.categorieIds.value.includes(categoryId);
- }
-
- generateSku(): void {
- const name = this.productForm.get('name')?.value || 'PROD';
- const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
- const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase();
- const sku = `${prefix}-${randomPart}`;
- this.productForm.get('sku')?.setValue(sku);
- }
-
- onSubmit(): void {
- if (this.productForm.invalid) return;
-
- const formData = new FormData();
- const formValue = this.productForm.value;
-
- Object.keys(formValue).forEach((key) => {
- const value = formValue[key];
- if (key === 'categorieIds' || key === 'imagesToDelete') {
- // FormArrays müssen speziell behandelt werden
- (value as string[]).forEach((id) =>
- formData.append(this.capitalizeFirstLetter(key), id)
- );
- } else if (value !== null && value !== undefined && value !== '') {
- // Leere Strings für optionale number-Felder nicht mitsenden
- if (
- ['oldPrice', 'purchasePrice', 'weight'].includes(key) &&
- value === ''
- )
- return;
- formData.append(this.capitalizeFirstLetter(key), value);
- }
- });
-
- if (this.mainImageFile) {
- formData.append('MainImageFile', this.mainImageFile);
- }
- this.additionalImageFiles.forEach((file) => {
- formData.append('AdditionalImageFiles', file);
- });
-
- if (this.selectedProductId) {
- formData.append('Id', this.selectedProductId);
- this.productService
- .update(this.selectedProductId, formData)
- .subscribe(() => this.reset());
} else {
- this.productService.create(formData).subscribe(() => this.reset());
+ newVisibleColumns = this.visibleColumns.filter(c => c.key !== column.key);
}
+ this.columnsChange.emit(newVisibleColumns);
}
-
- onDelete(id: string): void {
- if (confirm('Produkt wirklich löschen?')) {
- this.productService.delete(id).subscribe(() => this.loadInitialData());
- }
- }
-
- private reset(): void {
- this.loadInitialData();
- this.clearSelection();
- }
-
- private subscribeToNameChanges(): void {
- this.nameChangeSubscription = this.productForm
- .get('name')
- ?.valueChanges.pipe(debounceTime(300), distinctUntilChanged())
- .subscribe((name) => {
- if (name && !this.productForm.get('slug')?.dirty) {
- const slug = this.generateSlug(name);
- this.productForm.get('slug')?.setValue(slug);
- }
- });
- }
-
- private generateSlug(name: string): string {
- return name
- .toLowerCase()
- .replace(/\s+/g, '-')
- .replace(/[äöüß]/g, (char) => {
- switch (char) {
- case 'ä':
- return 'ae';
- case 'ö':
- return 'oe';
- case 'ü':
- return 'ue';
- case 'ß':
- return 'ss';
- default:
- return '';
- }
- })
- .replace(/[^a-z0-9-]/g, '')
- .replace(/-+/g, '-');
- }
-
- private capitalizeFirstLetter(string: string): string {
- return string.charAt(0).toUpperCase() + string.slice(1);
- }
-
- /**
- * Sucht das Hauptbild aus der Bilderliste eines Produkts und gibt dessen URL zurück.
- * Gibt eine Platzhalter-URL zurück, wenn kein Hauptbild gefunden wird.
- * @param images Die Liste der Produktbilder.
- * @returns Die URL des Hauptbildes oder eine Platzhalter-URL.
- */
- getMainImageUrl(images?: ProductImage[]): string {
- if (!images || images.length === 0) {
- return ''; // Platzhalter, wenn gar keine Bilder vorhanden sind
- }
- const mainImage = images.find(img => img.isMainImage);
- return mainImage?.url || images[0].url || ''; // Fallback auf das erste Bild, wenn kein Hauptbild markiert ist
- }
-}
+}
\ No newline at end of file
diff --git a/src/app/features/components/products/product-page/product-page.component.css b/src/app/features/components/products/product-page/product-page.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/features/components/products/product-page/product-page.component.html b/src/app/features/components/products/product-page/product-page.component.html
new file mode 100644
index 0000000..0353ccc
--- /dev/null
+++ b/src/app/features/components/products/product-page/product-page.component.html
@@ -0,0 +1,21 @@
+
+
Produktverwaltung
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/features/components/products/product-page/product-page.component.ts b/src/app/features/components/products/product-page/product-page.component.ts
new file mode 100644
index 0000000..e7945f8
--- /dev/null
+++ b/src/app/features/components/products/product-page/product-page.component.ts
@@ -0,0 +1,131 @@
+import { Component, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Observable } from 'rxjs';
+import { AdminProduct, ProductImage } from '../../../../core/models/product.model';
+import { ProductService } from '../../../services/product.service';
+import { ProductListComponent } from '../product-list/product-list.component';
+import { ProductEditComponent } from '../product-edit/product-edit.component';
+import { SnackbarService } from '../../../../shared/services/snackbar.service';
+import { StorageService } from '../../../../core/services/storage.service';
+import { ColumnConfig } from '../../../../shared/components/data-display/generic-table/generic-table.component';
+import { SupplierService } from '../../../services/supplier.service';
+import { DatePipe } from '@angular/common';
+
+@Component({
+ selector: 'app-products-page',
+ standalone: true,
+ imports: [CommonModule, ProductListComponent, ProductEditComponent],
+ providers: [DatePipe],
+ templateUrl: './product-page.component.html',
+ styleUrls: ['./product-page.component.css'],
+})
+export class ProductsPageComponent implements OnInit {
+ private productService = inject(ProductService);
+ private supplierService = inject(SupplierService);
+ private snackbar = inject(SnackbarService);
+ private storageService = inject(StorageService);
+ private datePipe = inject(DatePipe);
+
+ private readonly TABLE_SETTINGS_KEY = 'product-table-columns';
+
+ allProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
+ filteredProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
+
+ currentView: 'list' | 'edit' | 'create' = 'list';
+ selectedProductId: string | null = null;
+
+ // --- VOLLSTÄNDIGE SPALTEN-DEFINITION ---
+ readonly allTableColumns: ColumnConfig[] = [
+ { key: 'mainImage', title: 'Bild', type: 'image' },
+ { key: 'name', title: 'Name', type: 'text', subKey: 'sku' },
+ { key: 'price', title: 'Preis', type: 'currency', cssClass: 'text-right' },
+ { key: 'stockQuantity', title: 'Lager', type: 'text', cssClass: 'text-right' },
+ { key: 'supplierName', title: 'Lieferant', type: 'text' },
+ { key: 'isActive', title: 'Aktiv', type: 'status' },
+ { key: 'isFeatured', title: 'Hervorgehoben', type: 'status' },
+ { key: 'weight', title: 'Gewicht (kg)', type: 'text', cssClass: 'text-right' },
+ { key: 'purchasePrice', title: 'EK-Preis', type: 'currency', cssClass: 'text-right' },
+ { key: 'oldPrice', title: 'Alter Preis', type: 'currency', cssClass: 'text-right' },
+ { key: 'createdDate', title: 'Erstellt am', type: 'text' },
+ { key: 'lastModifiedDate', title: 'Geändert am', type: 'text' },
+ { key: 'actions', title: 'Aktionen', type: 'actions', cssClass: 'text-right' }
+ ];
+ visibleTableColumns: ColumnConfig[] = [];
+
+ constructor() {
+ this.loadTableSettings();
+ }
+
+ ngOnInit(): void {
+ this.loadProducts();
+ }
+
+ loadProducts(): void {
+ this.productService.getAll().subscribe(products => {
+ this.supplierService.getAll().subscribe(suppliers => {
+
+ this.allProducts = products.map(p => {
+ const supplier = suppliers.find(s => s.id === p.supplierId);
+ return {
+ ...p,
+ mainImage: this.getMainImageUrl(p.images),
+ supplierName: supplier?.name || '-',
+
+ // --- HIER IST DIE KORREKTUR ---
+ createdDate: this.datePipe.transform(p.createdDate, 'dd.MM.yyyy HH:mm') || '-',
+
+ // Wenn lastModifiedDate existiert, transformiere es. Wenn das Ergebnis null ist,
+ // gib einen leeren String oder einen Platzhalter zurück. Wenn es nicht existiert, bleibt es undefined.
+ lastModifiedDate: p.lastModifiedDate
+ ? (this.datePipe.transform(p.lastModifiedDate, 'dd.MM.yyyy HH:mm') || '-')
+ : undefined
+ };
+ });
+ // --- ENDE DER KORREKTUR ---
+
+ this.filteredProducts = [...this.allProducts];
+ });
+ });
+ }
+
+
+ onSearch(term: any): void {
+ const lowerTerm = term.toLowerCase();
+ this.filteredProducts = this.allProducts.filter(p =>
+ p.name?.toLowerCase().includes(lowerTerm) ||
+ p.sku?.toLowerCase().includes(lowerTerm) ||
+ p.supplierName?.toLowerCase().includes(lowerTerm)
+ );
+ }
+
+ onAddNew(): void { this.currentView = 'create'; this.selectedProductId = null; }
+ onEditProduct(productId: string): void { this.currentView = 'edit'; this.selectedProductId = productId; }
+ onFormClose(): void { this.currentView = 'list'; this.selectedProductId = null; this.loadProducts(); }
+ onDeleteProduct(productId: string): void {
+ if (confirm('Möchten Sie dieses Produkt wirklich endgültig löschen?')) {
+ this.productService.delete(productId).subscribe(() => {
+ this.snackbar.show('Produkt erfolgreich gelöscht.');
+ this.loadProducts();
+ });
+ }
+ }
+
+ private loadTableSettings(): void {
+ const savedKeys = this.storageService.getItem(this.TABLE_SETTINGS_KEY);
+ const defaultKeys = ['mainImage', 'name', 'price', 'stockQuantity', 'isActive', 'actions'];
+ const keysToUse = (savedKeys && savedKeys.length > 0) ? savedKeys : defaultKeys;
+ this.visibleTableColumns = this.allTableColumns.filter(c => keysToUse.includes(c.key));
+ }
+
+ onColumnsChange(newVisibleColumns: ColumnConfig[]): void {
+ this.visibleTableColumns = newVisibleColumns;
+ const visibleKeys = newVisibleColumns.map(c => c.key);
+ this.storageService.setItem(this.TABLE_SETTINGS_KEY, visibleKeys);
+ }
+
+ private getMainImageUrl(images?: ProductImage[]): string {
+ if (!images || images.length === 0) return 'https://via.placeholder.com/50';
+ const mainImage = images.find(img => img.isMainImage);
+ return mainImage?.url || images[0]?.url || 'https://via.placeholder.com/50';
+ }
+}
diff --git a/src/app/features/components/products/products.routes.ts b/src/app/features/components/products/products.routes.ts
index be8288b..192b3d4 100644
--- a/src/app/features/components/products/products.routes.ts
+++ b/src/app/features/components/products/products.routes.ts
@@ -1,10 +1,17 @@
import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
+import { GenericTableComponent } from '../../../shared/components/data-display/generic-table/generic-table.component';
+import { ProductsPageComponent } from './product-page/product-page.component';
export const PRODUCTS_ROUTES: Routes = [
{
path: '',
- component: ProductListComponent,
+ component: ProductsPageComponent,
+ title: '',
+ },
+ {
+ path: '1',
+ component: GenericTableComponent,
title: '',
},
];
diff --git a/src/app/shared/components/data-display/generic-table/generic-table.component.css b/src/app/shared/components/data-display/generic-table/generic-table.component.css
new file mode 100644
index 0000000..4bb1977
--- /dev/null
+++ b/src/app/shared/components/data-display/generic-table/generic-table.component.css
@@ -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);
+}
\ No newline at end of file
diff --git a/src/app/shared/components/data-display/generic-table/generic-table.component.html b/src/app/shared/components/data-display/generic-table/generic-table.component.html
new file mode 100644
index 0000000..4aa2bdb
--- /dev/null
+++ b/src/app/shared/components/data-display/generic-table/generic-table.component.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+ | {{ col.title }} |
+
+
+
+
+
+
+
+
+
+
+
+ {{ getProperty(item, col.key) }}
+
+
+ {{ getProperty(item, col.subKey) }}
+
+
+
+
+
+ {{ getProperty(item, col.key) | currency:'EUR' }}
+
+
+
+
+
+
+
+
+
+ ![]()
+
+ {{ getProperty(item, col.key) }}
+ {{ getProperty(item, col.subKey!) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ | Keine Daten gefunden. |
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/shared/components/data-display/generic-table/generic-table.component.ts b/src/app/shared/components/data-display/generic-table/generic-table.component.ts
new file mode 100644
index 0000000..f0876f0
--- /dev/null
+++ b/src/app/shared/components/data-display/generic-table/generic-table.component.ts
@@ -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();
+ @Output() edit = new EventEmitter();
+ @Output() delete = new EventEmitter();
+
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/app/shared/components/form/form-field/form-field.component.ts b/src/app/shared/components/form/form-field/form-field.component.ts
index c98fe57..8f0cd2d 100644
--- a/src/app/shared/components/form/form-field/form-field.component.ts
+++ b/src/app/shared/components/form/form-field/form-field.component.ts
@@ -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.';
+ }
+}
\ No newline at end of file
diff --git a/src/app/shared/components/ui/status-pill/status-pill.component.css b/src/app/shared/components/ui/status-pill/status-pill.component.css
index f840d06..ac41598 100644
--- a/src/app/shared/components/ui/status-pill/status-pill.component.css
+++ b/src/app/shared/components/ui/status-pill/status-pill.component.css
@@ -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; }
\ No newline at end of file
+: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; }
\ No newline at end of file
diff --git a/src/app/shared/components/ui/status-pill/status-pill.component.ts b/src/app/shared/components/ui/status-pill/status-pill.component.ts
index 56cc86a..95b72b5 100644
--- a/src/app/shared/components/ui/status-pill/status-pill.component.ts
+++ b/src/app/shared/components/ui/status-pill/status-pill.component.ts
@@ -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;
+ }
}
}
\ No newline at end of file