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
|
||||
|
||||
# --- Stufe 1: Bau-Umgebung ("builder") ---
|
||||
# Baut die Angular-Anwendung und erzeugt statische Dateien
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
# Der Befehl "RUN ls -la" kann jetzt entfernt werden, da das Problem identifiziert wurde.
|
||||
|
||||
# --- Stufe 2: Produktions-Umgebung ("runner") ---
|
||||
# Verwendet Node.js mit dem 'serve'-Paket, um die statischen Dateien auszuliefern
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
# --- KORREKTUR HIER ---
|
||||
COPY --from=builder /app/dist .
|
||||
EXPOSE 3001
|
||||
# --- BITTE ÜBERPRÜFEN ---
|
||||
# Möglicherweise müssen Sie auch diesen Pfad an die Struktur im "dist"-Ordner anpassen.
|
||||
CMD [ "node", "frontend/browser/index.html" ]
|
||||
|
||||
# Installiere das 'serve'-Paket global im Container
|
||||
RUN npm install -g serve
|
||||
|
||||
# Kopiere nur die gebauten Browser-Dateien aus der Builder-Stufe
|
||||
COPY --from=builder /app/dist/frontend/browser .
|
||||
|
||||
# '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 {
|
||||
title = 'frontend';
|
||||
}
|
||||
//image build
|
||||
@@ -88,6 +88,34 @@ export const routes: Routes = [
|
||||
'./features/components/shipping-methods/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 = [
|
||||
{
|
||||
path: '1',
|
||||
path: '',
|
||||
component: OrderListComponent,
|
||||
title: '',
|
||||
},
|
||||
|
||||
@@ -1,132 +1,227 @@
|
||||
<div>
|
||||
<h1>Produkte verwalten</h1>
|
||||
|
||||
<!-- Formular -->
|
||||
<!-- Das Formular bleibt unverändert und ist bereits korrekt -->
|
||||
<form [formGroup]="productForm" (ngSubmit)="onSubmit()">
|
||||
<h3>{{ selectedProductId ? 'Produkt bearbeiten' : 'Neues Produkt erstellen' }}</h3>
|
||||
|
||||
<!-- Basis-Informationen -->
|
||||
<!-- ... (Dein komplettes Formular hier) ... -->
|
||||
<h3>
|
||||
{{ selectedProductId ? "Produkt bearbeiten" : "Neues Produkt erstellen" }}
|
||||
</h3>
|
||||
<h4>Basis-Informationen</h4>
|
||||
<div><label>Name:</label><input type="text" formControlName="name" /></div>
|
||||
<div>
|
||||
<label for="name">Name:</label>
|
||||
<input id="name" type="text" formControlName="name">
|
||||
<label>Slug (automatisch generiert):</label
|
||||
><input type="text" formControlName="slug" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="slug">Slug (automatisch generiert):</label>
|
||||
<input id="slug" type="text" formControlName="slug">
|
||||
</div>
|
||||
<div>
|
||||
<label for="sku">SKU (Artikelnummer):</label>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input id="sku" type="text" formControlName="sku" style="flex-grow: 1;">
|
||||
<button type="button" (click)="generateSku()">Generieren</button>
|
||||
<label>SKU (Artikelnummer):</label>
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<input
|
||||
id="sku"
|
||||
type="text"
|
||||
formControlName="sku"
|
||||
style="flex-grow: 1"
|
||||
/><button type="button" (click)="generateSku()">Generieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Beschreibung:</label>
|
||||
<textarea id="description" formControlName="description" rows="5"></textarea>
|
||||
<label>Beschreibung:</label
|
||||
><textarea formControlName="description" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Preis & Lager -->
|
||||
<hr />
|
||||
<h4>Preis & Lager</h4>
|
||||
<div>
|
||||
<label for="price">Preis (€):</label>
|
||||
<input id="price" type="number" formControlName="price">
|
||||
<label>Preis (€):</label><input type="number" formControlName="price" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="oldPrice">Alter Preis (€) (optional):</label>
|
||||
<input id="oldPrice" type="number" formControlName="oldPrice">
|
||||
<label>Alter Preis (€) (optional):</label
|
||||
><input type="number" formControlName="oldPrice" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="purchasePrice">Einkaufspreis (€) (optional):</label>
|
||||
<input id="purchasePrice" type="number" formControlName="purchasePrice">
|
||||
<label>Einkaufspreis (€) (optional):</label
|
||||
><input type="number" formControlName="purchasePrice" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="stock">Lagerbestand:</label>
|
||||
<input id="stock" type="number" formControlName="stockQuantity">
|
||||
<label>Lagerbestand:</label
|
||||
><input type="number" formControlName="stockQuantity" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="weight">Gewicht (in kg) (optional):</label>
|
||||
<input id="weight" type="number" formControlName="weight">
|
||||
<label>Gewicht (in kg) (optional):</label
|
||||
><input type="number" formControlName="weight" />
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Zuweisungen -->
|
||||
<hr />
|
||||
<h4>Zuweisungen</h4>
|
||||
<div>
|
||||
<label for="supplier">Lieferant:</label>
|
||||
<select id="supplier" formControlName="supplierId">
|
||||
<option [ngValue]="null">-- Kein Lieferant --</option>
|
||||
<option *ngFor="let supplier of allSuppliers$ | async" [value]="supplier.id">{{ supplier.name }}</option>
|
||||
</select>
|
||||
<label>Lieferant:</label
|
||||
><select formControlName="supplierId">
|
||||
<option [ngValue]="null">-- Kein Lieferant --</option>
|
||||
<option
|
||||
*ngFor="let supplier of allSuppliers$ | async"
|
||||
[value]="supplier.id"
|
||||
>
|
||||
{{ supplier.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Kategorien:</label>
|
||||
<div class="category-checkbox-group" style="height: 100px; overflow-y: auto; border: 1px solid #ccc; padding: 5px; 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">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
[value]="category.id"
|
||||
[checked]="isCategorySelected(category.id)"
|
||||
(change)="onCategoryChange($event)">
|
||||
{{ category.name }}
|
||||
</label>
|
||||
(change)="onCategoryChange($event)"
|
||||
/>{{ category.name }}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Bilder -->
|
||||
<hr />
|
||||
<h4>Produktbilder</h4>
|
||||
<div *ngIf="selectedProductId && existingImages.length > 0">
|
||||
<p>Bestehende Bilder:</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px;">
|
||||
<div *ngFor="let img of existingImages" style="position: relative;">
|
||||
<img [src]="img.url" [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
|
||||
style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px"
|
||||
>
|
||||
<div *ngFor="let img of existingImages" style="position: relative">
|
||||
<img
|
||||
[src]="img.url"
|
||||
[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>
|
||||
<label for="main-image">Hauptbild (ersetzt bestehendes Hauptbild)</label>
|
||||
<input id="main-image" type="file" (change)="onMainFileChange($event)" accept="image/*">
|
||||
<label for="main-image">Hauptbild (ersetzt bestehendes Hauptbild)</label
|
||||
><input
|
||||
id="main-image"
|
||||
type="file"
|
||||
(change)="onMainFileChange($event)"
|
||||
accept="image/*"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="additional-images">Zusätzliche Bilder hinzufügen</label>
|
||||
<input id="additional-images" type="file" (change)="onAdditionalFilesChange($event)" accept="image/*" multiple>
|
||||
<label for="additional-images">Zusätzliche Bilder hinzufügen</label
|
||||
><input
|
||||
id="additional-images"
|
||||
type="file"
|
||||
(change)="onAdditionalFilesChange($event)"
|
||||
accept="image/*"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Status & Sichtbarkeit -->
|
||||
<hr />
|
||||
<h4>Status & Sichtbarkeit</h4>
|
||||
<div><label><input type="checkbox" formControlName="isActive"> Aktiv (im Shop sichtbar)</label></div>
|
||||
<div><label><input type="checkbox" formControlName="isFeatured"> Hervorgehoben (z.B. auf Startseite)</label></div>
|
||||
<div><label>Anzeigereihenfolge (Hervorgehoben):</label><input type="number" formControlName="featuredDisplayOrder"></div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button type="submit" [disabled]="productForm.invalid">{{ selectedProductId ? 'Aktualisieren' : 'Erstellen' }}</button>
|
||||
<button type="button" *ngIf="selectedProductId" (click)="clearSelection()">Abbrechen</button>
|
||||
<div>
|
||||
<label
|
||||
><input type="checkbox" formControlName="isActive" /> Aktiv (im Shop
|
||||
sichtbar)</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
><input type="checkbox" formControlName="isFeatured" /> Hervorgehoben
|
||||
(z.B. auf Startseite)</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label>Anzeigereihenfolge (Hervorgehoben):</label
|
||||
><input type="number" formControlName="featuredDisplayOrder" />
|
||||
</div>
|
||||
<br /><br />
|
||||
<button type="submit" [disabled]="productForm.invalid">
|
||||
{{ selectedProductId ? "Aktualisieren" : "Erstellen" }}
|
||||
</button>
|
||||
<button type="button" *ngIf="selectedProductId" (click)="clearSelection()">
|
||||
Abbrechen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Bestehende Produkte</h2>
|
||||
<ul>
|
||||
<li *ngFor="let product of products$ | async">
|
||||
{{ product.name }} (SKU: {{ product.sku }}) - Preis: {{ product.price | currency:'EUR' }}
|
||||
<button (click)="selectProduct(product)">Bearbeiten</button>
|
||||
<button (click)="onDelete(product.id)">Löschen</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr />
|
||||
<h2>Bestehende Produkte</h2>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr >
|
||||
<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)="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 {
|
||||
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>
|
||||
<h1>Einstellungen verwalten</h1>
|
||||
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()">
|
||||
<div formArrayName="settings">
|
||||
<div *ngFor="let group of settingGroups">
|
||||
<h3>{{ group.groupName }}</h3>
|
||||
<div *ngFor="let setting of group.settings; let i = index">
|
||||
<!-- Find the correct form group in the FormArray by key -->
|
||||
<ng-container
|
||||
*ngFor="let control of settingsArray.controls; let j = index"
|
||||
>
|
||||
<div
|
||||
*ngIf="control.get('key')?.value === setting.key"
|
||||
[formGroupName]="j"
|
||||
>
|
||||
<label>{{ setting.description || setting.key }}</label>
|
||||
<input type="text" formControlName="value" />
|
||||
<label
|
||||
><input type="checkbox" formControlName="isActive" />
|
||||
Aktiv</label
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
<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 *ngFor="let settingControl of getSettingsFormArray(i).controls; let j = index" [formGroupName]="j" class="setting-item">
|
||||
|
||||
<label [for]="'setting-value-' + i + '-' + j">
|
||||
{{ settingControl.get('description')?.value || settingControl.get('key')?.value }}
|
||||
</label>
|
||||
|
||||
<input [id]="'setting-value-' + i + '-' + j" type="text" formControlName="value" />
|
||||
|
||||
<label class="active-label">
|
||||
<input type="checkbox" formControlName="isActive" />
|
||||
Aktiv
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- +++ ENDE +++ -->
|
||||
</fieldset>
|
||||
</div>
|
||||
<button type="submit">Einstellungen speichern</button>
|
||||
|
||||
<br>
|
||||
<button type="submit" [disabled]="settingsForm.invalid">Einstellungen speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,41 +14,71 @@ export class SettingsComponent implements OnInit {
|
||||
private settingService = inject(SettingService);
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
settingsForm: FormGroup;
|
||||
settingGroups: { groupName: string, settings: Setting[] }[] = [];
|
||||
settingsForm!: FormGroup;
|
||||
|
||||
constructor() {
|
||||
this.settingsForm = this.fb.group({
|
||||
settings: this.fb.array([])
|
||||
groups: this.fb.array([])
|
||||
});
|
||||
}
|
||||
|
||||
get settingsArray(): FormArray {
|
||||
return this.settingsForm.get('settings') as FormArray;
|
||||
get groups(): 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 {
|
||||
this.settingService.getAllGrouped().subscribe(groupedSettings => {
|
||||
this.settingsArray.clear();
|
||||
this.settingGroups = [];
|
||||
this.groups.clear();
|
||||
|
||||
Object.keys(groupedSettings).forEach(groupName => {
|
||||
const settings = groupedSettings[groupName];
|
||||
this.settingGroups.push({ groupName, settings });
|
||||
settings.forEach(setting => {
|
||||
this.settingsArray.push(this.fb.group({
|
||||
|
||||
const settingsControls = settings.map(setting =>
|
||||
this.fb.group({
|
||||
key: [setting.key],
|
||||
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 {
|
||||
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>
|
||||
<h1>Shop-Stammdaten verwalten</h1>
|
||||
|
||||
<form *ngIf="shopInfoForm" [formGroup]="shopInfoForm" (ngSubmit)="onSubmit()">
|
||||
<input type="text" formControlName="shopName" placeholder="Shop-Name">
|
||||
<input type="text" formControlName="slogan" placeholder="Slogan">
|
||||
<input type="email" formControlName="contactEmail" placeholder="Kontakt E-Mail">
|
||||
<input type="tel" formControlName="phoneNumber" placeholder="Telefonnummer">
|
||||
<hr>
|
||||
<input type="text" formControlName="street" placeholder="Straße & Hausnummer">
|
||||
<input type="text" formControlName="city" placeholder="Stadt">
|
||||
<input type="text" formControlName="postalCode" placeholder="PLZ">
|
||||
<input type="text" formControlName="country" placeholder="Land">
|
||||
|
||||
<!-- Basis-Informationen -->
|
||||
<fieldset>
|
||||
<legend>Basis-Informationen</legend>
|
||||
<div>
|
||||
<label for="shopName">Shop-Name:</label>
|
||||
<input id="shopName" type="text" formControlName="shopName">
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
@@ -17,26 +17,53 @@ export class ShopInfoComponent implements OnInit {
|
||||
shopInfoForm!: FormGroup;
|
||||
|
||||
ngOnInit(): void {
|
||||
// --- FORMULAR UM ALLE FELDER ERWEITERT ---
|
||||
this.shopInfoForm = this.fb.group({
|
||||
// Basis-Daten
|
||||
shopName: ['', Validators.required],
|
||||
slogan: [''],
|
||||
contactEmail: ['', [Validators.required, Validators.email]],
|
||||
phoneNumber: [''],
|
||||
// Adresse
|
||||
street: [''],
|
||||
city: [''],
|
||||
postalCode: [''],
|
||||
country: ['']
|
||||
country: [''],
|
||||
// Rechtliches
|
||||
vatNumber: [''],
|
||||
companyRegistrationNumber: [''],
|
||||
// Social Media
|
||||
facebookUrl: [''],
|
||||
instagramUrl: [''],
|
||||
twitterUrl: ['']
|
||||
});
|
||||
// --- ENDE ERWEITERUNG ---
|
||||
|
||||
this.loadShopInfo();
|
||||
}
|
||||
|
||||
loadShopInfo(): void {
|
||||
this.shopInfoService.get().subscribe(data => {
|
||||
this.shopInfoForm.patchValue(data);
|
||||
if (data) {
|
||||
this.shopInfoForm.patchValue(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.shopInfoForm.invalid) return;
|
||||
this.shopInfoService.update(this.shopInfoForm.value).subscribe(() => {
|
||||
alert('Shop-Informationen gespeichert!');
|
||||
if (this.shopInfoForm.invalid) {
|
||||
this.shopInfoForm.markAllAsTouched(); // Zeigt alle Validierungsfehler an
|
||||
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>
|
||||
{{ selectedSupplierId ? "Lieferant bearbeiten" : "Neuer Lieferant" }}
|
||||
</h3>
|
||||
<input type="text" formControlName="name" placeholder="Name" />
|
||||
<input
|
||||
type="text"
|
||||
formControlName="contactPerson"
|
||||
placeholder="Ansprechpartner"
|
||||
/>
|
||||
<input type="email" formControlName="email" placeholder="E-Mail" />
|
||||
<input type="tel" formControlName="phoneNumber" placeholder="Telefon" />
|
||||
|
||||
<fieldset>
|
||||
<legend>Lieferanten-Daten</legend>
|
||||
<div><label>Name:</label><input type="text" formControlName="name" /></div>
|
||||
<div><label>Ansprechpartner:</label><input type="text" formControlName="contactPerson" /></div>
|
||||
<div><label>E-Mail:</label><input type="email" formControlName="email" /></div>
|
||||
<div><label>Telefon:</label><input type="tel" formControlName="phoneNumber" /></div>
|
||||
<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">
|
||||
{{ selectedSupplierId ? "Aktualisieren" : "Erstellen" }}
|
||||
</button>
|
||||
@@ -26,9 +41,9 @@
|
||||
<h2>Bestehende Lieferanten</h2>
|
||||
<ul>
|
||||
<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)="onDelete(supplier.id)">Löschen</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,27 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
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 {
|
||||
Address,
|
||||
CreateAddress,
|
||||
UpdateAddress,
|
||||
} from '../../../../core/models/address.model';
|
||||
|
||||
// Services
|
||||
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({
|
||||
selector: 'app-supplier-list',
|
||||
@@ -13,9 +31,12 @@ import { SupplierService } from '../../../services/supplier.service';
|
||||
})
|
||||
export class SupplierListComponent implements OnInit {
|
||||
private supplierService = inject(SupplierService);
|
||||
private addressService = inject(AddressService); // Korrekten Service verwenden
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
suppliers$!: Observable<Supplier[]>;
|
||||
// Das Dropdown für Adressen wird nicht mehr benötigt
|
||||
// addresses$!: Observable<Address[]>;
|
||||
supplierForm: FormGroup;
|
||||
selectedSupplierId: string | null = null;
|
||||
|
||||
@@ -24,16 +45,50 @@ export class SupplierListComponent implements OnInit {
|
||||
name: ['', Validators.required],
|
||||
contactPerson: [''],
|
||||
email: ['', Validators.email],
|
||||
phoneNumber: ['']
|
||||
phoneNumber: [''],
|
||||
addressId: [null],
|
||||
notes: [''],
|
||||
address: this.fb.group({
|
||||
street: [''],
|
||||
houseNumber: [''],
|
||||
city: [''],
|
||||
postalCode: [''],
|
||||
country: [''],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void { this.loadSuppliers(); }
|
||||
loadSuppliers(): void { this.suppliers$ = this.supplierService.getAll(); }
|
||||
get addressForm(): FormGroup {
|
||||
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 {
|
||||
this.selectedSupplierId = supplier.id;
|
||||
// Reset address form before patching to avoid leftover values
|
||||
this.addressForm.reset();
|
||||
this.supplierForm.patchValue(supplier);
|
||||
|
||||
if (supplier.addressId) {
|
||||
this.addressService
|
||||
.getAddressById(supplier.addressId)
|
||||
.subscribe((address) => {
|
||||
this.addressForm.patchValue(address);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
@@ -43,24 +98,80 @@ export class SupplierListComponent implements OnInit {
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.supplierForm.invalid) return;
|
||||
|
||||
const dataToSend = { ...this.supplierForm.value, id: this.selectedSupplierId || '0' };
|
||||
|
||||
if (this.selectedSupplierId) {
|
||||
this.supplierService.update(this.selectedSupplierId, dataToSend).subscribe(() => this.reset());
|
||||
const addressIdToSave$ = this.isAddressFormDirty()
|
||||
? this.saveAddress()
|
||||
: of(this.supplierForm.value.addressId || null);
|
||||
|
||||
addressIdToSave$
|
||||
.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 {
|
||||
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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private reset(): void {
|
||||
this.loadSuppliers();
|
||||
this.clearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<!-- Hier könnte ein "Rollen bearbeiten"-Button hin -->
|
||||
<button (click)="onDelete(user)">Löschen</button>
|
||||
<!-- <button (click)="onDelete(user)">Löschen</button> -->
|
||||
</td>
|
||||
</tr>
|
||||
</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">
|
||||
<div class="header-left">
|
||||
logo
|
||||
logo - 1
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex-grow: 1;
|
||||
|
||||
/* NEU: Fügt eine vertikale Scrollleiste hinzu, falls der Inhalt überläuft */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
|
||||
@@ -35,15 +35,95 @@
|
||||
|
||||
<span>discounts</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="nav-item"
|
||||
[class.active]="activeRoute === 'uebersicht'"
|
||||
(click)="setActive('uebersicht')"
|
||||
[class.active]="activeRoute === 'orders'"
|
||||
(click)="setActive('orders')"
|
||||
>
|
||||
<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>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user