changes
This commit is contained in:
@@ -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
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
11
src/app/features/components/analytics/analytics.routes.ts
Normal file
11
src/app/features/components/analytics/analytics.routes.ts
Normal 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: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
*ngFor="let supplier of allSuppliers$ | async"
|
||||||
|
[value]="supplier.id"
|
||||||
|
>
|
||||||
|
{{ supplier.name }}
|
||||||
|
</option>
|
||||||
</select>
|
</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>
|
||||||
|
<th style="padding: 8px; border: 1px solid #ddd;">Name</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #ddd;">SKU</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #ddd;">Preis</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #ddd;">Lagerbestand</th>
|
||||||
|
<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)="selectProduct(product)">Bearbeiten</button>
|
||||||
<button (click)="onDelete(product.id)">Löschen</button>
|
<button (click)="onDelete(product.id)" style="margin-left: 5px; background-color: #dc3545; color: white;">Löschen</button>
|
||||||
</li>
|
</td>
|
||||||
</ul>
|
</tr>
|
||||||
</div>
|
</ng-container>
|
||||||
|
<ng-template #loading>
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" style="text-align: center; padding: 16px;">Lade Produkte...</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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="groups">
|
||||||
|
<fieldset *ngFor="let groupControl of groups.controls; let i = index" [formGroupName]="i">
|
||||||
|
<legend>{{ groupControl.get('groupName')?.value }}</legend>
|
||||||
|
|
||||||
|
<!-- +++ VERWENDUNG DER NEUEN HILFSFUNKTION +++ -->
|
||||||
<div formArrayName="settings">
|
<div formArrayName="settings">
|
||||||
<div *ngFor="let group of settingGroups">
|
<div *ngFor="let settingControl of getSettingsFormArray(i).controls; let j = index" [formGroupName]="j" class="setting-item">
|
||||||
<h3>{{ group.groupName }}</h3>
|
|
||||||
<div *ngFor="let setting of group.settings; let i = index">
|
<label [for]="'setting-value-' + i + '-' + j">
|
||||||
<!-- Find the correct form group in the FormArray by key -->
|
{{ settingControl.get('description')?.value || settingControl.get('key')?.value }}
|
||||||
<ng-container
|
</label>
|
||||||
*ngFor="let control of settingsArray.controls; let j = index"
|
|
||||||
>
|
<input [id]="'setting-value-' + i + '-' + j" type="text" formControlName="value" />
|
||||||
<div
|
|
||||||
*ngIf="control.get('key')?.value === setting.key"
|
<label class="active-label">
|
||||||
[formGroupName]="j"
|
<input type="checkbox" formControlName="isActive" />
|
||||||
>
|
Aktiv
|
||||||
<label>{{ setting.description || setting.key }}</label>
|
</label>
|
||||||
<input type="text" formControlName="value" />
|
|
||||||
<label
|
|
||||||
><input type="checkbox" formControlName="isActive" />
|
|
||||||
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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user