komponenten

This commit is contained in:
Tizian.Breuch
2025-09-30 17:56:19 +02:00
parent b88af92095
commit a25aef11fc
10 changed files with 244 additions and 68 deletions

View File

@@ -62,6 +62,24 @@ export const routes: Routes = [
(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,

View File

@@ -1,6 +1,6 @@
import { Routes } from '@angular/router';
import { DashboardPageComponent } from './dashboard-page/dashboard-page.component';
// Importiere dein spezielles Layout für Auth-Seiten und alle Komponenten
import { DashboardComponent } from './dashboard/dashboard.component';
export const DASHBOARD_ROUTES: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
@@ -9,4 +9,9 @@ export const DASHBOARD_ROUTES: Routes = [
component: DashboardPageComponent,
title: 'Dashboard Übersicht',
},
{
path: 'kpi',
component: DashboardComponent,
title: 'kpi Übersicht',
},
];

View File

@@ -11,7 +11,7 @@ import { AnalyticsPeriod } from '../../../../core/enums/shared.enum';
imports: [CommonModule],
templateUrl: './dashboard.component.html',
})
export class AdminDashboardComponent implements OnInit {
export class DashboardComponent implements OnInit {
private analyticsService = inject(AnalyticsService);
analytics$!: Observable<Analytics>;

View File

@@ -3,7 +3,7 @@ import { OrderListComponent } from './order-list/order-list.component';
export const ORDERS_ROUTES: Routes = [
{
path: '',
path: '1',
component: OrderListComponent,
title: '',
},

View File

@@ -2,15 +2,55 @@
<h1>Zahlungsmethoden 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>
<select formControlName="paymentGatewayType">
<div><label for="name">Name:</label>
<input id="name" type="text" formControlName="name" placeholder="Name"></div>
<div><label for="desc">Beschreibung:</label>
<textarea id="desc" formControlName="description" placeholder="Beschreibung"></textarea></div>
<div><label for="fee">Bearbeitungsgebühr (%):</label>
<input id="fee" type="number" formControlName="processingFee" placeholder="z.B. 1.5"></div>
<div><label for="type">Gateway Typ:</label>
<select id="type" formControlName="paymentGatewayType">
<option *ngFor="let type of gatewayTypes" [value]="type">{{ type }}</option>
</select>
<label><input type="checkbox" formControlName="isActive"> Aktiv</label>
</select></div>
<div><label><input type="checkbox" formControlName="isActive"> Aktiv</label></div>
<!-- +++ DYNAMISCHER KONFIGURATIONS-BLOCK +++ -->
<div formGroupName="configuration">
<h4>Konfiguration</h4>
<div *ngIf="methodForm.get('paymentGatewayType')?.value === 'BankTransfer'">
<div><label for="iban">IBAN:</label><input id="iban" formControlName="IBAN"></div>
<div><label for="bic">BIC:</label><input id="bic" formControlName="BIC"></div>
<div><label for="bankName">Bankname:</label><input id="bankName" formControlName="BankName"></div>
</div>
<div *ngIf="methodForm.get('paymentGatewayType')?.value === 'Stripe'">
<div><label for="stripePk">Public Key:</label><input id="stripePk" formControlName="PublicKey"></div>
<div><label for="stripeSk">Secret Key:</label><input id="stripeSk" formControlName="SecretKey"></div>
</div>
<div *ngIf="methodForm.get('paymentGatewayType')?.value === 'PayPal'">
<div><label for="paypalCid">Client ID:</label><input id="paypalCid" formControlName="ClientId"></div>
<div><label for="paypalCs">Client Secret:</label><input id="paypalCs" formControlName="ClientSecret"></div>
</div>
<div *ngIf="methodForm.get('paymentGatewayType')?.value === 'Invoice'">
<div><label for="invoiceTerms">Zahlungsbedingungen:</label><input id="invoiceTerms" formControlName="PaymentTerms"></div>
</div>
</div>
<!-- +++ ENDE DYNAMISCH +++ -->
<br>
<button type="submit" [disabled]="methodForm.invalid">{{ selectedMethodId ? 'Aktualisieren' : 'Erstellen' }}</button>
<button type="button" *ngIf="selectedMethodId" (click)="clearSelection()">Abbrechen</button>
</form>
<hr>
<h2>Bestehende Methoden</h2>
<ul>

View File

@@ -19,33 +19,64 @@ export class PaymentMethodListComponent implements OnInit {
methods$!: Observable<AdminPaymentMethod[]>;
methodForm: FormGroup;
selectedMethodId: string | null = null;
gatewayTypes: PaymentGatewayType[] = ['BankTransfer', 'Invoice', 'PayPal', 'Stripe', 'CashOnDelivery'];
gatewayTypes: PaymentGatewayType[] = ['BankTransfer', 'PayPal', 'Stripe', 'Invoice', 'CashOnDelivery'];
constructor() {
this.methodForm = this.fb.group({
name: ['', Validators.required],
description: [''],
isActive: [true],
paymentGatewayType: ['Invoice', Validators.required]
paymentGatewayType: ['Invoice', Validators.required],
processingFee: [0, Validators.min(0)],
// Verschachteltes FormGroup für die dynamische Konfiguration
configuration: this.fb.group({})
});
}
ngOnInit(): void { this.loadMethods(); }
loadMethods(): void { this.methods$ = this.paymentMethodService.getAll(); }
ngOnInit(): void {
this.loadMethods();
// Beobachte Änderungen am Gateway-Typ, um das Formular dynamisch anzupassen
this.methodForm.get('paymentGatewayType')?.valueChanges.subscribe(type => {
this.updateConfigurationControls(type);
});
// Initialen Zustand für das Formular setzen
this.updateConfigurationControls(this.methodForm.value.paymentGatewayType);
}
// Getter für einfachen Zugriff im Template
get configurationGroup(): FormGroup {
return this.methodForm.get('configuration') as FormGroup;
}
loadMethods(): void {
this.methods$ = this.paymentMethodService.getAll();
}
selectMethod(method: AdminPaymentMethod): void {
this.selectedMethodId = method.id;
// Zuerst die korrekten Controls für den Typ erstellen
this.updateConfigurationControls(method.paymentGatewayType);
// Dann die Werte setzen (patchValue füllt auch verschachtelte Gruppen)
this.methodForm.patchValue(method);
}
clearSelection(): void {
this.selectedMethodId = null;
this.methodForm.reset({ isActive: true, paymentGatewayType: 'Invoice' });
this.methodForm.reset({
name: '',
description: '',
isActive: true,
paymentGatewayType: 'Invoice',
processingFee: 0
});
this.updateConfigurationControls('Invoice');
}
onSubmit(): void {
if (this.methodForm.invalid) return;
const dataToSend = { ...this.methodForm.value, id: this.selectedMethodId || '0' };
const dataToSend = { ...this.methodForm.value, id: this.selectedMethodId || undefined };
if (this.selectedMethodId) {
this.paymentMethodService.update(this.selectedMethodId, dataToSend).subscribe(() => this.reset());
@@ -64,4 +95,33 @@ export class PaymentMethodListComponent implements OnInit {
this.loadMethods();
this.clearSelection();
}
private updateConfigurationControls(type: PaymentGatewayType): void {
const configGroup = this.configurationGroup;
// Alle alten Controls entfernen
Object.keys(configGroup.controls).forEach(key => {
configGroup.removeControl(key);
});
// Neue Controls basierend auf dem Typ hinzufügen
switch (type) {
case 'BankTransfer':
configGroup.addControl('IBAN', this.fb.control('', Validators.required));
configGroup.addControl('BIC', this.fb.control('', Validators.required));
configGroup.addControl('BankName', this.fb.control('', Validators.required));
break;
case 'Stripe':
configGroup.addControl('PublicKey', this.fb.control('', Validators.required));
configGroup.addControl('SecretKey', this.fb.control('', Validators.required));
break;
case 'PayPal':
configGroup.addControl('ClientId', this.fb.control('', Validators.required));
configGroup.addControl('ClientSecret', this.fb.control('', Validators.required));
break;
case 'Invoice':
configGroup.addControl('PaymentTerms', this.fb.control('', Validators.required));
break;
}
}
}

View File

@@ -0,0 +1,10 @@
import { Routes } from '@angular/router';
import { PaymentMethodListComponent } from './payment-method-list/payment-method-list.component';
export const PAYMENT_METHODS_ROUTES: Routes = [
{
path: '',
component: PaymentMethodListComponent,
title: '',
},
];

View File

@@ -1,27 +1,50 @@
<div>
<h1>Produkte verwalten</h1>
<!-- Formular -->
<form [formGroup]="productForm" (ngSubmit)="onSubmit()">
<h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt' }}</h3>
<input type="text" formControlName="name" placeholder="Produktname">
<input type="text" formControlName="slug" placeholder="Slug">
<input type="text" formControlName="sku" placeholder="SKU">
<input type="number" formControlName="price" placeholder="Preis">
<input type="number" formControlName="stockQuantity" placeholder="Lagerbestand">
<label><input type="checkbox" formControlName="isActive"> Aktiv</label>
<label><input type="checkbox" formControlName="isFeatured"> Hervorgehoben</label>
<!-- 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 +++ -->
<div>
<label>SKU (Artikelnummer):</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="text" formControlName="sku" style="flex-grow: 1;">
<button type="button" (click)="generateSku()">Generieren</button>
</div>
</div>
<!-- +++ ENDE NEU +++ -->
<div><label>Beschreibung:</label><textarea formControlName="description" rows="5"></textarea></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>
<hr>
<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>
<hr>
<!-- Liste -->
<h2>Bestehende Produkte</h2>
<ul>
<li *ngFor="let product of products$ | async">
{{ product.name }} (SKU: {{ product.sku }}) - Preis: {{ product.price | number:'1.2-2' }}
{{ product.name }} (SKU: {{ product.sku }}) - Preis: {{ product.price | currency:'EUR' }}
<button (click)="selectProduct(product)">Bearbeiten</button>
<button (click)="onDelete(product.id)">Löschen</button>
</li>

View File

@@ -1,23 +1,37 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { AdminProduct } from '../../../../core/models/product.model';
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 { Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// ... (alle anderen Imports bleiben gleich)
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],
imports: [CommonModule, ReactiveFormsModule, CurrencyPipe],
templateUrl: './product-list.component.html',
})
export class ProductListComponent implements OnInit {
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);
private fb = inject(FormBuilder);
products$!: Observable<AdminProduct[]>;
allCategories$!: Observable<Category[]>;
allSuppliers$!: Observable<Supplier[]>;
productForm: FormGroup;
selectedProductId: string | null = null;
private nameChangeSubscription?: Subscription;
constructor() {
this.productForm = this.fb.group({
@@ -26,49 +40,45 @@ export class ProductListComponent implements OnInit {
sku: ['', Validators.required],
description: [''],
price: [0, [Validators.required, Validators.min(0)]],
oldPrice: [null, [Validators.min(0)]],
purchasePrice: [null, [Validators.min(0)]],
stockQuantity: [0, [Validators.required, Validators.min(0)]],
weight: [null, [Validators.min(0)]],
isActive: [true],
isFeatured: [false]
isFeatured: [false],
featuredDisplayOrder: [0],
supplierId: [null],
categorieIds: this.fb.array([])
});
}
ngOnInit(): void { this.loadProducts(); }
loadProducts(): void { this.products$ = this.productService.getAll(); }
selectProduct(product: AdminProduct): void {
this.selectedProductId = product.id;
this.productForm.patchValue(product);
// --- NEUE METHODE ZUM GENERIEREN DER SKU ---
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 ---
clearSelection(): void {
this.selectedProductId = null;
this.productForm.reset({ isActive: true, isFeatured: false });
get categorieIds(): FormArray {
return this.productForm.get('categorieIds') 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))); }
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, '-'); }
onSubmit(): void {
if (this.productForm.invalid) return;
const formData = new FormData();
Object.keys(this.productForm.value).forEach(key => {
formData.append(key, this.productForm.value[key]);
});
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.loadProducts());
}
}
private reset(): void {
this.loadProducts();
this.clearSelection();
}
}

View File

@@ -0,0 +1,10 @@
import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
export const PRODUCTS_ROUTES: Routes = [
{
path: '',
component: ProductListComponent,
title: '',
},
];