Compare commits
10 Commits
437533aea6
...
dfb2968510
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb2968510 | ||
|
|
1607832fcc | ||
|
|
0fd834089a | ||
|
|
ef994b89e3 | ||
|
|
e91fe838fa | ||
|
|
4924372087 | ||
|
|
8813dc21ee | ||
|
|
a38ab54119 | ||
|
|
5833964bec | ||
|
|
844c30c90e |
24
Dockerfile
24
Dockerfile
@@ -1,21 +1,29 @@
|
|||||||
# Dockerfile
|
# Dockerfile
|
||||||
|
|
||||||
# --- Stufe 1: Bau-Umgebung ("builder") ---
|
# --- Stufe 1: Bau-Umgebung ("builder") ---
|
||||||
|
# Baut die Angular-Anwendung und erzeugt statische Dateien
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
# Der Befehl "RUN ls -la" kann jetzt entfernt werden, da das Problem identifiziert wurde.
|
|
||||||
|
|
||||||
# --- Stufe 2: Produktions-Umgebung ("runner") ---
|
# --- Stufe 2: Produktions-Umgebung ("runner") ---
|
||||||
|
# Verwendet Node.js mit dem 'serve'-Paket, um die statischen Dateien auszuliefern
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
|
||||||
# --- KORREKTUR HIER ---
|
# Installiere das 'serve'-Paket global im Container
|
||||||
COPY --from=builder /app/dist .
|
RUN npm install -g serve
|
||||||
EXPOSE 3001
|
|
||||||
# --- BITTE ÜBERPRÜFEN ---
|
# Kopiere nur die gebauten Browser-Dateien aus der Builder-Stufe
|
||||||
# Möglicherweise müssen Sie auch diesen Pfad an die Struktur im "dist"-Ordner anpassen.
|
COPY --from=builder /app/dist/frontend/browser .
|
||||||
CMD [ "node", "frontend/browser/index.html" ]
|
|
||||||
|
# 'serve' läuft standardmäßig auf Port 3000. Wir exposen diesen Port.
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Starte den 'serve'-Webserver.
|
||||||
|
# Der Parameter "-s" ist wichtig für Single-Page-Applications wie Angular.
|
||||||
|
# Er stellt sicher, dass alle Anfragen an die index.html weitergeleitet werden.
|
||||||
|
CMD [ "serve", "-s", "." ]
|
||||||
@@ -12,3 +12,4 @@ import { CookieConsentComponent } from './core/components/cookie-consent/cookie-
|
|||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'frontend';
|
title = 'frontend';
|
||||||
}
|
}
|
||||||
|
//image build
|
||||||
@@ -88,6 +88,34 @@ export const routes: Routes = [
|
|||||||
'./features/components/shipping-methods/shipping-methods.routes'
|
'./features/components/shipping-methods/shipping-methods.routes'
|
||||||
).then((r) => r.SHIPPING_METHODS_ROUTES),
|
).then((r) => r.SHIPPING_METHODS_ROUTES),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'shop-info',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./features/components/shop-info/shop-info.routes').then(
|
||||||
|
(r) => r.SHOP_INFO_ROUTES
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'supplier-list',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./features/components/suppliers/suppliers.routes').then(
|
||||||
|
(r) => r.SUPPLIERS_ROUTES
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./features/components/users/users.routes').then(
|
||||||
|
(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
|
||||||
</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>
|
||||||
@@ -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 ''; // Platzhalter, wenn gar keine Bilder vorhanden sind
|
||||||
|
}
|
||||||
|
const mainImage = images.find(img => img.isMainImage);
|
||||||
|
return mainImage?.url || images[0].url || ''; // 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="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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
11
src/app/features/components/shop-info/shop-info.routes.ts
Normal file
11
src/app/features/components/shop-info/shop-info.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { ShopInfoComponent } from './shop-info/shop-info.component';
|
||||||
|
|
||||||
|
|
||||||
|
export const SHOP_INFO_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ShopInfoComponent,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,15 +1,87 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>Shop-Stammdaten verwalten</h1>
|
<h1>Shop-Stammdaten verwalten</h1>
|
||||||
|
|
||||||
<form *ngIf="shopInfoForm" [formGroup]="shopInfoForm" (ngSubmit)="onSubmit()">
|
<form *ngIf="shopInfoForm" [formGroup]="shopInfoForm" (ngSubmit)="onSubmit()">
|
||||||
<input type="text" formControlName="shopName" placeholder="Shop-Name">
|
|
||||||
<input type="text" formControlName="slogan" placeholder="Slogan">
|
<!-- Basis-Informationen -->
|
||||||
<input type="email" formControlName="contactEmail" placeholder="Kontakt E-Mail">
|
<fieldset>
|
||||||
<input type="tel" formControlName="phoneNumber" placeholder="Telefonnummer">
|
<legend>Basis-Informationen</legend>
|
||||||
<hr>
|
<div>
|
||||||
<input type="text" formControlName="street" placeholder="Straße & Hausnummer">
|
<label for="shopName">Shop-Name:</label>
|
||||||
<input type="text" formControlName="city" placeholder="Stadt">
|
<input id="shopName" type="text" formControlName="shopName">
|
||||||
<input type="text" formControlName="postalCode" placeholder="PLZ">
|
</div>
|
||||||
<input type="text" formControlName="country" placeholder="Land">
|
<div>
|
||||||
|
<label for="slogan">Slogan:</label>
|
||||||
|
<input id="slogan" type="text" formControlName="slogan">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Kontaktdaten -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Kontaktdaten</legend>
|
||||||
|
<div>
|
||||||
|
<label for="contactEmail">Kontakt E-Mail:</label>
|
||||||
|
<input id="contactEmail" type="email" formControlName="contactEmail">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="phoneNumber">Telefonnummer:</label>
|
||||||
|
<input id="phoneNumber" type="tel" formControlName="phoneNumber">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Adresse -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Firmenadresse</legend>
|
||||||
|
<div>
|
||||||
|
<label for="street">Straße & Hausnummer:</label>
|
||||||
|
<input id="street" type="text" formControlName="street">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="city">Stadt:</label>
|
||||||
|
<input id="city" type="text" formControlName="city">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="postalCode">PLZ:</label>
|
||||||
|
<input id="postalCode" type="text" formControlName="postalCode">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="country">Land:</label>
|
||||||
|
<input id="country" type="text" formControlName="country">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Rechtliche Informationen -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Rechtliche Informationen</legend>
|
||||||
|
<div>
|
||||||
|
<label for="vatNumber">Umsatzsteuer-ID:</label>
|
||||||
|
<input id="vatNumber" type="text" formControlName="vatNumber">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="regNumber">Handelsregisternummer:</label>
|
||||||
|
<input id="regNumber" type="text" formControlName="companyRegistrationNumber">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Social Media -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Social Media Links</legend>
|
||||||
|
<div>
|
||||||
|
<label for="fbUrl">Facebook URL:</label>
|
||||||
|
<input id="fbUrl" type="url" formControlName="facebookUrl" placeholder="https://facebook.com/deinshop">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="igUrl">Instagram URL:</label>
|
||||||
|
<input id="igUrl" type="url" formControlName="instagramUrl" placeholder="https://instagram.com/deinshop">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="twUrl">Twitter/X URL:</label>
|
||||||
|
<input id="twUrl" type="url" formControlName="twitterUrl" placeholder="https://x.com/deinshop">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
<button type="submit" [disabled]="shopInfoForm.invalid">Speichern</button>
|
<button type="submit" [disabled]="shopInfoForm.invalid">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,26 +17,53 @@ export class ShopInfoComponent implements OnInit {
|
|||||||
shopInfoForm!: FormGroup;
|
shopInfoForm!: FormGroup;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// --- FORMULAR UM ALLE FELDER ERWEITERT ---
|
||||||
this.shopInfoForm = this.fb.group({
|
this.shopInfoForm = this.fb.group({
|
||||||
|
// Basis-Daten
|
||||||
shopName: ['', Validators.required],
|
shopName: ['', Validators.required],
|
||||||
slogan: [''],
|
slogan: [''],
|
||||||
contactEmail: ['', [Validators.required, Validators.email]],
|
contactEmail: ['', [Validators.required, Validators.email]],
|
||||||
phoneNumber: [''],
|
phoneNumber: [''],
|
||||||
|
// Adresse
|
||||||
street: [''],
|
street: [''],
|
||||||
city: [''],
|
city: [''],
|
||||||
postalCode: [''],
|
postalCode: [''],
|
||||||
country: ['']
|
country: [''],
|
||||||
|
// Rechtliches
|
||||||
|
vatNumber: [''],
|
||||||
|
companyRegistrationNumber: [''],
|
||||||
|
// Social Media
|
||||||
|
facebookUrl: [''],
|
||||||
|
instagramUrl: [''],
|
||||||
|
twitterUrl: ['']
|
||||||
});
|
});
|
||||||
|
// --- ENDE ERWEITERUNG ---
|
||||||
|
|
||||||
|
this.loadShopInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadShopInfo(): void {
|
||||||
this.shopInfoService.get().subscribe(data => {
|
this.shopInfoService.get().subscribe(data => {
|
||||||
this.shopInfoForm.patchValue(data);
|
if (data) {
|
||||||
|
this.shopInfoForm.patchValue(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.shopInfoForm.invalid) return;
|
if (this.shopInfoForm.invalid) {
|
||||||
this.shopInfoService.update(this.shopInfoForm.value).subscribe(() => {
|
this.shopInfoForm.markAllAsTouched(); // Zeigt alle Validierungsfehler an
|
||||||
alert('Shop-Informationen gespeichert!');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shopInfoService.update(this.shopInfoForm.value).subscribe({
|
||||||
|
next: () => {
|
||||||
|
alert('Shop-Informationen erfolgreich gespeichert!');
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
alert('Fehler beim Speichern der Informationen.');
|
||||||
|
console.error('Failed to update shop info', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,14 +5,29 @@
|
|||||||
<h3>
|
<h3>
|
||||||
{{ selectedSupplierId ? "Lieferant bearbeiten" : "Neuer Lieferant" }}
|
{{ selectedSupplierId ? "Lieferant bearbeiten" : "Neuer Lieferant" }}
|
||||||
</h3>
|
</h3>
|
||||||
<input type="text" formControlName="name" placeholder="Name" />
|
|
||||||
<input
|
<fieldset>
|
||||||
type="text"
|
<legend>Lieferanten-Daten</legend>
|
||||||
formControlName="contactPerson"
|
<div><label>Name:</label><input type="text" formControlName="name" /></div>
|
||||||
placeholder="Ansprechpartner"
|
<div><label>Ansprechpartner:</label><input type="text" formControlName="contactPerson" /></div>
|
||||||
/>
|
<div><label>E-Mail:</label><input type="email" formControlName="email" /></div>
|
||||||
<input type="email" formControlName="email" placeholder="E-Mail" />
|
<div><label>Telefon:</label><input type="tel" formControlName="phoneNumber" /></div>
|
||||||
<input type="tel" formControlName="phoneNumber" placeholder="Telefon" />
|
<div><label>Notizen:</label><textarea formControlName="notes"></textarea></div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- +++ NEUES ADRESS-FORMULAR +++ -->
|
||||||
|
<fieldset formGroupName="address">
|
||||||
|
<legend>Adresse (optional)</legend>
|
||||||
|
<div><label>Straße:</label><input type="text" formControlName="street" /></div>
|
||||||
|
<div><label>Hausnummer:</label><input type="text" formControlName="houseNumber" /></div>
|
||||||
|
<div><label>Stadt:</label><input type="text" formControlName="city" /></div>
|
||||||
|
<div><label>PLZ:</label><input type="text" formControlName="postalCode" /></div>
|
||||||
|
<div><label>Land:</label><input type="text" formControlName="country" /></div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- +++ ENDE NEU +++ -->
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
<button type="submit" [disabled]="supplierForm.invalid">
|
<button type="submit" [disabled]="supplierForm.invalid">
|
||||||
{{ selectedSupplierId ? "Aktualisieren" : "Erstellen" }}
|
{{ selectedSupplierId ? "Aktualisieren" : "Erstellen" }}
|
||||||
</button>
|
</button>
|
||||||
@@ -26,7 +41,7 @@
|
|||||||
<h2>Bestehende Lieferanten</h2>
|
<h2>Bestehende Lieferanten</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let supplier of suppliers$ | async">
|
<li *ngFor="let supplier of suppliers$ | async">
|
||||||
{{ supplier.name }} ({{ supplier.contactPerson }})
|
{{ supplier.name }} ({{ supplier.contactPerson || 'Kein Ansprechpartner' }})
|
||||||
<button (click)="selectSupplier(supplier)">Bearbeiten</button>
|
<button (click)="selectSupplier(supplier)">Bearbeiten</button>
|
||||||
<button (click)="onDelete(supplier.id)">Löschen</button>
|
<button (click)="onDelete(supplier.id)">Löschen</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import {
|
||||||
import { Observable } from 'rxjs';
|
FormBuilder,
|
||||||
|
FormGroup,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { switchMap, map } from 'rxjs/operators'; // <-- KORREKTUR: 'map' importiert
|
||||||
|
|
||||||
|
// Models
|
||||||
import { Supplier } from '../../../../core/models/supplier.model';
|
import { Supplier } from '../../../../core/models/supplier.model';
|
||||||
|
import {
|
||||||
|
Address,
|
||||||
|
CreateAddress,
|
||||||
|
UpdateAddress,
|
||||||
|
} from '../../../../core/models/address.model';
|
||||||
|
|
||||||
|
// Services
|
||||||
import { SupplierService } from '../../../services/supplier.service';
|
import { SupplierService } from '../../../services/supplier.service';
|
||||||
|
import { AddressService } from '../../../services/address.service';
|
||||||
|
// HINWEIS: Du hast den CustomerAddressService nicht mehr gebraucht, daher habe ich ihn entfernt.
|
||||||
|
// Falls du ihn doch an anderer Stelle brauchst, füge den Import wieder hinzu.
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-supplier-list',
|
selector: 'app-supplier-list',
|
||||||
@@ -13,9 +31,12 @@ import { SupplierService } from '../../../services/supplier.service';
|
|||||||
})
|
})
|
||||||
export class SupplierListComponent implements OnInit {
|
export class SupplierListComponent implements OnInit {
|
||||||
private supplierService = inject(SupplierService);
|
private supplierService = inject(SupplierService);
|
||||||
|
private addressService = inject(AddressService); // Korrekten Service verwenden
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
suppliers$!: Observable<Supplier[]>;
|
suppliers$!: Observable<Supplier[]>;
|
||||||
|
// Das Dropdown für Adressen wird nicht mehr benötigt
|
||||||
|
// addresses$!: Observable<Address[]>;
|
||||||
supplierForm: FormGroup;
|
supplierForm: FormGroup;
|
||||||
selectedSupplierId: string | null = null;
|
selectedSupplierId: string | null = null;
|
||||||
|
|
||||||
@@ -24,16 +45,50 @@ export class SupplierListComponent implements OnInit {
|
|||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
contactPerson: [''],
|
contactPerson: [''],
|
||||||
email: ['', Validators.email],
|
email: ['', Validators.email],
|
||||||
phoneNumber: ['']
|
phoneNumber: [''],
|
||||||
|
addressId: [null],
|
||||||
|
notes: [''],
|
||||||
|
address: this.fb.group({
|
||||||
|
street: [''],
|
||||||
|
houseNumber: [''],
|
||||||
|
city: [''],
|
||||||
|
postalCode: [''],
|
||||||
|
country: [''],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void { this.loadSuppliers(); }
|
get addressForm(): FormGroup {
|
||||||
loadSuppliers(): void { this.suppliers$ = this.supplierService.getAll(); }
|
return this.supplierForm.get('address') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAddressFormDirty(): boolean {
|
||||||
|
const address = this.addressForm.value;
|
||||||
|
return Object.values(address).some((value) => !!value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadSuppliers();
|
||||||
|
// this.addresses$ = this.adminAddressService.getAllUnlinkedAddresses(); // Lade ungebundene Adressen
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSuppliers(): void {
|
||||||
|
this.suppliers$ = this.supplierService.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
selectSupplier(supplier: Supplier): void {
|
selectSupplier(supplier: Supplier): void {
|
||||||
this.selectedSupplierId = supplier.id;
|
this.selectedSupplierId = supplier.id;
|
||||||
|
// Reset address form before patching to avoid leftover values
|
||||||
|
this.addressForm.reset();
|
||||||
this.supplierForm.patchValue(supplier);
|
this.supplierForm.patchValue(supplier);
|
||||||
|
|
||||||
|
if (supplier.addressId) {
|
||||||
|
this.addressService
|
||||||
|
.getAddressById(supplier.addressId)
|
||||||
|
.subscribe((address) => {
|
||||||
|
this.addressForm.patchValue(address);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSelection(): void {
|
clearSelection(): void {
|
||||||
@@ -44,17 +99,73 @@ export class SupplierListComponent implements OnInit {
|
|||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.supplierForm.invalid) return;
|
if (this.supplierForm.invalid) return;
|
||||||
|
|
||||||
const dataToSend = { ...this.supplierForm.value, id: this.selectedSupplierId || '0' };
|
const addressIdToSave$ = this.isAddressFormDirty()
|
||||||
|
? this.saveAddress()
|
||||||
|
: of(this.supplierForm.value.addressId || null);
|
||||||
|
|
||||||
if (this.selectedSupplierId) {
|
addressIdToSave$
|
||||||
this.supplierService.update(this.selectedSupplierId, dataToSend).subscribe(() => this.reset());
|
.pipe(
|
||||||
|
switchMap((addressId: string | null) => {
|
||||||
|
const supplierData: Supplier = {
|
||||||
|
...this.supplierForm.value,
|
||||||
|
id: this.selectedSupplierId || undefined,
|
||||||
|
addressId: addressId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.selectedSupplierId) {
|
||||||
|
return this.supplierService.update(
|
||||||
|
this.selectedSupplierId,
|
||||||
|
supplierData
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.supplierService.create(supplierData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(() => this.reset());
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveAddress(): Observable<string | null> {
|
||||||
|
const supplierName = this.supplierForm.get('name')?.value || 'Lieferant';
|
||||||
|
const contactPerson = this.supplierForm
|
||||||
|
.get('contactPerson')
|
||||||
|
?.value.split(' ') || [''];
|
||||||
|
const firstName = contactPerson[0] || supplierName;
|
||||||
|
const lastName = contactPerson.slice(1).join(' ') || '(Lieferant)';
|
||||||
|
|
||||||
|
const addressData = {
|
||||||
|
...this.addressForm.value,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
type: 'Billing',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingAddressId = this.supplierForm.get('addressId')?.value;
|
||||||
|
|
||||||
|
if (existingAddressId) {
|
||||||
|
const updateData: UpdateAddress = {
|
||||||
|
...addressData,
|
||||||
|
id: existingAddressId,
|
||||||
|
};
|
||||||
|
return this.addressService
|
||||||
|
.updateAddress(existingAddressId, updateData)
|
||||||
|
.pipe(
|
||||||
|
map(() => existingAddressId) // Gibt die bestehende ID zurück
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.supplierService.create(dataToSend).subscribe(() => this.reset());
|
const createData: CreateAddress = addressData;
|
||||||
|
return this.addressService.createAddress(createData).pipe(
|
||||||
|
map((createdAddress: Address) => createdAddress.id) // Gibt die neue ID zurück
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDelete(id: string): void {
|
onDelete(id: string): void {
|
||||||
if (confirm('Lieferant wirklich löschen?')) {
|
if (
|
||||||
|
confirm(
|
||||||
|
'Lieferant wirklich löschen? (Zugehörige Adresse wird nicht gelöscht)'
|
||||||
|
)
|
||||||
|
) {
|
||||||
this.supplierService.delete(id).subscribe(() => this.loadSuppliers());
|
this.supplierService.delete(id).subscribe(() => this.loadSuppliers());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/app/features/components/suppliers/suppliers.routes.ts
Normal file
11
src/app/features/components/suppliers/suppliers.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { SupplierListComponent } from './supplier-list/supplier-list.component';
|
||||||
|
|
||||||
|
|
||||||
|
export const SUPPLIERS_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SupplierListComponent,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<td>{{ user.roles?.join(', ') }}</td>
|
<td>{{ user.roles?.join(', ') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<!-- Hier könnte ein "Rollen bearbeiten"-Button hin -->
|
<!-- Hier könnte ein "Rollen bearbeiten"-Button hin -->
|
||||||
<button (click)="onDelete(user)">Löschen</button>
|
<!-- <button (click)="onDelete(user)">Löschen</button> -->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
11
src/app/features/components/users/users.routes.ts
Normal file
11
src/app/features/components/users/users.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { UserListComponent } from './user-list/user-list.component';
|
||||||
|
|
||||||
|
|
||||||
|
export const USERS_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: UserListComponent,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
59
src/app/features/services/address.service.ts
Normal file
59
src/app/features/services/address.service.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { API_URL } from '../../core/tokens/api-url.token';
|
||||||
|
import { Address, CreateAddress, UpdateAddress } from '../../core/models/address.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AddressService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private apiUrl = inject(API_URL);
|
||||||
|
private readonly endpoint = '/admin/AdminAddresses';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all addresses that are not linked to a customer (e.g., for suppliers).
|
||||||
|
* @returns An Observable array of Address objects.
|
||||||
|
*/
|
||||||
|
getAllUnlinkedAddresses(): Observable<Address[]> {
|
||||||
|
return this.http.get<Address[]>(`${this.apiUrl}${this.endpoint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single unlinked address by its ID.
|
||||||
|
* @param id The unique identifier of the address.
|
||||||
|
* @returns An Observable of a single Address object.
|
||||||
|
*/
|
||||||
|
getAddressById(id: string): Observable<Address> {
|
||||||
|
return this.http.get<Address>(`${this.apiUrl}${this.endpoint}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new unlinked address.
|
||||||
|
* @param data The data for the new address.
|
||||||
|
* @returns An Observable of the newly created Address object.
|
||||||
|
*/
|
||||||
|
createAddress(data: CreateAddress): Observable<Address> {
|
||||||
|
return this.http.post<Address>(`${this.apiUrl}${this.endpoint}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing unlinked address.
|
||||||
|
* @param id The unique identifier of the address to update.
|
||||||
|
* @param data The updated address data.
|
||||||
|
* @returns An Observable that completes when the update is successful.
|
||||||
|
*/
|
||||||
|
updateAddress(id: string, data: UpdateAddress): Observable<void> {
|
||||||
|
return this.http.put<void>(`${this.apiUrl}${this.endpoint}/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an unlinked address by its ID.
|
||||||
|
* @param id The unique identifier of the address to delete.
|
||||||
|
* @returns An Observable that completes when the deletion is successful.
|
||||||
|
*/
|
||||||
|
deleteAddress(id: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(`${this.apiUrl}${this.endpoint}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
logo
|
logo - 1
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
/* NEU: Fügt eine vertikale Scrollleiste hinzu, falls der Inhalt überläuft */
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
|
|||||||
@@ -35,15 +35,95 @@
|
|||||||
|
|
||||||
<span>discounts</span>
|
<span>discounts</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
[class.active]="activeRoute === 'uebersicht'"
|
[class.active]="activeRoute === 'orders'"
|
||||||
(click)="setActive('uebersicht')"
|
(click)="setActive('orders')"
|
||||||
>
|
>
|
||||||
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
<span>Übersicht</span>
|
<span>orders</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'payment-methods'"
|
||||||
|
(click)="setActive('payment-methods')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>payment-methods</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'products'"
|
||||||
|
(click)="setActive('products')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>products</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'reviews'"
|
||||||
|
(click)="setActive('reviews')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>reviews</span>
|
||||||
|
</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
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'shipping-methods'"
|
||||||
|
(click)="setActive('shipping-methods')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>shipping-methods</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'shop-info'"
|
||||||
|
(click)="setActive('shop-info')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>shop-info</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'supplier-list'"
|
||||||
|
(click)="setActive('supplier-list')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>supplier-list</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'users'"
|
||||||
|
(click)="setActive('users')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>users</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
[class.active]="activeRoute === 'analytics'"
|
||||||
|
(click)="setActive('analytics')"
|
||||||
|
>
|
||||||
|
<app-icon [iconName]="'placeholder'" [svgColor]="'#8e44ad'"></app-icon>
|
||||||
|
|
||||||
|
<span>analytics</span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
Reference in New Issue
Block a user