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 (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', path: 'access-denied',
component: AccessDeniedComponent, component: AccessDeniedComponent,

View File

@@ -1,6 +1,6 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { DashboardPageComponent } from './dashboard-page/dashboard-page.component'; 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 = [ export const DASHBOARD_ROUTES: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '', redirectTo: 'home', pathMatch: 'full' },
@@ -9,4 +9,9 @@ export const DASHBOARD_ROUTES: Routes = [
component: DashboardPageComponent, component: DashboardPageComponent,
title: 'Dashboard Übersicht', 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], imports: [CommonModule],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
}) })
export class AdminDashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
private analyticsService = inject(AnalyticsService); private analyticsService = inject(AnalyticsService);
analytics$!: Observable<Analytics>; analytics$!: Observable<Analytics>;

View File

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

View File

@@ -2,15 +2,55 @@
<h1>Zahlungsmethoden verwalten</h1> <h1>Zahlungsmethoden verwalten</h1>
<form [formGroup]="methodForm" (ngSubmit)="onSubmit()"> <form [formGroup]="methodForm" (ngSubmit)="onSubmit()">
<h3>{{ selectedMethodId ? 'Methode bearbeiten' : 'Neue Methode' }}</h3> <h3>{{ selectedMethodId ? 'Methode bearbeiten' : 'Neue Methode' }}</h3>
<input type="text" formControlName="name" placeholder="Name">
<textarea formControlName="description" placeholder="Beschreibung"></textarea> <div><label for="name">Name:</label>
<select formControlName="paymentGatewayType"> <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> <option *ngFor="let type of gatewayTypes" [value]="type">{{ type }}</option>
</select> </select></div>
<label><input type="checkbox" formControlName="isActive"> Aktiv</label>
<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="submit" [disabled]="methodForm.invalid">{{ selectedMethodId ? 'Aktualisieren' : 'Erstellen' }}</button>
<button type="button" *ngIf="selectedMethodId" (click)="clearSelection()">Abbrechen</button> <button type="button" *ngIf="selectedMethodId" (click)="clearSelection()">Abbrechen</button>
</form> </form>
<hr> <hr>
<h2>Bestehende Methoden</h2> <h2>Bestehende Methoden</h2>
<ul> <ul>

View File

@@ -19,33 +19,64 @@ export class PaymentMethodListComponent implements OnInit {
methods$!: Observable<AdminPaymentMethod[]>; methods$!: Observable<AdminPaymentMethod[]>;
methodForm: FormGroup; methodForm: FormGroup;
selectedMethodId: string | null = null; selectedMethodId: string | null = null;
gatewayTypes: PaymentGatewayType[] = ['BankTransfer', 'Invoice', 'PayPal', 'Stripe', 'CashOnDelivery']; gatewayTypes: PaymentGatewayType[] = ['BankTransfer', 'PayPal', 'Stripe', 'Invoice', 'CashOnDelivery'];
constructor() { constructor() {
this.methodForm = this.fb.group({ this.methodForm = this.fb.group({
name: ['', Validators.required], name: ['', Validators.required],
description: [''], description: [''],
isActive: [true], 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(); } ngOnInit(): void {
loadMethods(): void { this.methods$ = this.paymentMethodService.getAll(); } 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 { selectMethod(method: AdminPaymentMethod): void {
this.selectedMethodId = method.id; 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); this.methodForm.patchValue(method);
} }
clearSelection(): void { clearSelection(): void {
this.selectedMethodId = null; 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 { onSubmit(): void {
if (this.methodForm.invalid) return; 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) { if (this.selectedMethodId) {
this.paymentMethodService.update(this.selectedMethodId, dataToSend).subscribe(() => this.reset()); this.paymentMethodService.update(this.selectedMethodId, dataToSend).subscribe(() => this.reset());
@@ -64,4 +95,33 @@ export class PaymentMethodListComponent implements OnInit {
this.loadMethods(); this.loadMethods();
this.clearSelection(); 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> <div>
<h1>Produkte verwalten</h1> <h1>Produkte verwalten</h1>
<!-- Formular -->
<form [formGroup]="productForm" (ngSubmit)="onSubmit()"> <form [formGroup]="productForm" (ngSubmit)="onSubmit()">
<h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt' }}</h3> <h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt' }}</h3>
<input type="text" formControlName="name" placeholder="Produktname">
<input type="text" formControlName="slug" placeholder="Slug"> <!-- Basis-Informationen -->
<input type="text" formControlName="sku" placeholder="SKU"> <div><label>Name:</label><input type="text" formControlName="name"></div>
<input type="number" formControlName="price" placeholder="Preis"> <div><label>Slug (automatisch generiert):</label><input type="text" formControlName="slug"></div>
<input type="number" formControlName="stockQuantity" placeholder="Lagerbestand">
<label><input type="checkbox" formControlName="isActive"> Aktiv</label> <!-- +++ SKU-FELD MIT GENERIEREN-BUTTON +++ -->
<label><input type="checkbox" formControlName="isFeatured"> Hervorgehoben</label> <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="submit" [disabled]="productForm.invalid">{{ selectedProductId ? 'Aktualisieren' : 'Erstellen' }}</button>
<button type="button" *ngIf="selectedProductId" (click)="clearSelection()">Abbrechen</button> <button type="button" *ngIf="selectedProductId" (click)="clearSelection()">Abbrechen</button>
</form> </form>
<hr> <hr>
<!-- Liste -->
<h2>Bestehende Produkte</h2> <h2>Bestehende Produkte</h2>
<ul> <ul>
<li *ngFor="let product of products$ | async"> <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)="selectProduct(product)">Bearbeiten</button>
<button (click)="onDelete(product.id)">Löschen</button> <button (click)="onDelete(product.id)">Löschen</button>
</li> </li>

View File

@@ -1,23 +1,37 @@
import { Component, OnInit, inject } from '@angular/core'; import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { AdminProduct } from '../../../../core/models/product.model'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// ... (alle anderen Imports bleiben gleich)
import { ProductService } from '../../../services/product.service'; 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({ @Component({
selector: 'app-product-list', selector: 'app-product-list',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule], imports: [CommonModule, ReactiveFormsModule, CurrencyPipe],
templateUrl: './product-list.component.html', 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 productService = inject(ProductService);
private categoryService = inject(CategoryService);
private supplierService = inject(SupplierService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
products$!: Observable<AdminProduct[]>; products$!: Observable<AdminProduct[]>;
allCategories$!: Observable<Category[]>;
allSuppliers$!: Observable<Supplier[]>;
productForm: FormGroup; productForm: FormGroup;
selectedProductId: string | null = null; selectedProductId: string | null = null;
private nameChangeSubscription?: Subscription;
constructor() { constructor() {
this.productForm = this.fb.group({ this.productForm = this.fb.group({
@@ -26,49 +40,45 @@ export class ProductListComponent implements OnInit {
sku: ['', Validators.required], sku: ['', Validators.required],
description: [''], description: [''],
price: [0, [Validators.required, Validators.min(0)]], price: [0, [Validators.required, Validators.min(0)]],
oldPrice: [null, [Validators.min(0)]],
purchasePrice: [null, [Validators.min(0)]],
stockQuantity: [0, [Validators.required, Validators.min(0)]], stockQuantity: [0, [Validators.required, Validators.min(0)]],
weight: [null, [Validators.min(0)]],
isActive: [true], isActive: [true],
isFeatured: [false] isFeatured: [false],
featuredDisplayOrder: [0],
supplierId: [null],
categorieIds: this.fb.array([])
}); });
} }
ngOnInit(): void { this.loadProducts(); } // --- NEUE METHODE ZUM GENERIEREN DER SKU ---
loadProducts(): void { this.products$ = this.productService.getAll(); } generateSku(): void {
const name = this.productForm.get('name')?.value || 'PROD';
selectProduct(product: AdminProduct): void { // Nimmt die ersten 4 Buchstaben des Namens (oder weniger, falls kürzer)
this.selectedProductId = product.id; const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
this.productForm.patchValue(product); // 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; get categorieIds(): FormArray {
this.productForm.reset({ isActive: true, isFeatured: false }); 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: '',
},
];