This commit is contained in:
Tizian.Breuch
2025-10-10 15:46:03 +02:00
parent e91fe838fa
commit ef994b89e3
11 changed files with 408 additions and 124 deletions

View File

@@ -109,6 +109,13 @@ export const routes: Routes = [
(r) => r.USERS_ROUTES (r) => r.USERS_ROUTES
), ),
}, },
{
path: 'analytics',
loadChildren: () =>
import('./features/components/analytics/analytics.routes').then(
(r) => r.ANALYTICS_ROUTES
),
},
], ],
}, },
{ {

View File

@@ -0,0 +1,11 @@
import { Routes } from '@angular/router';
import { AnalyticsComponent } from './analytics/analytics.component';
export const ANALYTICS_ROUTES: Routes = [
{
path: '',
component: AnalyticsComponent,
title: '',
},
];

View File

@@ -0,0 +1,70 @@
<!-- /src/app/features/admin/components/analytics/analytics.component.html -->
<div>
<h1>Analytics Dashboard</h1>
<div class="period-selector">
<button (click)="loadAnalytics('Last7Days')" [class.active]="selectedPeriod === 'Last7Days'">Letzte 7 Tage</button>
<button (click)="loadAnalytics('Last30Days')" [class.active]="selectedPeriod === 'Last30Days'">Letzte 30 Tage</button>
<button (click)="loadAnalytics('AllTime')" [class.active]="selectedPeriod === 'AllTime'">Gesamt</button>
</div>
<hr>
<div *ngIf="analytics$ | async as data; else loading">
<fieldset>
<legend>KPI Übersicht</legend>
<div class="kpi-grid">
<div><strong>Umsatz:</strong> {{ data.kpiSummary.totalRevenue | currency:'EUR' }}</div>
<div><strong>Bestellungen:</strong> {{ data.kpiSummary.totalOrders }}</div>
<div><strong>Ø Bestellwert:</strong> {{ data.kpiSummary.averageOrderValue | currency:'EUR' }}</div>
<div><strong>Gesamtkunden:</strong> {{ data.kpiSummary.totalCustomers }}</div>
<div><strong>Neukunden (im Zeitraum):</strong> {{ data.kpiSummary.newCustomersThisPeriod }}</div>
</div>
</fieldset>
<fieldset>
<legend>Top Produkte</legend>
<table>
<thead>
<tr>
<th>Produkt</th>
<th>SKU</th>
<th>Verkaufte Einheiten</th>
<th>Umsatz</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let product of data.topPerformingProducts">
<td>{{ product.name }}</td>
<td>{{ product.sku }}</td>
<td>{{ product.unitsSold }}</td>
<td>{{ product.totalRevenue | currency:'EUR' }}</td>
</tr>
<tr *ngIf="!data.topPerformingProducts || data.topPerformingProducts.length === 0">
<td colspan="4">Keine Produktdaten in diesem Zeitraum.</td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset>
<legend>Lagerstatus</legend>
<div class="kpi-grid">
<div><strong>Produkte mit niedrigem Lagerbestand:</strong> {{ data.inventoryStatus.productsWithLowStock }}</div>
<div><strong>Lagerverfügbarkeit (gesamt):</strong> {{ data.inventoryStatus.overallStockAvailabilityPercentage | number:'1.1-2' }}%</div>
</div>
</fieldset>
<fieldset>
<legend>Verkaufsverlauf (Rohdaten)</legend>
<pre>{{ data.salesOverTime | json }}</pre>
</fieldset>
</div>
<ng-template #loading>
<p>Lade Analysedaten...</p>
</ng-template>
</div>

View File

@@ -0,0 +1,35 @@
// /src/app/features/admin/components/analytics/analytics.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule, CurrencyPipe, DecimalPipe } from '@angular/common';
import { Observable } from 'rxjs';
// Models & Enums
import { Analytics } from '../../../../core/models/analytics.model';
import { AnalyticsPeriod } from '../../../../core/enums/shared.enum';
// Services
import { AnalyticsService } from '../../../services/analytics.service';
@Component({
selector: 'app-analytics', // Dein gewünschter Selector
standalone: true,
imports: [CommonModule, CurrencyPipe, DecimalPipe],
templateUrl: './analytics.component.html',
styleUrl: './analytics.component.css'
})
export class AnalyticsComponent implements OnInit { // Dein gewünschter Klassenname
private analyticsService = inject(AnalyticsService);
analytics$!: Observable<Analytics>;
selectedPeriod: AnalyticsPeriod = 'Last30Days';
ngOnInit(): void {
this.loadAnalytics(this.selectedPeriod);
}
loadAnalytics(period: AnalyticsPeriod): void {
this.selectedPeriod = period;
this.analytics$ = this.analyticsService.get(period);
}
}

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: '1', path: '',
component: OrderListComponent, component: OrderListComponent,
title: '', title: '',
}, },

View File

@@ -1,132 +1,227 @@
<div> <div>
<h1>Produkte verwalten</h1> <h1>Produkte verwalten</h1>
<!-- Formular --> <!-- Das Formular bleibt unverändert und ist bereits korrekt -->
<form [formGroup]="productForm" (ngSubmit)="onSubmit()"> <form [formGroup]="productForm" (ngSubmit)="onSubmit()">
<h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt erstellen' }}</h3> <!-- ... (Dein komplettes Formular hier) ... -->
<h3>
<!-- Basis-Informationen --> {{ selectedProductId ? "Produkt bearbeiten" : "Neues Produkt erstellen" }}
</h3>
<h4>Basis-Informationen</h4> <h4>Basis-Informationen</h4>
<div><label>Name:</label><input type="text" formControlName="name" /></div>
<div> <div>
<label for="name">Name:</label> <label>Slug (automatisch generiert):</label
<input id="name" type="text" formControlName="name"> ><input type="text" formControlName="slug" />
</div> </div>
<div> <div>
<label for="slug">Slug (automatisch generiert):</label> <label>SKU (Artikelnummer):</label>
<input id="slug" type="text" formControlName="slug"> <div style="display: flex; align-items: center; gap: 10px">
</div> <input
<div> id="sku"
<label for="sku">SKU (Artikelnummer):</label> type="text"
<div style="display: flex; align-items: center; gap: 10px;"> formControlName="sku"
<input id="sku" type="text" formControlName="sku" style="flex-grow: 1;"> style="flex-grow: 1"
<button type="button" (click)="generateSku()">Generieren</button> /><button type="button" (click)="generateSku()">Generieren</button>
</div> </div>
</div> </div>
<div> <div>
<label for="description">Beschreibung:</label> <label>Beschreibung:</label
<textarea id="description" formControlName="description" rows="5"></textarea> ><textarea formControlName="description" rows="5"></textarea>
</div> </div>
<hr />
<hr>
<!-- Preis & Lager -->
<h4>Preis & Lager</h4> <h4>Preis & Lager</h4>
<div> <div>
<label for="price">Preis (€):</label> <label>Preis (€):</label><input type="number" formControlName="price" />
<input id="price" type="number" formControlName="price">
</div> </div>
<div> <div>
<label for="oldPrice">Alter Preis (€) (optional):</label> <label>Alter Preis (€) (optional):</label
<input id="oldPrice" type="number" formControlName="oldPrice"> ><input type="number" formControlName="oldPrice" />
</div> </div>
<div> <div>
<label for="purchasePrice">Einkaufspreis (€) (optional):</label> <label>Einkaufspreis (€) (optional):</label
<input id="purchasePrice" type="number" formControlName="purchasePrice"> ><input type="number" formControlName="purchasePrice" />
</div> </div>
<div> <div>
<label for="stock">Lagerbestand:</label> <label>Lagerbestand:</label
<input id="stock" type="number" formControlName="stockQuantity"> ><input type="number" formControlName="stockQuantity" />
</div> </div>
<div> <div>
<label for="weight">Gewicht (in kg) (optional):</label> <label>Gewicht (in kg) (optional):</label
<input id="weight" type="number" formControlName="weight"> ><input type="number" formControlName="weight" />
</div> </div>
<hr />
<hr>
<!-- Zuweisungen -->
<h4>Zuweisungen</h4> <h4>Zuweisungen</h4>
<div> <div>
<label for="supplier">Lieferant:</label> <label>Lieferant:</label
<select id="supplier" formControlName="supplierId"> ><select formControlName="supplierId">
<option [ngValue]="null">-- Kein Lieferant --</option> <option [ngValue]="null">-- Kein Lieferant --</option>
<option *ngFor="let supplier of allSuppliers$ | async" [value]="supplier.id">{{ supplier.name }}</option> <option
</select> *ngFor="let supplier of allSuppliers$ | async"
[value]="supplier.id"
>
{{ supplier.name }}
</option>
</select>
</div> </div>
<div> <div>
<label>Kategorien:</label> <label>Kategorien:</label>
<div class="category-checkbox-group" style="height: 100px; overflow-y: auto; border: 1px solid #ccc; padding: 5px; margin-top: 5px;"> <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"> <div *ngFor="let category of allCategories$ | async">
<label> <label
<input ><input
type="checkbox" type="checkbox"
[value]="category.id" [value]="category.id"
[checked]="isCategorySelected(category.id)" [checked]="isCategorySelected(category.id)"
(change)="onCategoryChange($event)"> (change)="onCategoryChange($event)"
{{ category.name }} />{{ category.name }}</label
</label> >
</div> </div>
</div> </div>
</div> </div>
<hr />
<hr>
<!-- Bilder -->
<h4>Produktbilder</h4> <h4>Produktbilder</h4>
<div *ngIf="selectedProductId && existingImages.length > 0"> <div *ngIf="selectedProductId && existingImages.length > 0">
<p>Bestehende Bilder:</p> <p>Bestehende Bilder:</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px;"> <div
<div *ngFor="let img of existingImages" style="position: relative;"> style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px"
<img [src]="img.url" [alt]="productForm.get('name')?.value" >
style="width: 100px; height: 100px; object-fit: cover; border: 2px solid" <div *ngFor="let img of existingImages" style="position: relative">
[style.borderColor]="img.isMainImage ? 'green' : 'gray'"> <img
<button (click)="deleteExistingImage(img.id, $event)" [src]="img.url"
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> [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>
</div> </div>
<div> <div>
<label for="main-image">Hauptbild (ersetzt bestehendes Hauptbild)</label> <label for="main-image">Hauptbild (ersetzt bestehendes Hauptbild)</label
<input id="main-image" type="file" (change)="onMainFileChange($event)" accept="image/*"> ><input
id="main-image"
type="file"
(change)="onMainFileChange($event)"
accept="image/*"
/>
</div> </div>
<div> <div>
<label for="additional-images">Zusätzliche Bilder hinzufügen</label> <label for="additional-images">Zusätzliche Bilder hinzufügen</label
<input id="additional-images" type="file" (change)="onAdditionalFilesChange($event)" accept="image/*" multiple> ><input
id="additional-images"
type="file"
(change)="onAdditionalFilesChange($event)"
accept="image/*"
multiple
/>
</div> </div>
<hr />
<hr>
<!-- Status & Sichtbarkeit -->
<h4>Status & Sichtbarkeit</h4> <h4>Status & Sichtbarkeit</h4>
<div><label><input type="checkbox" formControlName="isActive"> Aktiv (im Shop sichtbar)</label></div> <div>
<div><label><input type="checkbox" formControlName="isFeatured"> Hervorgehoben (z.B. auf Startseite)</label></div> <label
<div><label>Anzeigereihenfolge (Hervorgehoben):</label><input type="number" formControlName="featuredDisplayOrder"></div> ><input type="checkbox" formControlName="isActive" /> Aktiv (im Shop
sichtbar)</label
<br><br> >
</div>
<button type="submit" [disabled]="productForm.invalid">{{ selectedProductId ? 'Aktualisieren' : 'Erstellen' }}</button> <div>
<button type="button" *ngIf="selectedProductId" (click)="clearSelection()">Abbrechen</button> <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> </form>
<hr> <hr />
<h2>Bestehende Produkte</h2>
<h2>Bestehende Produkte</h2> <table style="width: 100%; border-collapse: collapse;">
<ul> <thead>
<li *ngFor="let product of products$ | async"> <tr >
{{ product.name }} (SKU: {{ product.sku }}) - Preis: {{ product.price | currency:'EUR' }} <th style="padding: 8px; border: 1px solid #ddd;">Bild</th>
<button (click)="selectProduct(product)">Bearbeiten</button> <th style="padding: 8px; border: 1px solid #ddd;">Name</th>
<button (click)="onDelete(product.id)">Löschen</button> <th style="padding: 8px; border: 1px solid #ddd;">SKU</th>
</li> <th style="padding: 8px; border: 1px solid #ddd;">Preis</th>
</ul> <th style="padding: 8px; border: 1px solid #ddd;">Lagerbestand</th>
</div> <th style="padding: 8px; border: 1px solid #ddd;">Aktiv</th>
<th style="padding: 8px; border: 1px solid #ddd;">Aktionen</th>
</tr>
</thead>
<tbody>
<ng-container *ngIf="products$ | async as products; else loading">
<tr *ngIf="products.length === 0">
<td colspan="7" style="text-align: center; padding: 16px;">Keine Produkte gefunden.</td>
</tr>
<tr *ngFor="let product of products">
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
<!-- +++ HIER IST DIE KORREKTUR +++ -->
<img
[src]="getMainImageUrl(product.images)"
[alt]="product.name"
style="width: 50px; height: 50px; object-fit: cover;">
</td>
<td style="padding: 8px; border: 1px solid #ddd;">
<strong>{{ product.name }}</strong><br>
<small style="color: #777;">Slug: {{ product.slug }}</small>
</td>
<td style="padding: 8px; border: 1px solid #ddd;">{{ product.sku }}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{{ product.price | currency:'EUR' }}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{{ product.stockQuantity }}</td>
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;" [style.color]="product.isActive ? 'green' : 'red'">
{{ product.isActive ? 'Ja' : 'Nein' }}
</td>
<td style="padding: 8px; border: 1px solid #ddd; width: 150px; text-align: center;">
<button (click)="selectProduct(product)">Bearbeiten</button>
<button (click)="onDelete(product.id)" style="margin-left: 5px; background-color: #dc3545; color: white;">Löschen</button>
</td>
</tr>
</ng-container>
<ng-template #loading>
<tr>
<td colspan="7" style="text-align: center; padding: 16px;">Lade Produkte...</td>
</tr>
</ng-template>
</tbody>
</table>

View File

@@ -265,4 +265,18 @@ export class ProductListComponent implements OnInit, OnDestroy {
private capitalizeFirstLetter(string: string): string { private capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
/**
* Sucht das Hauptbild aus der Bilderliste eines Produkts und gibt dessen URL zurück.
* Gibt eine Platzhalter-URL zurück, wenn kein Hauptbild gefunden wird.
* @param images Die Liste der Produktbilder.
* @returns Die URL des Hauptbildes oder eine Platzhalter-URL.
*/
getMainImageUrl(images?: ProductImage[]): string {
if (!images || images.length === 0) {
return 'https://via.placeholder.com/50'; // Platzhalter, wenn gar keine Bilder vorhanden sind
}
const mainImage = images.find(img => img.isMainImage);
return mainImage?.url || images[0].url || 'https://via.placeholder.com/50'; // Fallback auf das erste Bild, wenn kein Hauptbild markiert ist
}
} }

View File

@@ -1,29 +1,33 @@
<div> <div>
<h1>Einstellungen verwalten</h1> <h1>Einstellungen verwalten</h1>
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()"> <form [formGroup]="settingsForm" (ngSubmit)="onSubmit()">
<div formArrayName="settings"> <div formArrayName="groups">
<div *ngFor="let group of settingGroups"> <fieldset *ngFor="let groupControl of groups.controls; let i = index" [formGroupName]="i">
<h3>{{ group.groupName }}</h3> <legend>{{ groupControl.get('groupName')?.value }}</legend>
<div *ngFor="let setting of group.settings; let i = index">
<!-- Find the correct form group in the FormArray by key --> <!-- +++ VERWENDUNG DER NEUEN HILFSFUNKTION +++ -->
<ng-container <div formArrayName="settings">
*ngFor="let control of settingsArray.controls; let j = index" <div *ngFor="let settingControl of getSettingsFormArray(i).controls; let j = index" [formGroupName]="j" class="setting-item">
>
<div <label [for]="'setting-value-' + i + '-' + j">
*ngIf="control.get('key')?.value === setting.key" {{ settingControl.get('description')?.value || settingControl.get('key')?.value }}
[formGroupName]="j" </label>
>
<label>{{ setting.description || setting.key }}</label> <input [id]="'setting-value-' + i + '-' + j" type="text" formControlName="value" />
<input type="text" formControlName="value" />
<label <label class="active-label">
><input type="checkbox" formControlName="isActive" /> <input type="checkbox" formControlName="isActive" />
Aktiv</label Aktiv
> </label>
</div>
</ng-container> </div>
</div> </div>
</div> <!-- +++ ENDE +++ -->
</fieldset>
</div> </div>
<button type="submit">Einstellungen speichern</button>
<br>
<button type="submit" [disabled]="settingsForm.invalid">Einstellungen speichern</button>
</form> </form>
</div> </div>

View File

@@ -14,41 +14,71 @@ export class SettingsComponent implements OnInit {
private settingService = inject(SettingService); private settingService = inject(SettingService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
settingsForm: FormGroup; settingsForm!: FormGroup;
settingGroups: { groupName: string, settings: Setting[] }[] = [];
constructor() { constructor() {
this.settingsForm = this.fb.group({ this.settingsForm = this.fb.group({
settings: this.fb.array([]) groups: this.fb.array([])
}); });
} }
get settingsArray(): FormArray { get groups(): FormArray {
return this.settingsForm.get('settings') as FormArray; return this.settingsForm.get('groups') as FormArray;
} }
// +++ NEUE HILFSFUNKTION +++
/**
* Gibt das 'settings'-FormArray für eine bestimmte Gruppe zurück.
* Ermöglicht einen sauberen Zugriff im Template.
* @param groupIndex Der Index der Gruppe im 'groups'-FormArray.
* @returns Das verschachtelte 'settings'-FormArray.
*/
getSettingsFormArray(groupIndex: number): FormArray {
return (this.groups.at(groupIndex) as FormGroup).get('settings') as FormArray;
}
// +++ ENDE NEU +++
ngOnInit(): void { ngOnInit(): void {
this.settingService.getAllGrouped().subscribe(groupedSettings => { this.settingService.getAllGrouped().subscribe(groupedSettings => {
this.settingsArray.clear(); this.groups.clear();
this.settingGroups = [];
Object.keys(groupedSettings).forEach(groupName => { Object.keys(groupedSettings).forEach(groupName => {
const settings = groupedSettings[groupName]; const settings = groupedSettings[groupName];
this.settingGroups.push({ groupName, settings });
settings.forEach(setting => { const settingsControls = settings.map(setting =>
this.settingsArray.push(this.fb.group({ this.fb.group({
key: [setting.key], key: [setting.key],
value: [setting.value], value: [setting.value],
isActive: [setting.isActive] isActive: [setting.isActive],
})); description: [setting.description]
}); })
);
this.groups.push(this.fb.group({
groupName: [groupName],
settings: this.fb.array(settingsControls)
}));
}); });
}); });
} }
onSubmit(): void { onSubmit(): void {
if (this.settingsForm.invalid) return; if (this.settingsForm.invalid) return;
this.settingService.update(this.settingsForm.value.settings).subscribe(() => {
alert('Einstellungen gespeichert!'); const allSettings: Setting[] = this.groups.value
.flatMap((group: any) => group.settings)
.map((setting: any) => ({
key: setting.key,
value: setting.value,
isActive: setting.isActive
}));
this.settingService.update(allSettings).subscribe({
next: () => alert('Einstellungen erfolgreich gespeichert!'),
error: (err) => {
alert('Fehler beim Speichern der Einstellungen.');
console.error('Failed to update settings', err);
}
}); });
} }
} }

View File

@@ -71,6 +71,15 @@
<span>reviews</span> <span>reviews</span>
</div> </div>
<!-- <div
class="nav-item"
[class.active]="activeRoute === 'settings'"
(click)="setActive('settings')"
>
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
<span>settings</span>
</div> -->
<div <div
class="nav-item" class="nav-item"
[class.active]="activeRoute === 'shipping-methods'" [class.active]="activeRoute === 'shipping-methods'"
@@ -107,5 +116,14 @@
<span>users</span> <span>users</span>
</div> </div>
<div
class="nav-item"
[class.active]="activeRoute === 'analytics'"
(click)="setActive('analytics')"
>
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
<span>analytics</span>
</div>
</nav> </nav>
</aside> </aside>