Compare commits

...

10 Commits

Author SHA1 Message Date
Tizian.Breuch
dfb2968510 image build
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m7s
2025-10-23 11:39:50 +02:00
Tizian.Breuch
1607832fcc sidenav scroll 2025-10-23 10:10:14 +02:00
Tizian.Breuch
0fd834089a logo 2025-10-21 09:53:58 +02:00
Tizian.Breuch
ef994b89e3 changes 2025-10-10 15:46:03 +02:00
Tizian.Breuch
e91fe838fa btn raus 2025-10-10 12:29:03 +02:00
Tizian.Breuch
4924372087 users 2025-10-10 12:19:04 +02:00
Tizian.Breuch
8813dc21ee supplier und address 2025-10-10 12:13:09 +02:00
Tizian.Breuch
a38ab54119 shop info 2025-10-10 10:55:19 +02:00
Tizian.Breuch
5833964bec ok 2025-10-09 16:55:37 +02:00
Tizian.Breuch
844c30c90e df2 2025-10-09 16:52:39 +02:00
24 changed files with 872 additions and 175 deletions

View File

@@ -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", "." ]

View File

@@ -12,3 +12,4 @@ import { CookieConsentComponent } from './core/components/cookie-consent/cookie-
export class AppComponent { export class AppComponent {
title = 'frontend'; title = 'frontend';
} }
//image build

View File

@@ -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
),
},
], ],
}, },
{ {

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { OrderListComponent } from './order-list/order-list.component';
export const ORDERS_ROUTES: Routes = [ export const ORDERS_ROUTES: Routes = [
{ {
path: '1', path: '',
component: OrderListComponent, component: OrderListComponent,
title: '', title: '',
}, },

View File

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

View File

@@ -265,4 +265,18 @@ export class ProductListComponent implements OnInit, OnDestroy {
private capitalizeFirstLetter(string: string): string { private capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
/**
* Sucht das Hauptbild aus der Bilderliste eines Produkts und gibt dessen URL zurück.
* Gibt eine Platzhalter-URL zurück, wenn kein Hauptbild gefunden wird.
* @param images Die Liste der Produktbilder.
* @returns Die URL des Hauptbildes oder eine Platzhalter-URL.
*/
getMainImageUrl(images?: ProductImage[]): string {
if (!images || images.length === 0) {
return ''; // 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
}
} }

View File

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

View File

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

View File

@@ -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: '',
},
];

View File

@@ -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>

View File

@@ -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);
}
}); });
} }
} }

View File

@@ -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>

View File

@@ -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());
} }
} }

View 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: '',
},
];

View File

@@ -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>

View 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: '',
},
];

View 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}`);
}
}

View File

@@ -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">

View File

@@ -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 {

View File

@@ -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>