products
This commit is contained in:
@@ -1,127 +1,108 @@
|
||||
<form [formGroup]="productForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
<div class="form-header">
|
||||
<h3 card-header>
|
||||
{{ isEditMode ? "Produkt bearbeiten" : "Neues Produkt erstellen" }}
|
||||
</h3>
|
||||
<!-- /src/app/features/admin/components/products/product-edit/product-edit.component.html -->
|
||||
|
||||
<app-card>
|
||||
<div *ngIf="isLoading; else formContent" class="loading-container">
|
||||
<p>Lade Produktdaten...</p>
|
||||
</div>
|
||||
|
||||
<!-- Sektion 1: Basis-Informationen -->
|
||||
<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">
|
||||
<label class="form-label">SKU (Artikelnummer)</label>
|
||||
<div class="input-with-button">
|
||||
<input type="text" class="form-input" formControlName="sku" />
|
||||
<app-button buttonType="secondary" (click)="generateSku()"
|
||||
>Generieren</app-button
|
||||
>
|
||||
<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-field">
|
||||
<label class="form-label">SKU (Artikelnummer)</label>
|
||||
<div class="input-with-button">
|
||||
<input type="text" class="form-input" formControlName="sku" />
|
||||
<app-button buttonType="icon" (click)="generateSku()" iconName="placeholder"></app-button>
|
||||
</div>
|
||||
</div>
|
||||
<app-form-field label="Lagerbestand" type="number" formControlName="stockQuantity"></app-form-field>
|
||||
<app-form-field label="Gewicht (kg)" type="number" formControlName="weight"></app-form-field>
|
||||
<app-form-select label="Lieferant" [options]="(supplierOptions$ | async) || []" formControlName="supplierId"></app-form-select>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<app-card>
|
||||
<h4 card-header>Kategorien</h4>
|
||||
<div class="form-section">
|
||||
<div class="multi-select-container">
|
||||
<div class="selected-pills">
|
||||
<span *ngFor="let catId of categorieIds.value" class="pill">
|
||||
{{ getCategoryName(catId) }}
|
||||
<app-icon iconName="x" (click)="removeCategoryById(catId)"></app-icon>
|
||||
</span>
|
||||
<span *ngIf="categorieIds.length === 0" class="placeholder">Keine ausgewählt</span>
|
||||
</div>
|
||||
<div class="category-checkbox-group">
|
||||
<label *ngFor="let category of allCategories$ | async">
|
||||
<input type="checkbox" [value]="category.id" [checked]="isCategorySelected(category.id)" (change)="onCategoryChange($event)">
|
||||
{{ category.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<app-card>
|
||||
<h4 card-header>Hervorheben</h4>
|
||||
<div class="form-section">
|
||||
<app-slide-toggle formControlName="isFeatured">Auf Startseite anzeigen</app-slide-toggle>
|
||||
<app-form-field *ngIf="productForm.get('isFeatured')?.value" label="Anzeigereihenfolge">
|
||||
<input type="number" formControlName="featuredDisplayOrder">
|
||||
</app-form-field>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
<app-form-textarea
|
||||
class="grid-col-span-2"
|
||||
label="Beschreibung"
|
||||
[rows]="5"
|
||||
formControlName="description"
|
||||
></app-form-textarea>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Sektion 2: Preis & Lager -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Preis & Lager</h4>
|
||||
<div class="form-grid">
|
||||
<app-form-field label="Preis (€)"
|
||||
><input type="number" formControlName="price"
|
||||
/></app-form-field>
|
||||
<app-form-field label="Alter Preis (€)"
|
||||
><input type="number" formControlName="oldPrice"
|
||||
/></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>
|
||||
<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
|
||||
*ngFor="let category of allCategories$ | async"
|
||||
class="checkbox-item"
|
||||
>
|
||||
<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 class="form-actions">
|
||||
<app-button buttonType="stroked" (click)="cancel()">Abbrechen</app-button>
|
||||
<app-button submitType="submit" buttonType="primary" [disabled]="productForm.invalid || isLoading">
|
||||
{{ isEditMode ? "Änderungen speichern" : "Produkt erstellen" }}
|
||||
</app-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Sektion 4: Bilder -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Produktbilder</h4>
|
||||
<!-- ... (Bild-Management-Logik hier einfügen) ... -->
|
||||
</div>
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- 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 class="form-actions">
|
||||
<app-button buttonType="stroked" (click)="cancel()">Abbrechen</app-button>
|
||||
<app-button
|
||||
submitType="submit"
|
||||
buttonType="primary"
|
||||
[disabled]="productForm.invalid"
|
||||
>
|
||||
{{ isEditMode ? "Änderungen speichern" : "Produkt erstellen" }}
|
||||
</app-button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</ng-template>
|
||||
</app-card>
|
||||
@@ -1,19 +1,24 @@
|
||||
// /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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
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 { 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 { SnackbarService } from '../../../../shared/services/snackbar.service';
|
||||
|
||||
// Wiederverwendbare UI- & Form-Komponenten
|
||||
import { CardComponent } from '../../../../shared/components/ui/card/card.component';
|
||||
import { ButtonComponent } from '../../../../shared/components/ui/button/button.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'
|
||||
})
|
||||
export class ProductEditComponent implements OnInit, OnDestroy {
|
||||
@Input() productId: string | null = null;
|
||||
@Output() formClose = new EventEmitter<void>();
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private productService = inject(ProductService);
|
||||
private categoryService = inject(CategoryService);
|
||||
private supplierService = inject(SupplierService);
|
||||
private fb = inject(FormBuilder);
|
||||
private snackbarService = inject(SnackbarService);
|
||||
|
||||
productId: string | null = null;
|
||||
isEditMode = false;
|
||||
isLoading = true;
|
||||
productForm: FormGroup;
|
||||
|
||||
allCategories$!: Observable<Category[]>;
|
||||
supplierOptions$!: Observable<SelectOption[]>;
|
||||
allCategories: Category[] = [];
|
||||
|
||||
private nameChangeSubscription?: Subscription;
|
||||
existingImages: ProductImage[] = [];
|
||||
@@ -65,13 +72,22 @@ export class ProductEditComponent implements OnInit, OnDestroy {
|
||||
get imagesToDelete(): FormArray { return this.productForm.get('imagesToDelete') as FormArray; }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.productId = this.route.snapshot.paramMap.get('id');
|
||||
this.isEditMode = !!this.productId;
|
||||
|
||||
this.loadDropdownData();
|
||||
this.subscribeToNameChanges();
|
||||
|
||||
if (this.isEditMode && this.productId) {
|
||||
this.isLoading = true;
|
||||
this.productService.getById(this.productId).subscribe(product => {
|
||||
this.selectProduct(product);
|
||||
if (product) {
|
||||
this.populateForm(product);
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,16 +95,17 @@ export class ProductEditComponent implements OnInit, OnDestroy {
|
||||
this.nameChangeSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
loadDropdownData(): void {
|
||||
this.allCategories$ = this.categoryService.getAll();
|
||||
// --- HIER IST DIE KORREKTUR ---
|
||||
loadDropdownData(): void {
|
||||
this.allCategories$ = this.categoryService.getAll().pipe(
|
||||
tap(categories => this.allCategories = categories)
|
||||
);
|
||||
this.supplierOptions$ = this.supplierService.getAll().pipe(
|
||||
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.categorieIds.clear();
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const name = this.productForm.get('name')?.value || 'PROD';
|
||||
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);
|
||||
this.additionalImageFiles.forEach((file) => formData.append('AdditionalImageFiles', file));
|
||||
|
||||
// KORREKTUR: Explizites Casten zu einem gemeinsamen Typ (any) hilft TypeScript
|
||||
const operation: Observable<any> = this.isEditMode
|
||||
? this.productService.update(this.productId!, formData)
|
||||
: this.productService.create(formData);
|
||||
|
||||
// KORREKTUR: Korrekte subscribe-Syntax mit Objekt
|
||||
operation.subscribe({
|
||||
next: () => {
|
||||
this.snackbarService.show(this.isEditMode ? 'Produkt erfolgreich aktualisiert' : 'Produkt erfolgreich erstellt');
|
||||
this.formClose.emit();
|
||||
this.snackbarService.show(this.isEditMode ? 'Produkt aktualisiert' : 'Produkt erstellt');
|
||||
this.router.navigate(['/admin/products']);
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.formClose.emit();
|
||||
this.router.navigate(['/admin/products']);
|
||||
}
|
||||
|
||||
private subscribeToNameChanges(): void {
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<!-- /src/app/features/admin/components/products/product-list/product-list.component.html -->
|
||||
|
||||
<div class="table-header">
|
||||
<app-search-bar placeholder="Produkte nach Name oder SKU suchen..." (search)="onSearch($event)"></app-search-bar>
|
||||
<div class="header-actions">
|
||||
<div class="page-container">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Produktübersicht</h1>
|
||||
<app-button buttonType="primary" (click)="onAddNew()">
|
||||
<app-icon iconName="plus"></app-icon> Neues Produkt
|
||||
</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>
|
||||
<app-button buttonType="stroked" (click)="toggleColumnFilter()" iconName="filter">Spalten</app-button>
|
||||
<div class="column-filter-dropdown" *ngIf="isColumnFilterVisible">
|
||||
<!-- Iteriert jetzt über allColumns, das vom Parent kommt -->
|
||||
<label *ngFor="let col of allColumns">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isColumnVisible(col.key)"
|
||||
(change)="onColumnToggle(col, $event)">
|
||||
<label *ngFor="let col of allTableColumns">
|
||||
<input type="checkbox" [checked]="isColumnVisible(col.key)" (change)="onColumnToggle(col, $event)">
|
||||
{{ col.title }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<app-button buttonType="primary" (click)="addNew.emit()">
|
||||
<app-icon iconName="plus"></app-icon> Neues Produkt
|
||||
</app-button>
|
||||
</div>
|
||||
</div>
|
||||
<app-generic-table
|
||||
[data]="filteredProducts"
|
||||
[columns]="visibleColumns"
|
||||
(edit)="editProduct.emit($event.id)"
|
||||
(delete)="deleteProduct.emit($event.id)">
|
||||
</app-generic-table>
|
||||
|
||||
<app-generic-table
|
||||
[data]="filteredProducts"
|
||||
[columns]="visibleTableColumns"
|
||||
(edit)="onEditProduct($event.id)"
|
||||
(delete)="onDeleteProduct($event.id)">
|
||||
</app-generic-table>
|
||||
</div>
|
||||
@@ -1,8 +1,13 @@
|
||||
// /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 { AdminProduct } from '../../../../core/models/product.model';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
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 { SearchBarComponent } from '../../../../shared/components/layout/search-bar/search-bar.component';
|
||||
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
|
||||
@@ -11,35 +16,96 @@ import { IconComponent } from '../../../../shared/components/ui/icon/icon.compon
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
|
||||
imports: [CommonModule, CurrencyPipe, DatePipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
|
||||
providers: [DatePipe],
|
||||
templateUrl: './product-list.component.html',
|
||||
styleUrl: './product-list.component.css'
|
||||
})
|
||||
export class ProductListComponent implements OnChanges {
|
||||
// --- Inputs & Outputs ---
|
||||
@Input() products: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
|
||||
@Input() allColumns: ColumnConfig[] = [];
|
||||
@Input() visibleColumns: ColumnConfig[] = [];
|
||||
export class ProductListComponent implements OnInit {
|
||||
private productService = inject(ProductService);
|
||||
private supplierService = inject(SupplierService);
|
||||
private router = inject(Router);
|
||||
private snackbar = inject(SnackbarService);
|
||||
private storageService = inject(StorageService);
|
||||
private datePipe = inject(DatePipe);
|
||||
|
||||
@Output() addNew = new EventEmitter<void>();
|
||||
@Output() editProduct = new EventEmitter<string>();
|
||||
@Output() deleteProduct = new EventEmitter<string>();
|
||||
@Output() columnsChange = new EventEmitter<ColumnConfig[]>();
|
||||
@Output() searchChange = new EventEmitter<string>();
|
||||
private readonly TABLE_SETTINGS_KEY = 'product-table-columns';
|
||||
|
||||
allProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
|
||||
filteredProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
|
||||
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 {
|
||||
if (changes['products']) {
|
||||
this.filteredProducts = this.products ? [...this.products] : [];
|
||||
}
|
||||
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 || '-',
|
||||
createdDate: this.datePipe.transform(p.createdDate, 'dd.MM.yyyy HH:mm') || '-',
|
||||
};
|
||||
});
|
||||
this.onSearch('');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -47,22 +113,26 @@ export class ProductListComponent implements OnChanges {
|
||||
}
|
||||
|
||||
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 {
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
let newVisibleColumns: ColumnConfig[];
|
||||
|
||||
if (checkbox.checked) {
|
||||
newVisibleColumns = [...this.visibleColumns, column];
|
||||
newVisibleColumns.sort((a, b) =>
|
||||
this.allColumns.findIndex(c => c.key === a.key) -
|
||||
this.allColumns.findIndex(c => c.key === b.key)
|
||||
this.visibleTableColumns.push(column);
|
||||
this.visibleTableColumns.sort((a, b) =>
|
||||
this.allTableColumns.findIndex(c => c.key === a.key) -
|
||||
this.allTableColumns.findIndex(c => c.key === b.key)
|
||||
);
|
||||
} 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';
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
// /src/app/features/admin/components/products/products.routes.ts
|
||||
|
||||
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';
|
||||
import { ProductEditComponent } from './product-edit/product-edit.component';
|
||||
|
||||
export const PRODUCTS_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProductsPageComponent,
|
||||
title: '',
|
||||
{
|
||||
path: '',
|
||||
component: ProductListComponent,
|
||||
title: 'Produktübersicht'
|
||||
},
|
||||
{
|
||||
path: '1',
|
||||
component: GenericTableComponent,
|
||||
title: '',
|
||||
{
|
||||
path: 'new',
|
||||
component: ProductEditComponent,
|
||||
title: 'Neues Produkt erstellen'
|
||||
},
|
||||
];
|
||||
{
|
||||
path: ':id',
|
||||
component: ProductEditComponent,
|
||||
title: 'Produkt bearbeiten'
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user