endpoint anpassen

This commit is contained in:
Tizian.Breuch
2025-09-30 13:22:58 +02:00
parent eff6d8d8aa
commit b88af92095
20 changed files with 354 additions and 74 deletions

View File

@@ -31,7 +31,36 @@ export const routes: Routes = [
{
path: 'auth',
loadChildren: () =>
import('./features/components/auth/auth.routes').then((r) => r.AUTH_ROUTES),
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: 'access-denied',

View File

@@ -0,0 +1,10 @@
import { Routes } from '@angular/router';
import { CategoryListComponent } from './category-list/category-list.component';
export const CATEGORIES_ROUTES: Routes = [
{
path: '',
component: CategoryListComponent,
title: 'Category Übersicht',
},
];

View File

@@ -1,26 +1,66 @@
<div>
<h1>Kategorien verwalten</h1>
<!-- Formular für Erstellen/Bearbeiten -->
<form [formGroup]="categoryForm" (ngSubmit)="onSubmit()">
<h3>{{ selectedCategoryId ? 'Kategorie bearbeiten' : 'Neue Kategorie erstellen' }}</h3>
<input type="text" formControlName="name" placeholder="Name">
<input type="text" formControlName="slug" placeholder="Slug">
<textarea formControlName="description" placeholder="Beschreibung"></textarea>
<label>
<input type="checkbox" formControlName="isActive"> Aktiv
</label>
<input type="file" (change)="onFileChange($event)">
<div>
<label for="name">Name</label>
<input id="name" type="text" formControlName="name" placeholder="Name">
</div>
<div>
<label for="slug">Slug</label>
<input id="slug" type="text" formControlName="slug" placeholder="Slug">
</div>
<div>
<label for="parent">Übergeordnete Kategorie</label>
<select id="parent" formControlName="parentcategorieId">
<option [ngValue]="null">-- Keine (Hauptkategorie) --</option>
<option *ngFor="let parent of parentCategories$ | async" [value]="parent.id">
{{ parent.name }}
</option>
</select>
</div>
<div>
<label for="description">Beschreibung</label>
<textarea id="description" formControlName="description" placeholder="Beschreibung"></textarea>
</div>
<div>
<label>
<input type="checkbox" formControlName="isActive"> Aktiv
</label>
</div>
<!-- +++ NEUER BILD-MANAGEMENT-BLOCK +++ -->
<div>
<label for="file">Bild</label>
<!-- Bild-Vorschau -->
<div *ngIf="existingImageUrl">
<img [src]="existingImageUrl" alt="Kategorie-Vorschau" style="max-width: 100px; max-height: 100px; display: block; margin-bottom: 10px;">
<button type="button" (click)="removeImage()">Bild entfernen</button>
</div>
<!-- Datei-Input -->
<input id="file" type="file" (change)="onFileChange($event)">
</div>
<!-- +++ ENDE NEU +++ -->
<br>
<button type="submit" [disabled]="categoryForm.invalid">{{ selectedCategoryId ? 'Aktualisieren' : 'Erstellen' }}</button>
<button type="button" *ngIf="selectedCategoryId" (click)="clearSelection()">Abbrechen</button>
</form>
<hr>
<!-- Liste der Kategorien -->
<h2>Bestehende Kategorien</h2>
<ul>
<li *ngFor="let category of categories$ | async">
<!-- Bild-Vorschau in der Liste -->
<img *ngIf="category.imageUrl" [src]="category.imageUrl" alt="{{ category.name }}" style="width: 30px; height: 30px; vertical-align: middle; margin-right: 10px;">
{{ category.name }} (Slug: {{ category.slug }}) - Aktiv: {{ category.isActive }}
<button (click)="selectCategory(category)">Bearbeiten</button>
<button (click)="onDelete(category.id)">Löschen</button>

View File

@@ -1,7 +1,7 @@
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 { Observable, map } from 'rxjs';
import { Category } from '../../../../core/models/category.model';
import { CategoryService } from '../../../services/category.service';
@@ -16,9 +16,14 @@ export class CategoryListComponent implements OnInit {
private fb = inject(FormBuilder);
categories$!: Observable<Category[]>;
parentCategories$!: Observable<Category[]>;
categoryForm: FormGroup;
selectedCategoryId: string | null = null;
// --- NEUE EIGENSCHAFTEN FÜR BILD-MANAGEMENT ---
selectedFile: File | null = null;
existingImageUrl: string | null = null;
// --- ENDE NEU ---
constructor() {
this.categoryForm = this.fb.group({
@@ -26,6 +31,7 @@ export class CategoryListComponent implements OnInit {
slug: ['', Validators.required],
description: [''],
isActive: [true],
parentcategorieId: [null]
});
}
@@ -35,45 +41,74 @@ export class CategoryListComponent implements OnInit {
loadCategories(): void {
this.categories$ = this.categoryService.getAll();
this.updateParentCategoryList();
}
onFileChange(event: Event): void {
const element = event.currentTarget as HTMLInputElement;
let fileList: FileList | null = element.files;
if (fileList) {
const fileList: FileList | null = element.files;
if (fileList && fileList.length > 0) {
this.selectedFile = fileList[0];
// Zeige eine Vorschau des neu ausgewählten Bildes an
this.existingImageUrl = URL.createObjectURL(this.selectedFile);
}
}
selectCategory(category: Category): void {
this.selectedCategoryId = category.id;
this.categoryForm.patchValue(category);
// --- NEU: Bild-URL speichern ---
this.existingImageUrl = category.imageUrl || null;
this.updateParentCategoryList();
}
clearSelection(): void {
this.selectedCategoryId = null;
this.categoryForm.reset({ isActive: true });
this.categoryForm.reset({ isActive: true, parentcategorieId: null });
// --- NEU: Bild-Referenzen zurücksetzen ---
this.selectedFile = null;
this.existingImageUrl = null;
this.updateParentCategoryList();
}
// --- NEU: Methode zum Entfernen des Bildes ---
removeImage(): void {
this.selectedFile = null;
this.existingImageUrl = null;
}
onSubmit(): void {
if (this.categoryForm.invalid) return;
const formData = new FormData();
Object.keys(this.categoryForm.value).forEach(key => {
formData.append(key, this.categoryForm.value[key]);
const formValue = this.categoryForm.value;
Object.keys(formValue).forEach(key => {
const value = formValue[key] === null || formValue[key] === undefined ? '' : formValue[key];
formData.append(key, value);
});
// --- ÜBERARBEITETE BILD-LOGIK ---
if (this.selectedFile) {
// Nur wenn eine NEUE Datei ausgewählt wurde, wird sie gesendet.
formData.append('ImageFile', this.selectedFile, this.selectedFile.name);
} else if (this.existingImageUrl) {
// Wenn keine neue Datei da ist, aber ein altes Bild existiert,
// senden wir die URL, damit das Backend weiß, dass es erhalten bleiben soll.
formData.append('ImageUrl', this.existingImageUrl);
}
// Wenn beides null ist (z.B. nach Klick auf "Bild entfernen"),
// wird kein Bild-Parameter gesendet und das Backend sollte das Bild löschen.
// --- ENDE ÜBERARBEITUNG ---
if (this.selectedCategoryId) {
// Update
formData.append('Id', this.selectedCategoryId);
this.categoryService.update(this.selectedCategoryId, formData).subscribe(() => this.reset());
} else {
// Create
this.categoryService.create(formData).subscribe(() => this.reset());
}
}
@@ -88,4 +123,10 @@ export class CategoryListComponent implements OnInit {
this.loadCategories();
this.clearSelection();
}
private updateParentCategoryList(): void {
this.parentCategories$ = this.categories$.pipe(
map(categories => categories.filter(c => c.id !== this.selectedCategoryId))
);
}
}

View File

@@ -3,28 +3,83 @@
<form [formGroup]="discountForm" (ngSubmit)="onSubmit()">
<h3>{{ selectedDiscountId ? "Rabatt bearbeiten" : "Neuer Rabatt" }}</h3>
<input type="text" formControlName="name" placeholder="Name des Rabatts" />
<select formControlName="discountType">
<option *ngFor="let type of discountTypes" [value]="type">
{{ type }}
</option>
</select>
<input
type="number"
formControlName="discountValue"
placeholder="Wert (z.B. 10 für 10% oder 10€)"
/>
<input
type="text"
formControlName="couponCode"
placeholder="Gutscheincode (optional)"
/>
<label
><input type="checkbox" formControlName="requiresCouponCode" /> Code
erforderlich</label
>
<label><input type="checkbox" formControlName="isActive" /> Aktiv</label>
<input type="date" formControlName="startDate" />
<div><label for="name">Name:</label>
<input id="name" type="text" formControlName="name" placeholder="Name des Rabatts" /></div>
<div><label for="desc">Beschreibung:</label>
<textarea id="desc" formControlName="description" placeholder="Beschreibung"></textarea></div>
<div><label for="type">Typ:</label>
<select id="type" formControlName="discountType">
<option *ngFor="let type of discountTypes" [value]="type">{{ type }}</option>
</select></div>
<div><label for="value">Wert:</label>
<input id="value" type="number" formControlName="discountValue" placeholder="z.B. 10 für 10% oder 10€" /></div>
<hr>
<div><label for="couponCode">Gutscheincode:</label>
<input id="couponCode" type="text" formControlName="couponCode" placeholder="z.B. SOMMER2024" /></div>
<div><label><input type="checkbox" formControlName="requiresCouponCode" /> Code erforderlich</label></div>
<div><label><input type="checkbox" formControlName="isActive" /> Aktiv</label></div>
<hr>
<div><label for="startDate">Startdatum:</label>
<input id="startDate" type="date" formControlName="startDate" /></div>
<div><label for="endDate">Enddatum (optional):</label>
<input id="endDate" type="date" formControlName="endDate" /></div>
<div><label for="minOrder">Mindestbestellwert (optional):</label>
<input id="minOrder" type="number" formControlName="minimumOrderAmount" placeholder="z.B. 50" /></div>
<div><label for="maxUsage">Maximale Nutzungen (optional):</label>
<input id="maxUsage" type="number" formControlName="maximumUsageCount" placeholder="z.B. 100" /></div>
<hr>
<h4>Produkten zuweisen</h4>
<div>
<select #productSelect>
<option [ngValue]="null">-- Produkt auswählen --</option>
<option *ngFor="let product of allProducts$ | async" [value]="product.id">
{{ product.name }} ({{ product.sku }})
</option>
</select>
<button type="button" (click)="addProduct(productSelect.value)">+</button>
</div>
<ul>
<li *ngFor="let control of assignedProductIds.controls; let i = index">
{{ getProductName(control.value, (allProducts$ | async)) }}
<button type="button" (click)="removeProduct(i)">x</button>
</li>
</ul>
<hr>
<h4>Kategorien zuweisen</h4>
<div>
<select #categorySelect>
<option [ngValue]="null">-- Kategorie auswählen --</option>
<option *ngFor="let category of allCategories$ | async" [value]="category.id">
{{ category.name }}
</option>
</select>
<button type="button" (click)="addCategory(categorySelect.value)">+</button>
</div>
<ul>
<li *ngFor="let control of assignedCategoryIds.controls; let i = index">
{{ getCategoryName(control.value, (allCategories$ | async)) }}
<button type="button" (click)="removeCategory(i)">x</button>
</li>
</ul>
<br><br>
<button type="submit" [disabled]="discountForm.invalid">
{{ selectedDiscountId ? "Aktualisieren" : "Erstellen" }}
</button>
@@ -38,8 +93,8 @@
<h2>Bestehende Rabatte</h2>
<ul>
<li *ngFor="let discount of discounts$ | async">
{{ discount.name }} ({{ discount.discountValue
}}{{ discount.discountType === "Percentage" ? "%" : "€" }})
{{ discount.name }} ({{ discount.discountValue }}{{ discount.discountType === "Percentage" ? "%" : "€" }})
- Code: {{ discount.couponCode || 'Kein Code' }}
<button (click)="selectDiscount(discount)">Bearbeiten</button>
<button (click)="onDelete(discount.id)">Löschen</button>
</li>

View File

@@ -1,11 +1,20 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, FormArray, ReactiveFormsModule, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
// Models
import { Discount } from '../../../../core/models/discount.model';
import { DiscountService } from '../../../services/discount.service';
import { AdminProduct } from '../../../../core/models/product.model';
import { Category } from '../../../../core/models/category.model';
import { DiscountType } from '../../../../core/enums/shared.enum';
// Services
import { DiscountService } from '../../../services/discount.service';
import { ProductService } from '../../../services/product.service';
import { CategoryService } from '../../../services/category.service';
@Component({
selector: 'app-discount-list',
standalone: true,
@@ -13,10 +22,18 @@ import { DiscountType } from '../../../../core/enums/shared.enum';
templateUrl: './discount-list.component.html',
})
export class DiscountListComponent implements OnInit {
// --- Service Injection ---
private discountService = inject(DiscountService);
private productService = inject(ProductService);
private categoryService = inject(CategoryService);
private fb = inject(FormBuilder);
// --- Data Observables ---
discounts$!: Observable<Discount[]>;
allProducts$!: Observable<AdminProduct[]>;
allCategories$!: Observable<Category[]>;
// --- Form Properties ---
discountForm: FormGroup;
selectedDiscountId: string | null = null;
discountTypes: DiscountType[] = ['Percentage', 'FixedAmount'];
@@ -29,31 +46,87 @@ export class DiscountListComponent implements OnInit {
couponCode: [''],
requiresCouponCode: [false],
isActive: [true],
startDate: [new Date().toISOString().split('T')[0], Validators.required]
startDate: [new Date().toISOString().split('T')[0], Validators.required],
// +++ HIER SIND DIE FEHLENDEN FELDER +++
endDate: [null],
minimumOrderAmount: [null, [Validators.min(0)]],
maximumUsageCount: [null, [Validators.min(0)]],
description: [''],
assignedProductIds: this.fb.array([]),
assignedCategoryIds: this.fb.array([])
});
}
ngOnInit(): void { this.loadDiscounts(); }
loadDiscounts(): void { this.discounts$ = this.discountService.getAll(); }
get assignedProductIds(): FormArray {
return this.discountForm.get('assignedProductIds') as FormArray;
}
get assignedCategoryIds(): FormArray {
return this.discountForm.get('assignedCategoryIds') as FormArray;
}
ngOnInit(): void {
this.loadInitialData();
}
loadInitialData(): void {
this.discounts$ = this.discountService.getAll();
this.allProducts$ = this.productService.getAll();
this.allCategories$ = this.categoryService.getAll();
}
selectDiscount(discount: Discount): void {
this.selectedDiscountId = discount.id;
// Format date for the input[type=date]
const formattedDiscount = {
this.discountForm.patchValue({
...discount,
startDate: new Date(discount.startDate).toISOString().split('T')[0]
};
this.discountForm.patchValue(formattedDiscount);
startDate: new Date(discount.startDate).toISOString().split('T')[0],
endDate: discount.endDate ? new Date(discount.endDate).toISOString().split('T')[0] : null,
});
this.assignedProductIds.clear();
discount.assignedProductIds?.forEach(id => this.assignedProductIds.push(this.fb.control(id)));
this.assignedCategoryIds.clear();
discount.assignedCategoryIds?.forEach(id => this.assignedCategoryIds.push(this.fb.control(id)));
}
clearSelection(): void {
this.selectedDiscountId = null;
this.discountForm.reset({
name: '',
discountType: 'Percentage',
isActive: true,
discountValue: 0,
couponCode: '',
requiresCouponCode: false,
startDate: new Date().toISOString().split('T')[0]
isActive: true,
startDate: new Date().toISOString().split('T')[0],
endDate: null,
minimumOrderAmount: null,
maximumUsageCount: null,
description: '',
});
this.assignedProductIds.clear();
this.assignedCategoryIds.clear();
}
addProduct(productId: string | null): void {
if (productId && !this.assignedProductIds.value.includes(productId)) {
this.assignedProductIds.push(this.fb.control(productId));
}
}
removeProduct(index: number): void {
this.assignedProductIds.removeAt(index);
}
addCategory(categoryId: string | null): void {
if (categoryId && !this.assignedCategoryIds.value.includes(categoryId)) {
this.assignedCategoryIds.push(this.fb.control(categoryId));
}
}
removeCategory(index: number): void {
this.assignedCategoryIds.removeAt(index);
}
onSubmit(): void {
@@ -62,8 +135,12 @@ export class DiscountListComponent implements OnInit {
const formValue = this.discountForm.value;
const dataToSend: Discount = {
...formValue,
id: this.selectedDiscountId || '00000000-0000-0000-0000-000000000000', // Dummy ID for create
startDate: new Date(formValue.startDate).toISOString()
id: this.selectedDiscountId || undefined,
startDate: new Date(formValue.startDate).toISOString(),
endDate: formValue.endDate ? new Date(formValue.endDate).toISOString() : null,
// Die Werte aus den FormArrays sind bereits korrekte Arrays von Strings
assignedProductIds: this.assignedProductIds.value,
assignedCategoryIds: this.assignedCategoryIds.value
};
if (this.selectedDiscountId) {
@@ -75,12 +152,20 @@ export class DiscountListComponent implements OnInit {
onDelete(id: string): void {
if (confirm('Rabatt wirklich löschen?')) {
this.discountService.delete(id).subscribe(() => this.loadDiscounts());
this.discountService.delete(id).subscribe(() => this.loadInitialData());
}
}
private reset(): void {
this.loadDiscounts();
this.loadInitialData();
this.clearSelection();
}
getProductName(productId: string, products: AdminProduct[] | null): string {
return products?.find(p => p.id === productId)?.name || 'Unbekanntes Produkt';
}
getCategoryName(categoryId: string, categories: Category[] | null): string {
return categories?.find(c => c.id === categoryId)?.name || 'Unbekannte Kategorie';
}
}

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import { AnalyticsPeriod } from '../../core/enums/shared.enum';
export class AnalyticsService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminAnalytics';
private readonly endpoint = '/admin/AdminAnalytics';
get(period: AnalyticsPeriod): Observable<Analytics> {
const params = new HttpParams().set('period', period);

View File

@@ -8,7 +8,7 @@ import { Category } from '../../core/models/category.model';
export class CategoryService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminCategories';
private readonly endpoint = '/admin/AdminCategories';
getAll(): Observable<Category[]> {
return this.http.get<Category[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { Discount } from '../../core/models/discount.model';
export class DiscountService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminDiscounts';
private readonly endpoint = '/admin/AdminDiscounts';
getAll(): Observable<Discount[]> {
return this.http.get<Discount[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { OrderDetail, OrderSummary, UpdateOrderStatusRequest } from '../../core/
export class OrderService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminOrders';
private readonly endpoint = '/admin/AdminOrders';
getAll(): Observable<OrderSummary[]> {
return this.http.get<OrderSummary[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { AdminPaymentMethod } from '../../core/models/payment.model';
export class PaymentMethodService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminPaymentMethods';
private readonly endpoint = '/admin/AdminPaymentMethods';
getAll(): Observable<AdminPaymentMethod[]> {
return this.http.get<AdminPaymentMethod[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { AdminProduct } from '../../core/models/product.model';
export class ProductService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminProducts';
private readonly endpoint = '/admin/AdminProducts';
getAll(): Observable<AdminProduct[]> {
return this.http.get<AdminProduct[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { Review } from '../../core/models/review.model';
export class ReviewService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminReviews';
private readonly endpoint = '/admin/AdminReviews';
getAll(): Observable<Review[]> {
return this.http.get<Review[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { Setting } from '../../core/models/setting.model';
export class SettingService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminSettings';
private readonly endpoint = '/admin/AdminSettings';
getAllGrouped(): Observable<{ [group: string]: Setting[] }> {
return this.http.get<{ [group: string]: Setting[] }>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { ShippingMethod } from '../../core/models/shipping.model';
export class ShippingMethodService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminShippingMethods';
private readonly endpoint = '/admin/AdminShippingMethods';
getAll(): Observable<ShippingMethod[]> {
return this.http.get<ShippingMethod[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { AdminShopInfo } from '../../core/models/shop.model';
export class ShopInfoService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminShopInfo';
private readonly endpoint = '/admin/AdminShopInfo';
get(): Observable<AdminShopInfo> {
return this.http.get<AdminShopInfo>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { Supplier } from '../../core/models/supplier.model';
export class SupplierService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminSuppliers';
private readonly endpoint = '/admin/AdminSuppliers';
getAll(): Observable<Supplier[]> {
return this.http.get<Supplier[]>(`${this.apiUrl}${this.endpoint}`);

View File

@@ -8,7 +8,7 @@ import { User, UpdateUserRolesRequest } from '../../core/models/user.model';
export class UserService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
private readonly endpoint = '/AdminUsers';
private readonly endpoint = '/admin/AdminUsers';
getAll(): Observable<User[]> {
return this.http.get<User[]>(`${this.apiUrl}${this.endpoint}`);