komponenten
This commit is contained in:
@@ -12,9 +12,15 @@ export const routes: Routes = [
|
||||
redirectTo: 'auth',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
|
||||
{
|
||||
path: 'dashboard',
|
||||
path: 'auth',
|
||||
loadChildren: () =>
|
||||
import('./features/components/auth/auth.routes').then(
|
||||
(r) => r.AUTH_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'shop',
|
||||
component: DefaultLayoutComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { requiredRole: 'Admin' },
|
||||
@@ -26,60 +32,64 @@ export const routes: Routes = [
|
||||
(r) => r.DASHBOARD_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
loadChildren: () =>
|
||||
import('./features/components/categories/categories.routes').then(
|
||||
(r) => r.CATEGORIES_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'discounts',
|
||||
loadChildren: () =>
|
||||
import('./features/components/discounts/discounts.routes').then(
|
||||
(r) => r.DISCOUNTS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
loadChildren: () =>
|
||||
import('./features/components/orders/orders.routes').then(
|
||||
(r) => r.ORDERS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'payment-methods',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./features/components/payment-methods/payment-methods.routes'
|
||||
).then((r) => r.PAYMENT_METHODS_ROUTES),
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
loadChildren: () =>
|
||||
import('./features/components/products/products.routes').then(
|
||||
(r) => r.PRODUCTS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'reviews',
|
||||
loadChildren: () =>
|
||||
import('./features/components/reviews/reviews.routes').then(
|
||||
(r) => r.REVIEWS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
loadChildren: () =>
|
||||
import('./features/components/settings/settings.routes').then(
|
||||
(r) => r.SETTINGS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'shipping-methods',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./features/components/shipping-methods/shipping-methods.routes'
|
||||
).then((r) => r.SHIPPING_METHODS_ROUTES),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
loadChildren: () =>
|
||||
import('./features/components/auth/auth.routes').then(
|
||||
(r) => r.AUTH_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
canActivate: [authGuard],
|
||||
data: { requiredRole: 'Admin' },
|
||||
loadChildren: () =>
|
||||
import('./features/components/categories/categories.routes').then(
|
||||
(r) => r.CATEGORIES_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'discounts',
|
||||
canActivate: [authGuard],
|
||||
data: { requiredRole: 'Admin' },
|
||||
loadChildren: () =>
|
||||
import('./features/components/discounts/discounts.routes').then(
|
||||
(r) => r.DISCOUNTS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
canActivate: [authGuard],
|
||||
data: { requiredRole: 'Admin' },
|
||||
loadChildren: () =>
|
||||
import('./features/components/orders/orders.routes').then(
|
||||
(r) => r.ORDERS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'payment-methods',
|
||||
canActivate: [authGuard],
|
||||
data: { requiredRole: 'Admin' },
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./features/components/payment-methods/payment-methods.routes'
|
||||
).then((r) => r.PAYMENT_METHODS_ROUTES),
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
canActivate: [authGuard],
|
||||
data: { requiredRole: 'Admin' },
|
||||
loadChildren: () =>
|
||||
import('./features/components/products/products.routes').then(
|
||||
(r) => r.PRODUCTS_ROUTES
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'access-denied',
|
||||
component: AccessDeniedComponent,
|
||||
@@ -90,7 +100,6 @@ export const routes: Routes = [
|
||||
component: NotFoundComponent,
|
||||
title: '404 - Seite nicht gefunden',
|
||||
},
|
||||
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '404',
|
||||
|
||||
@@ -67,7 +67,7 @@ export class LoginComponent {
|
||||
if (response && response.isAuthSuccessful) {
|
||||
this.logger.info('Admin login successful', { email: credentials.email });
|
||||
// Erfolgreich eingeloggt -> Weiterleiten zum Admin-Dashboard
|
||||
this.router.navigate(['/dashboard']); // Passe die Route ggf. an
|
||||
this.router.navigate(['/shop']); // Passe die Route ggf. an
|
||||
} else {
|
||||
// Login fehlgeschlagen (falsches Passwort etc.), vom Backend kontrolliert
|
||||
this.errorMessage = 'E-Mail oder Passwort ist ungültig.';
|
||||
|
||||
@@ -1,40 +1,120 @@
|
||||
<div>
|
||||
<h1>Produkte verwalten</h1>
|
||||
|
||||
<!-- Formular -->
|
||||
<form [formGroup]="productForm" (ngSubmit)="onSubmit()">
|
||||
<h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt' }}</h3>
|
||||
<h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt erstellen' }}</h3>
|
||||
|
||||
<!-- Basis-Informationen -->
|
||||
<div><label>Name:</label><input type="text" formControlName="name"></div>
|
||||
<div><label>Slug (automatisch generiert):</label><input type="text" formControlName="slug"></div>
|
||||
|
||||
<!-- +++ SKU-FELD MIT GENERIEREN-BUTTON +++ -->
|
||||
<h4>Basis-Informationen</h4>
|
||||
<div>
|
||||
<label>SKU (Artikelnummer):</label>
|
||||
<label for="name">Name:</label>
|
||||
<input id="name" type="text" formControlName="name">
|
||||
</div>
|
||||
<div>
|
||||
<label for="slug">Slug (automatisch generiert):</label>
|
||||
<input id="slug" type="text" formControlName="slug">
|
||||
</div>
|
||||
<div>
|
||||
<label for="sku">SKU (Artikelnummer):</label>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="text" formControlName="sku" style="flex-grow: 1;">
|
||||
<input id="sku" type="text" formControlName="sku" style="flex-grow: 1;">
|
||||
<button type="button" (click)="generateSku()">Generieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- +++ ENDE NEU +++ -->
|
||||
<div>
|
||||
<label for="description">Beschreibung:</label>
|
||||
<textarea id="description" formControlName="description" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<div><label>Beschreibung:</label><textarea formControlName="description" rows="5"></textarea></div>
|
||||
<hr>
|
||||
|
||||
<!-- Preis & Lager -->
|
||||
<h4>Preis & Lager</h4>
|
||||
<div>
|
||||
<label for="price">Preis (€):</label>
|
||||
<input id="price" type="number" formControlName="price">
|
||||
</div>
|
||||
<div>
|
||||
<label for="oldPrice">Alter Preis (€) (optional):</label>
|
||||
<input id="oldPrice" type="number" formControlName="oldPrice">
|
||||
</div>
|
||||
<div>
|
||||
<label for="purchasePrice">Einkaufspreis (€) (optional):</label>
|
||||
<input id="purchasePrice" type="number" formControlName="purchasePrice">
|
||||
</div>
|
||||
<div>
|
||||
<label for="stock">Lagerbestand:</label>
|
||||
<input id="stock" type="number" formControlName="stockQuantity">
|
||||
</div>
|
||||
<div>
|
||||
<label for="weight">Gewicht (in kg) (optional):</label>
|
||||
<input id="weight" type="number" formControlName="weight">
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Zuweisungen -->
|
||||
<h4>Zuweisungen</h4>
|
||||
<div>
|
||||
<label for="supplier">Lieferant:</label>
|
||||
<select id="supplier" formControlName="supplierId">
|
||||
<option [ngValue]="null">-- Kein Lieferant --</option>
|
||||
<option *ngFor="let supplier of allSuppliers$ | async" [value]="supplier.id">{{ supplier.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Kategorien:</label>
|
||||
<div class="category-checkbox-group" style="height: 100px; overflow-y: auto; border: 1px solid #ccc; padding: 5px; margin-top: 5px;">
|
||||
<div *ngFor="let category of allCategories$ | async">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[value]="category.id"
|
||||
[checked]="isCategorySelected(category.id)"
|
||||
(change)="onCategoryChange($event)">
|
||||
{{ category.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ... (alle anderen Felder bleiben unverändert) ... -->
|
||||
<hr>
|
||||
<div><label>Preis (€):</label><input type="number" formControlName="price"></div>
|
||||
<div><label>Alter Preis (€) (optional):</label><input type="number" formControlName="oldPrice"></div>
|
||||
<div><label>Einkaufspreis (€) (optional):</label><input type="number" formControlName="purchasePrice"></div>
|
||||
<div><label>Lagerbestand:</label><input type="number" formControlName="stockQuantity"></div>
|
||||
<div><label>Gewicht (in kg) (optional):</label><input type="number" formControlName="weight"></div>
|
||||
<hr>
|
||||
<div><label>Lieferant:</label><select formControlName="supplierId"><option [ngValue]="null">-- Kein Lieferant --</option><option *ngFor="let supplier of allSuppliers$ | async" [value]="supplier.id">{{ supplier.name }}</option></select></div>
|
||||
<div><label>Kategorien:</label><div class="category-checkbox-group" style="height: 100px; overflow-y: auto; border: 1px solid #ccc; padding: 5px;"><div *ngFor="let category of allCategories$ | async"><label><input type="checkbox" [value]="category.id" [checked]="isCategorySelected(category.id)" (change)="onCategoryChange($event)">{{ category.name }}</label></div></div></div>
|
||||
|
||||
<!-- Bilder -->
|
||||
<h4>Produktbilder</h4>
|
||||
<div *ngIf="selectedProductId && existingImages.length > 0">
|
||||
<p>Bestehende Bilder:</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px;">
|
||||
<div *ngFor="let img of existingImages" style="position: relative;">
|
||||
<img [src]="img.url" [alt]="productForm.get('name')?.value"
|
||||
style="width: 100px; height: 100px; object-fit: cover; border: 2px solid"
|
||||
[style.borderColor]="img.isMainImage ? 'green' : 'gray'">
|
||||
<button (click)="deleteExistingImage(img.id, $event)"
|
||||
style="position: absolute; top: -5px; right: -5px; background: red; color: white; border-radius: 50%; width: 20px; height: 20px; border: none; cursor: pointer; line-height: 20px; text-align: center; padding: 0;">X</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="main-image">Hauptbild (ersetzt bestehendes Hauptbild)</label>
|
||||
<input id="main-image" type="file" (change)="onMainFileChange($event)" accept="image/*">
|
||||
</div>
|
||||
<div>
|
||||
<label for="additional-images">Zusätzliche Bilder hinzufügen</label>
|
||||
<input id="additional-images" type="file" (change)="onAdditionalFilesChange($event)" accept="image/*" multiple>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Status & Sichtbarkeit -->
|
||||
<h4>Status & Sichtbarkeit</h4>
|
||||
<div><label><input type="checkbox" formControlName="isActive"> Aktiv (im Shop sichtbar)</label></div>
|
||||
<div><label><input type="checkbox" formControlName="isFeatured"> Hervorgehoben (z.B. auf Startseite)</label></div>
|
||||
<div><label>Anzeigereihenfolge (Hervorgehoben):</label><input type="number" formControlName="featuredDisplayOrder"></div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button type="submit" [disabled]="productForm.invalid">{{ selectedProductId ? 'Aktualisieren' : 'Erstellen' }}</button>
|
||||
<button type="button" *ngIf="selectedProductId" (click)="clearSelection()">Abbrechen</button>
|
||||
</form>
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
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';
|
||||
|
||||
// ... (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 { AdminProduct } from '../../../../core/models/product.model';
|
||||
import { Category } from '../../../../core/models/category.model';
|
||||
import { Supplier } from '../../../../core/models/supplier.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, CurrencyPipe],
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './product-list.component.html',
|
||||
})
|
||||
export class ProductListComponent implements OnInit, OnDestroy {
|
||||
// ... (alle Properties und der Konstruktor bleiben gleich)
|
||||
private productService = inject(ProductService);
|
||||
private categoryService = inject(CategoryService);
|
||||
private supplierService = inject(SupplierService);
|
||||
@@ -33,6 +44,11 @@ export class ProductListComponent implements OnInit, OnDestroy {
|
||||
selectedProductId: string | null = null;
|
||||
private nameChangeSubscription?: Subscription;
|
||||
|
||||
// Eigenschaften für das Bild-Management
|
||||
existingImages: ProductImage[] = [];
|
||||
mainImageFile: File | null = null;
|
||||
additionalImageFiles: File[] = [];
|
||||
|
||||
constructor() {
|
||||
this.productForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
@@ -48,37 +64,205 @@ export class ProductListComponent implements OnInit, OnDestroy {
|
||||
isFeatured: [false],
|
||||
featuredDisplayOrder: [0],
|
||||
supplierId: [null],
|
||||
categorieIds: this.fb.array([])
|
||||
categorieIds: this.fb.array([]),
|
||||
imagesToDelete: this.fb.array([]), // FormArray für die IDs der zu löschenden Bilder
|
||||
});
|
||||
}
|
||||
|
||||
// --- NEUE METHODE ZUM GENERIEREN DER SKU ---
|
||||
// Getter für einfachen Zugriff auf FormArrays
|
||||
get categorieIds(): FormArray {
|
||||
return this.productForm.get('categorieIds') as FormArray;
|
||||
}
|
||||
|
||||
get imagesToDelete(): FormArray {
|
||||
return this.productForm.get('imagesToDelete') as FormArray;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadInitialData();
|
||||
this.subscribeToNameChanges();
|
||||
}
|
||||
|
||||
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 {
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
const categoryId = checkbox.value;
|
||||
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
|
||||
);
|
||||
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';
|
||||
// Nimmt die ersten 4 Buchstaben des Namens (oder weniger, falls kürzer)
|
||||
const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
|
||||
// Hängt einen zufälligen, 6-stelligen alphanumerischen String an
|
||||
const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
const sku = `${prefix}-${randomPart}`;
|
||||
this.productForm.get('sku')?.setValue(sku);
|
||||
}
|
||||
// --- ENDE NEUE METHODE ---
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.productForm.invalid) return;
|
||||
|
||||
get categorieIds(): FormArray {
|
||||
return this.productForm.get('categorieIds') as FormArray;
|
||||
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());
|
||||
}
|
||||
}
|
||||
ngOnInit(): void { this.loadInitialData(); this.subscribeToNameChanges(); }
|
||||
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))); }
|
||||
clearSelection(): void { this.selectedProductId = null; this.productForm.reset({ price: 0, stockQuantity: 0, isActive: true, isFeatured: false, featuredDisplayOrder: 0 }); this.categorieIds.clear(); }
|
||||
onCategoryChange(event: Event): void { const checkbox = event.target as HTMLInputElement; const categoryId = checkbox.value; 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); if (index !== -1) { this.categorieIds.removeAt(index); } } }
|
||||
isCategorySelected(categoryId: string): boolean { return this.categorieIds.value.includes(categoryId); }
|
||||
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') { (value as string[]).forEach(id => formData.append('CategorieIds', id)); } else if (value !== null && value !== undefined && value !== '') { if (['oldPrice', 'purchasePrice', 'weight'].includes(key) && value === '') return; formData.append(key, value); } }); 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()); } }
|
||||
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, '-'); }
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,60 @@
|
||||
<!-- /src/app/features/admin/components/reviews/review-list/review-list.component.html -->
|
||||
|
||||
<div>
|
||||
<h1>Bewertungen verwalten</h1>
|
||||
<table>
|
||||
|
||||
<table class="review-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kunde</th>
|
||||
<th>Produkt</th>
|
||||
<th>Bewertung</th>
|
||||
<th>Titel</th>
|
||||
<th>Datum</th>
|
||||
<th>Status</th>
|
||||
<th>Produkt</th>
|
||||
<th>Kunde</th>
|
||||
<th>Bewertung (1-5)</th>
|
||||
<th>Titel / Kommentar</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let review of reviews$ | async">
|
||||
<td>{{ review.customerName }}</td>
|
||||
<td>{{ review.productName }}</td>
|
||||
<td>{{ review.rating }}/5</td>
|
||||
<td>{{ review.title }}</td>
|
||||
<td>{{ review.isApproved ? "Freigegeben" : "Ausstehend" }}</td>
|
||||
<td>
|
||||
<button *ngIf="!review.isApproved" (click)="onApprove(review.id)">
|
||||
Freigeben
|
||||
</button>
|
||||
<button (click)="onDelete(review.id)">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Ladeanzeige oder Leere-Liste-Nachricht -->
|
||||
<ng-container *ngIf="reviews$ | async as reviews; else loading">
|
||||
<tr *ngIf="reviews.length === 0">
|
||||
<td colspan="7">Keine Bewertungen gefunden.</td>
|
||||
</tr>
|
||||
|
||||
<!-- Iteration über die Bewertungen -->
|
||||
<tr *ngFor="let review of reviews">
|
||||
<td>{{ review.reviewDate | date:'dd.MM.yyyy HH:mm' }}</td>
|
||||
<td>
|
||||
<span [class.status-approved]="review.isApproved" [class.status-pending]="!review.isApproved">
|
||||
{{ review.isApproved ? 'Freigegeben' : 'Ausstehend' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ review.productName || 'N/A' }}</td>
|
||||
<td>{{ review.customerName }}</td>
|
||||
<td>{{ review.rating }}</td>
|
||||
<td class="comment-cell">
|
||||
<strong>{{ review.title }}</strong>
|
||||
<p>{{ review.comment }}</p>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<button *ngIf="!review.isApproved" (click)="onApprove(review.id)" class="approve-btn">
|
||||
Freigeben
|
||||
</button>
|
||||
<button (click)="onDelete(review.id)" class="delete-btn">
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Template für den Ladezustand -->
|
||||
<ng-template #loading>
|
||||
<tr>
|
||||
<td colspan="7">Lade Bewertungen...</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
@@ -1,14 +1,19 @@
|
||||
// /src/app/features/admin/components/reviews/review-list/review-list.component.ts
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// Models & Services
|
||||
import { Review } from '../../../../core/models/review.model';
|
||||
import { ReviewService } from '../../../services/review.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-review-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, DatePipe], // DatePipe für die Formatierung des Datums hinzufügen
|
||||
templateUrl: './review-list.component.html',
|
||||
styleUrl: './review-list.component.css' // Optional: CSS für besseres Tabellen-Layout
|
||||
})
|
||||
export class ReviewListComponent implements OnInit {
|
||||
private reviewService = inject(ReviewService);
|
||||
@@ -23,12 +28,26 @@ export class ReviewListComponent implements OnInit {
|
||||
}
|
||||
|
||||
onApprove(id: string): void {
|
||||
this.reviewService.approve(id).subscribe(() => this.loadReviews());
|
||||
if (confirm('Möchten Sie diese Bewertung wirklich freigeben?')) {
|
||||
this.reviewService.approve(id).subscribe({
|
||||
next: () => {
|
||||
console.log(`Review ${id} approved successfully.`);
|
||||
this.loadReviews(); // Liste neu laden, um den geänderten Status anzuzeigen
|
||||
},
|
||||
error: (err) => console.error(`Failed to approve review ${id}`, err)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDelete(id: string): void {
|
||||
if (confirm('Bewertung wirklich löschen?')) {
|
||||
this.reviewService.delete(id).subscribe(() => this.loadReviews());
|
||||
if (confirm('Möchten Sie diese Bewertung wirklich endgültig löschen?')) {
|
||||
this.reviewService.delete(id).subscribe({
|
||||
next: () => {
|
||||
console.log(`Review ${id} deleted successfully.`);
|
||||
this.loadReviews(); // Liste neu laden
|
||||
},
|
||||
error: (err) => console.error(`Failed to delete review ${id}`, err)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/app/features/components/reviews/reviews.routes.ts
Normal file
10
src/app/features/components/reviews/reviews.routes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ReviewListComponent } from './review-list/review-list.component';
|
||||
|
||||
export const REVIEWS_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ReviewListComponent,
|
||||
title: '',
|
||||
},
|
||||
];
|
||||
10
src/app/features/components/settings/settings.routes.ts
Normal file
10
src/app/features/components/settings/settings.routes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
|
||||
export const SETTINGS_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: SettingsComponent,
|
||||
title: '',
|
||||
},
|
||||
];
|
||||
@@ -39,7 +39,7 @@ export class SettingsComponent implements OnInit {
|
||||
key: [setting.key],
|
||||
value: [setting.value],
|
||||
isActive: [setting.isActive]
|
||||
}));
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,70 @@
|
||||
<div>
|
||||
<h1>Versandmethoden verwalten</h1>
|
||||
|
||||
<form [formGroup]="methodForm" (ngSubmit)="onSubmit()">
|
||||
<h3>{{ selectedMethodId ? 'Methode bearbeiten' : 'Neue Methode' }}</h3>
|
||||
<input type="text" formControlName="name" placeholder="Name">
|
||||
<textarea formControlName="description" placeholder="Beschreibung"></textarea>
|
||||
<input type="number" formControlName="cost" placeholder="Kosten">
|
||||
<label><input type="checkbox" formControlName="isActive"> Aktiv</label>
|
||||
<button type="submit" [disabled]="methodForm.invalid">{{ selectedMethodId ? 'Aktualisieren' : 'Erstellen' }}</button>
|
||||
<button type="button" *ngIf="selectedMethodId" (click)="clearSelection()">Abbrechen</button>
|
||||
<h3>{{ selectedMethodId ? "Methode bearbeiten" : "Neue Methode" }}</h3>
|
||||
|
||||
<div>
|
||||
<label>Name:</label>
|
||||
<input type="text" formControlName="name" placeholder="Name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Beschreibung:</label>
|
||||
<textarea
|
||||
formControlName="description"
|
||||
placeholder="Beschreibung"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Kosten (€):</label>
|
||||
<input type="number" formControlName="cost" placeholder="Kosten" />
|
||||
</div>
|
||||
|
||||
<!-- +++ NEUE FELDER HINZUGEFÜGT +++ -->
|
||||
<div>
|
||||
<label>Minimale Liefertage:</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="minDeliveryDays"
|
||||
placeholder="z.B. 1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Maximale Liefertage:</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="maxDeliveryDays"
|
||||
placeholder="z.B. 3"
|
||||
/>
|
||||
</div>
|
||||
<!-- +++ ENDE NEU +++ -->
|
||||
|
||||
<div>
|
||||
<label><input type="checkbox" formControlName="isActive" /> Aktiv</label>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<button type="submit" [disabled]="methodForm.invalid">
|
||||
{{ selectedMethodId ? "Aktualisieren" : "Erstellen" }}
|
||||
</button>
|
||||
<button type="button" *ngIf="selectedMethodId" (click)="clearSelection()">
|
||||
Abbrechen
|
||||
</button>
|
||||
</form>
|
||||
<hr>
|
||||
<hr />
|
||||
<h2>Bestehende Methoden</h2>
|
||||
<ul>
|
||||
<li *ngFor="let method of methods$ | async">
|
||||
{{ method.name }} ({{ method.cost | currency:'EUR' }}) - Aktiv: {{ method.isActive }}
|
||||
{{ method.name }}
|
||||
({{ method.cost | currency : "EUR" }}) - Lieferzeit:
|
||||
{{ method.minDeliveryDays }}-{{ method.maxDeliveryDays }} Tage - Aktiv:
|
||||
{{ method.isActive }}
|
||||
<button (click)="selectMethod(method)">Bearbeiten</button>
|
||||
<button (click)="onDelete(method.id)">Löschen</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ShippingMethod } from '../../../../core/models/shipping.model';
|
||||
@@ -8,7 +8,7 @@ import { ShippingMethodService } from '../../../services/shipping-method.service
|
||||
@Component({
|
||||
selector: 'app-shipping-method-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
imports: [CommonModule, ReactiveFormsModule, CurrencyPipe],
|
||||
templateUrl: './shipping-method-list.component.html',
|
||||
})
|
||||
export class ShippingMethodListComponent implements OnInit {
|
||||
@@ -24,7 +24,9 @@ export class ShippingMethodListComponent implements OnInit {
|
||||
name: ['', Validators.required],
|
||||
description: [''],
|
||||
cost: [0, [Validators.required, Validators.min(0)]],
|
||||
isActive: [true]
|
||||
isActive: [true],
|
||||
minDeliveryDays: [1, [Validators.required, Validators.min(0)]],
|
||||
maxDeliveryDays: [3, [Validators.required, Validators.min(0)]]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,12 +40,25 @@ export class ShippingMethodListComponent implements OnInit {
|
||||
|
||||
clearSelection(): void {
|
||||
this.selectedMethodId = null;
|
||||
this.methodForm.reset({ isActive: true });
|
||||
this.methodForm.reset({
|
||||
name: '',
|
||||
description: '',
|
||||
cost: 0,
|
||||
isActive: true,
|
||||
minDeliveryDays: 1,
|
||||
maxDeliveryDays: 3
|
||||
});
|
||||
}
|
||||
|
||||
// --- KORREKTUR: onSubmit sendet jetzt direkt das Formularwert-Objekt als JSON ---
|
||||
onSubmit(): void {
|
||||
if (this.methodForm.invalid) return;
|
||||
const dataToSend = { ...this.methodForm.value, id: this.selectedMethodId || '0' };
|
||||
|
||||
// Das Formular-Objekt hat bereits die richtige Struktur, die das Backend erwartet.
|
||||
const dataToSend: ShippingMethod = {
|
||||
id: this.selectedMethodId || '00000000-0000-0000-0000-000000000000',
|
||||
...this.methodForm.value
|
||||
};
|
||||
|
||||
if (this.selectedMethodId) {
|
||||
this.shippingMethodService.update(this.selectedMethodId, dataToSend).subscribe(() => this.reset());
|
||||
@@ -51,6 +66,7 @@ export class ShippingMethodListComponent implements OnInit {
|
||||
this.shippingMethodService.create(dataToSend).subscribe(() => this.reset());
|
||||
}
|
||||
}
|
||||
// --- ENDE KORREKTUR ---
|
||||
|
||||
onDelete(id: string): void {
|
||||
if (confirm('Versandmethode wirklich löschen?')) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ShippingMethodListComponent } from './shipping-method-list/shipping-method-list.component';
|
||||
|
||||
export const SHIPPING_METHODS_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ShippingMethodListComponent,
|
||||
title: '',
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user