This commit is contained in:
Tizian.Breuch
2025-10-29 11:41:15 +01:00
parent fd68b47414
commit 4549149e48
8 changed files with 283 additions and 353 deletions

View File

@@ -1,127 +1,108 @@
<form [formGroup]="productForm" (ngSubmit)="onSubmit()" novalidate> <!-- /src/app/features/admin/components/products/product-edit/product-edit.component.html -->
<div class="form-header">
<h3 card-header> <app-card>
{{ isEditMode ? "Produkt bearbeiten" : "Neues Produkt erstellen" }} <div *ngIf="isLoading; else formContent" class="loading-container">
</h3> <p>Lade Produktdaten...</p>
</div> </div>
<!-- Sektion 1: Basis-Informationen --> <ng-template #formContent>
<form [formGroup]="productForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-header">
<h3 card-header>{{ isEditMode ? "Produkt bearbeiten" : "Neues Produkt erstellen" }}</h3>
</div>
<div class="edit-layout">
<!-- LINKE SPALTE -->
<div class="main-content">
<app-card>
<h4 card-header>Allgemein</h4>
<div class="form-section">
<app-form-field label="Name"><input type="text" formControlName="name"></app-form-field>
<app-form-field label="Slug"><input type="text" formControlName="slug"></app-form-field>
<app-form-textarea label="Beschreibung" [rows]="8" formControlName="description"></app-form-textarea>
</div>
</app-card>
<app-card>
<h4 card-header>Produktbilder</h4>
<!-- ... (Bild-Management bleibt gleich) ... -->
</app-card>
<app-card>
<h4 card-header>Preisgestaltung</h4>
<div class="form-grid price-grid">
<!-- KORREKTUR: Wrapper entfernt, formControlName auf die Komponente -->
<app-form-field label="Preis (€)" type="number" formControlName="price"></app-form-field>
<app-form-field label="Alter Preis (€)" type="number" formControlName="oldPrice"></app-form-field>
<app-form-field label="Einkaufspreis (€)" type="number" formControlName="purchasePrice"></app-form-field>
</div>
</app-card>
</div>
<!-- RECHTE SPALTE -->
<div class="sidebar-content">
<app-card>
<h4 card-header>Status</h4>
<div class="form-section">
<app-slide-toggle formControlName="isActive">Aktiv (im Shop sichtbar)</app-slide-toggle>
</div>
</app-card>
<app-card>
<h4 card-header>Organisation</h4>
<div class="form-section"> <div class="form-section">
<h4 class="section-title">Basis-Informationen</h4>
<div class="form-grid">
<app-form-field class="grid-col-span-2" label="Name"
><input type="text" formControlName="name"
/></app-form-field>
<app-form-field label="Slug (automatisch generiert)"
><input type="text" formControlName="slug"
/></app-form-field>
<div class="form-field"> <div class="form-field">
<label class="form-label">SKU (Artikelnummer)</label> <label class="form-label">SKU (Artikelnummer)</label>
<div class="input-with-button"> <div class="input-with-button">
<input type="text" class="form-input" formControlName="sku" /> <input type="text" class="form-input" formControlName="sku" />
<app-button buttonType="secondary" (click)="generateSku()" <app-button buttonType="icon" (click)="generateSku()" iconName="placeholder"></app-button>
>Generieren</app-button
>
</div> </div>
</div> </div>
<app-form-textarea <app-form-field label="Lagerbestand" type="number" formControlName="stockQuantity"></app-form-field>
class="grid-col-span-2" <app-form-field label="Gewicht (kg)" type="number" formControlName="weight"></app-form-field>
label="Beschreibung" <app-form-select label="Lieferant" [options]="(supplierOptions$ | async) || []" formControlName="supplierId"></app-form-select>
[rows]="5"
formControlName="description"
></app-form-textarea>
</div> </div>
</div> </app-card>
<hr class="divider" />
<!-- Sektion 2: Preis & Lager --> <app-card>
<h4 card-header>Kategorien</h4>
<div class="form-section"> <div class="form-section">
<h4 class="section-title">Preis & Lager</h4> <div class="multi-select-container">
<div class="form-grid"> <div class="selected-pills">
<app-form-field label="Preis (€)" <span *ngFor="let catId of categorieIds.value" class="pill">
><input type="number" formControlName="price" {{ getCategoryName(catId) }}
/></app-form-field> <app-icon iconName="x" (click)="removeCategoryById(catId)"></app-icon>
<app-form-field label="Alter Preis (€)" </span>
><input type="number" formControlName="oldPrice" <span *ngIf="categorieIds.length === 0" class="placeholder">Keine ausgewählt</span>
/></app-form-field>
<app-form-field label="Einkaufspreis (€)"
><input type="number" formControlName="purchasePrice"
/></app-form-field>
<app-form-field label="Lagerbestand"
><input type="number" formControlName="stockQuantity"
/></app-form-field>
<app-form-field label="Gewicht (kg)"
><input type="number" formControlName="weight"
/></app-form-field>
</div> </div>
</div>
<hr class="divider" />
<!-- Sektion 3: Zuweisungen -->
<div class="form-section">
<h4 class="section-title">Zuweisungen</h4>
<div class="form-grid">
<!-- KORREKTUR: Fallback auf leeres Array für den async-Pipe -->
<app-form-select
label="Lieferant"
[options]="(supplierOptions$ | async) || []"
formControlName="supplierId"
></app-form-select>
<div class="form-field">
<label class="form-label">Kategorien</label>
<div class="category-checkbox-group"> <div class="category-checkbox-group">
<div <label *ngFor="let category of allCategories$ | async">
*ngFor="let category of allCategories$ | async" <input type="checkbox" [value]="category.id" [checked]="isCategorySelected(category.id)" (change)="onCategoryChange($event)">
class="checkbox-item" {{ category.name }}
> </label>
<input
type="checkbox"
[id]="'cat-' + category.id"
[value]="category.id"
[checked]="isCategorySelected(category.id)"
(change)="onCategoryChange($event)"
/>
<label [for]="'cat-' + category.id">{{ category.name }}</label>
</div> </div>
</div> </div>
</div> </div>
</div> </app-card>
</div>
<hr class="divider" />
<!-- Sektion 4: Bilder --> <app-card>
<h4 card-header>Hervorheben</h4>
<div class="form-section"> <div class="form-section">
<h4 class="section-title">Produktbilder</h4> <app-slide-toggle formControlName="isFeatured">Auf Startseite anzeigen</app-slide-toggle>
<!-- ... (Bild-Management-Logik hier einfügen) ... --> <app-form-field *ngIf="productForm.get('isFeatured')?.value" label="Anzeigereihenfolge">
<input type="number" formControlName="featuredDisplayOrder">
</app-form-field>
</div> </div>
<hr class="divider" /> </app-card>
<!-- Sektion 5: Status & Sichtbarkeit -->
<div class="form-section">
<h4 class="section-title">Status & Sichtbarkeit</h4>
<div class="checkbox-group">
<app-slide-toggle formControlName="isActive"
>Aktiv (im Shop sichtbar)</app-slide-toggle
>
<app-slide-toggle formControlName="isFeatured"
>Hervorgehoben (z.B. auf Startseite)</app-slide-toggle
>
</div>
<div class="form-grid" style="margin-top: 1rem">
<app-form-field label="Anzeigereihenfolge"
><input type="number" formControlName="featuredDisplayOrder"
/></app-form-field>
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<app-button buttonType="stroked" (click)="cancel()">Abbrechen</app-button> <app-button buttonType="stroked" (click)="cancel()">Abbrechen</app-button>
<app-button <app-button submitType="submit" buttonType="primary" [disabled]="productForm.invalid || isLoading">
submitType="submit"
buttonType="primary"
[disabled]="productForm.invalid"
>
{{ isEditMode ? "Änderungen speichern" : "Produkt erstellen" }} {{ isEditMode ? "Änderungen speichern" : "Produkt erstellen" }}
</app-button> </app-button>
</div> </div>
</form> </form>
</ng-template>
</app-card>

View File

@@ -1,19 +1,24 @@
// /src/app/features/admin/components/products/product-edit/product-edit.component.ts // /src/app/features/admin/components/products/product-edit/product-edit.component.ts
import { Component, OnInit, OnDestroy, inject, EventEmitter, Input, Output } from '@angular/core'; import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, startWith, tap } from 'rxjs/operators';
// ... (alle anderen Imports bleiben gleich) // Models
import { AdminProduct, ProductImage } from '../../../../core/models/product.model'; import { AdminProduct, ProductImage } from '../../../../core/models/product.model';
import { Category } from '../../../../core/models/category.model'; import { Category } from '../../../../core/models/category.model';
import { Supplier } from '../../../../core/models/supplier.model'; import { Supplier } from '../../../../core/models/supplier.model';
// Services
import { ProductService } from '../../../services/product.service'; import { ProductService } from '../../../services/product.service';
import { CategoryService } from '../../../services/category.service'; import { CategoryService } from '../../../services/category.service';
import { SupplierService } from '../../../services/supplier.service'; import { SupplierService } from '../../../services/supplier.service';
import { SnackbarService } from '../../../../shared/services/snackbar.service'; import { SnackbarService } from '../../../../shared/services/snackbar.service';
// Wiederverwendbare UI- & Form-Komponenten
import { CardComponent } from '../../../../shared/components/ui/card/card.component'; import { CardComponent } from '../../../../shared/components/ui/card/card.component';
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { IconComponent } from '../../../../shared/components/ui/icon/icon.component'; import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
@@ -30,20 +35,22 @@ import { SlideToggleComponent } from '../../../../shared/components/form/slide-t
styleUrl: './product-edit.component.css' styleUrl: './product-edit.component.css'
}) })
export class ProductEditComponent implements OnInit, OnDestroy { export class ProductEditComponent implements OnInit, OnDestroy {
@Input() productId: string | null = null; private route = inject(ActivatedRoute);
@Output() formClose = new EventEmitter<void>(); private router = inject(Router);
private productService = inject(ProductService); private productService = inject(ProductService);
private categoryService = inject(CategoryService); private categoryService = inject(CategoryService);
private supplierService = inject(SupplierService); private supplierService = inject(SupplierService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private snackbarService = inject(SnackbarService); private snackbarService = inject(SnackbarService);
productId: string | null = null;
isEditMode = false; isEditMode = false;
isLoading = true;
productForm: FormGroup; productForm: FormGroup;
allCategories$!: Observable<Category[]>; allCategories$!: Observable<Category[]>;
supplierOptions$!: Observable<SelectOption[]>; supplierOptions$!: Observable<SelectOption[]>;
allCategories: Category[] = [];
private nameChangeSubscription?: Subscription; private nameChangeSubscription?: Subscription;
existingImages: ProductImage[] = []; existingImages: ProductImage[] = [];
@@ -65,13 +72,22 @@ export class ProductEditComponent implements OnInit, OnDestroy {
get imagesToDelete(): FormArray { return this.productForm.get('imagesToDelete') as FormArray; } get imagesToDelete(): FormArray { return this.productForm.get('imagesToDelete') as FormArray; }
ngOnInit(): void { ngOnInit(): void {
this.productId = this.route.snapshot.paramMap.get('id');
this.isEditMode = !!this.productId; this.isEditMode = !!this.productId;
this.loadDropdownData(); this.loadDropdownData();
this.subscribeToNameChanges(); this.subscribeToNameChanges();
if (this.isEditMode && this.productId) { if (this.isEditMode && this.productId) {
this.isLoading = true;
this.productService.getById(this.productId).subscribe(product => { this.productService.getById(this.productId).subscribe(product => {
this.selectProduct(product); if (product) {
this.populateForm(product);
}
this.isLoading = false;
}); });
} else {
this.isLoading = false;
} }
} }
@@ -80,15 +96,16 @@ export class ProductEditComponent implements OnInit, OnDestroy {
} }
loadDropdownData(): void { loadDropdownData(): void {
this.allCategories$ = this.categoryService.getAll(); this.allCategories$ = this.categoryService.getAll().pipe(
// --- HIER IST DIE KORREKTUR --- tap(categories => this.allCategories = categories)
);
this.supplierOptions$ = this.supplierService.getAll().pipe( this.supplierOptions$ = this.supplierService.getAll().pipe(
map(suppliers => suppliers.map(s => ({ value: s.id, label: s.name || 'Unbenannt' }))), map(suppliers => suppliers.map(s => ({ value: s.id, label: s.name || 'Unbenannt' }))),
startWith([]) // Stellt sicher, dass das Observable sofort ein leeres Array ausgibt startWith([])
); );
} }
selectProduct(product: AdminProduct): void { populateForm(product: AdminProduct): void {
this.productForm.patchValue(product); this.productForm.patchValue(product);
this.categorieIds.clear(); this.categorieIds.clear();
product.categorieIds?.forEach(id => this.categorieIds.push(this.fb.control(id))); product.categorieIds?.forEach(id => this.categorieIds.push(this.fb.control(id)));
@@ -126,6 +143,17 @@ export class ProductEditComponent implements OnInit, OnDestroy {
return this.categorieIds.value.includes(categoryId); return this.categorieIds.value.includes(categoryId);
} }
getCategoryName(categoryId: string): string {
return this.allCategories.find(c => c.id === categoryId)?.name || '';
}
removeCategoryById(categoryId: string): void {
const index = this.categorieIds.controls.findIndex(x => x.value === categoryId);
if (index !== -1) {
this.categorieIds.removeAt(index);
}
}
generateSku(): void { generateSku(): void {
const name = this.productForm.get('name')?.value || 'PROD'; const name = this.productForm.get('name')?.value || 'PROD';
const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, ''); const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
@@ -158,26 +186,24 @@ export class ProductEditComponent implements OnInit, OnDestroy {
if (this.mainImageFile) formData.append('MainImageFile', this.mainImageFile); if (this.mainImageFile) formData.append('MainImageFile', this.mainImageFile);
this.additionalImageFiles.forEach((file) => formData.append('AdditionalImageFiles', file)); this.additionalImageFiles.forEach((file) => formData.append('AdditionalImageFiles', file));
// KORREKTUR: Explizites Casten zu einem gemeinsamen Typ (any) hilft TypeScript
const operation: Observable<any> = this.isEditMode const operation: Observable<any> = this.isEditMode
? this.productService.update(this.productId!, formData) ? this.productService.update(this.productId!, formData)
: this.productService.create(formData); : this.productService.create(formData);
// KORREKTUR: Korrekte subscribe-Syntax mit Objekt
operation.subscribe({ operation.subscribe({
next: () => { next: () => {
this.snackbarService.show(this.isEditMode ? 'Produkt erfolgreich aktualisiert' : 'Produkt erfolgreich erstellt'); this.snackbarService.show(this.isEditMode ? 'Produkt aktualisiert' : 'Produkt erstellt');
this.formClose.emit(); this.router.navigate(['/admin/products']);
}, },
error: (err: any) => { error: (err: any) => {
this.snackbarService.show('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); this.snackbarService.show('Ein Fehler ist aufgetreten.');
console.error(err); console.error(err);
} }
}); });
} }
cancel(): void { cancel(): void {
this.formClose.emit(); this.router.navigate(['/admin/products']);
} }
private subscribeToNameChanges(): void { private subscribeToNameChanges(): void {

View File

@@ -1,31 +1,30 @@
<!-- /src/app/features/admin/components/products/product-list/product-list.component.html --> <!-- /src/app/features/admin/components/products/product-list/product-list.component.html -->
<div class="table-header"> <div class="page-container">
<app-search-bar placeholder="Produkte nach Name oder SKU suchen..." (search)="onSearch($event)"></app-search-bar> <div class="header">
<div class="header-actions"> <h1 class="page-title">Produktübersicht</h1>
<div class="column-filter-container"> <app-button buttonType="primary" (click)="onAddNew()">
<app-button buttonType="stroked" (click)="toggleColumnFilter()" iconName="filter"> <app-icon iconName="plus"></app-icon> Neues Produkt
Spalten
</app-button> </app-button>
</div>
<div class="table-header">
<app-search-bar placeholder="Produkte nach Name oder SKU suchen..." (search)="onSearch($event)"></app-search-bar>
<div class="column-filter-container">
<app-button buttonType="stroked" (click)="toggleColumnFilter()" iconName="filter">Spalten</app-button>
<div class="column-filter-dropdown" *ngIf="isColumnFilterVisible"> <div class="column-filter-dropdown" *ngIf="isColumnFilterVisible">
<!-- Iteriert jetzt über allColumns, das vom Parent kommt --> <label *ngFor="let col of allTableColumns">
<label *ngFor="let col of allColumns"> <input type="checkbox" [checked]="isColumnVisible(col.key)" (change)="onColumnToggle(col, $event)">
<input
type="checkbox"
[checked]="isColumnVisible(col.key)"
(change)="onColumnToggle(col, $event)">
{{ col.title }} {{ col.title }}
</label> </label>
</div> </div>
</div> </div>
<app-button buttonType="primary" (click)="addNew.emit()">
<app-icon iconName="plus"></app-icon> Neues Produkt
</app-button>
</div> </div>
</div>
<app-generic-table <app-generic-table
[data]="filteredProducts" [data]="filteredProducts"
[columns]="visibleColumns" [columns]="visibleTableColumns"
(edit)="editProduct.emit($event.id)" (edit)="onEditProduct($event.id)"
(delete)="deleteProduct.emit($event.id)"> (delete)="onDeleteProduct($event.id)">
</app-generic-table> </app-generic-table>
</div>

View File

@@ -1,8 +1,13 @@
// /src/app/features/admin/components/products/product-list/product-list.component.ts // /src/app/features/admin/components/products/product-list/product-list.component.ts
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { Router } from '@angular/router';
import { AdminProduct } from '../../../../core/models/product.model'; import { CommonModule, CurrencyPipe, DatePipe } from '@angular/common';
import { AdminProduct, ProductImage } from '../../../../core/models/product.model';
import { ProductService } from '../../../services/product.service';
import { SupplierService } from '../../../services/supplier.service';
import { SnackbarService } from '../../../../shared/services/snackbar.service';
import { StorageService } from '../../../../core/services/storage.service';
import { GenericTableComponent, ColumnConfig } from '../../../../shared/components/data-display/generic-table/generic-table.component'; 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 { SearchBarComponent } from '../../../../shared/components/layout/search-bar/search-bar.component';
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
@@ -11,35 +16,96 @@ import { IconComponent } from '../../../../shared/components/ui/icon/icon.compon
@Component({ @Component({
selector: 'app-product-list', selector: 'app-product-list',
standalone: true, standalone: true,
imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent], imports: [CommonModule, CurrencyPipe, DatePipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
providers: [DatePipe],
templateUrl: './product-list.component.html', templateUrl: './product-list.component.html',
styleUrl: './product-list.component.css' styleUrl: './product-list.component.css'
}) })
export class ProductListComponent implements OnChanges { export class ProductListComponent implements OnInit {
// --- Inputs & Outputs --- private productService = inject(ProductService);
@Input() products: (AdminProduct & { mainImage?: string; supplierName?: string })[] = []; private supplierService = inject(SupplierService);
@Input() allColumns: ColumnConfig[] = []; private router = inject(Router);
@Input() visibleColumns: ColumnConfig[] = []; private snackbar = inject(SnackbarService);
private storageService = inject(StorageService);
private datePipe = inject(DatePipe);
@Output() addNew = new EventEmitter<void>(); private readonly TABLE_SETTINGS_KEY = 'product-table-columns';
@Output() editProduct = new EventEmitter<string>();
@Output() deleteProduct = new EventEmitter<string>();
@Output() columnsChange = new EventEmitter<ColumnConfig[]>();
@Output() searchChange = new EventEmitter<string>();
allProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
filteredProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = []; filteredProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
isColumnFilterVisible = false; isColumnFilterVisible = false;
constructor() {} 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: 'actions', title: 'Aktionen', type: 'actions', cssClass: 'text-right' }
];
visibleTableColumns: ColumnConfig[] = [];
ngOnChanges(changes: SimpleChanges): void { constructor() {
if (changes['products']) { this.loadTableSettings();
this.filteredProducts = this.products ? [...this.products] : [];
} }
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 || '-',
createdDate: this.datePipe.transform(p.createdDate, 'dd.MM.yyyy HH:mm') || '-',
};
});
this.onSearch('');
});
});
} }
onSearch(term: string): void { onSearch(term: string): void {
this.searchChange.emit(term); const lowerTerm = term.toLowerCase();
this.filteredProducts = this.allProducts.filter(p =>
p.name?.toLowerCase().includes(lowerTerm) ||
p.sku?.toLowerCase().includes(lowerTerm)
);
}
onAddNew(): void {
this.router.navigate(['/shop/products/new']);
}
onEditProduct(productId: string): void {
this.router.navigate(['/shop/products', productId]);
}
onDeleteProduct(productId: string): void {
if (confirm('Möchten Sie dieses Produkt wirklich löschen?')) {
this.productService.delete(productId).subscribe(() => {
this.snackbar.show('Produkt erfolgreich gelöscht.');
this.loadProducts();
});
}
}
private loadTableSettings(): void {
const savedKeys = this.storageService.getItem<string[]>(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));
}
private saveTableSettings(): void {
const visibleKeys = this.visibleTableColumns.map(c => c.key);
this.storageService.setItem(this.TABLE_SETTINGS_KEY, visibleKeys);
} }
toggleColumnFilter(): void { toggleColumnFilter(): void {
@@ -47,22 +113,26 @@ export class ProductListComponent implements OnChanges {
} }
isColumnVisible(columnKey: string): boolean { isColumnVisible(columnKey: string): boolean {
return this.visibleColumns.some(c => c.key === columnKey); return this.visibleTableColumns.some(c => c.key === columnKey);
} }
onColumnToggle(column: ColumnConfig, event: Event): void { onColumnToggle(column: ColumnConfig, event: Event): void {
const checkbox = event.target as HTMLInputElement; const checkbox = event.target as HTMLInputElement;
let newVisibleColumns: ColumnConfig[];
if (checkbox.checked) { if (checkbox.checked) {
newVisibleColumns = [...this.visibleColumns, column]; this.visibleTableColumns.push(column);
newVisibleColumns.sort((a, b) => this.visibleTableColumns.sort((a, b) =>
this.allColumns.findIndex(c => c.key === a.key) - this.allTableColumns.findIndex(c => c.key === a.key) -
this.allColumns.findIndex(c => c.key === b.key) this.allTableColumns.findIndex(c => c.key === b.key)
); );
} else { } else {
newVisibleColumns = this.visibleColumns.filter(c => c.key !== column.key); this.visibleTableColumns = this.visibleTableColumns.filter(c => c.key !== column.key);
} }
this.columnsChange.emit(newVisibleColumns); this.saveTableSettings();
}
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';
} }
} }

View File

@@ -1,21 +0,0 @@
<div class="page-container">
<h1 class="page-title">Produktverwaltung</h1>
<app-product-edit
*ngIf="currentView === 'edit' || currentView === 'create'"
[productId]="selectedProductId"
(formClose)="onFormClose()">
</app-product-edit>
<app-product-list
*ngIf="currentView === 'list'"
[products]="(filteredProducts)"
[allColumns]="allTableColumns"
[visibleColumns]="visibleTableColumns"
(addNew)="onAddNew()"
(editProduct)="onEditProduct($event)"
(deleteProduct)="onDeleteProduct($event)"
(columnsChange)="onColumnsChange($event)"
(searchChange)="onSearch($event)">
</app-product-list>
</div>

View File

@@ -1,131 +0,0 @@
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<string[]>(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';
}
}

View File

@@ -1,17 +1,23 @@
// /src/app/features/admin/components/products/products.routes.ts
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component'; import { ProductListComponent } from './product-list/product-list.component';
import { GenericTableComponent } from '../../../shared/components/data-display/generic-table/generic-table.component'; import { ProductEditComponent } from './product-edit/product-edit.component';
import { ProductsPageComponent } from './product-page/product-page.component';
export const PRODUCTS_ROUTES: Routes = [ export const PRODUCTS_ROUTES: Routes = [
{ {
path: '', path: '',
component: ProductsPageComponent, component: ProductListComponent,
title: '', title: 'Produktübersicht'
}, },
{ {
path: '1', path: 'new',
component: GenericTableComponent, component: ProductEditComponent,
title: '', title: 'Neues Produkt erstellen'
},
{
path: ':id',
component: ProductEditComponent,
title: 'Produkt bearbeiten'
}, },
]; ];