Compare commits
2 Commits
c10e6b4faa
...
dfe631edf6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe631edf6 | ||
|
|
ac42f8b1b9 |
@@ -6,4 +6,7 @@ export interface ShippingMethod {
|
||||
isActive: boolean;
|
||||
minDeliveryDays: number;
|
||||
maxDeliveryDays: number;
|
||||
// NEU: Gewichtsgrenzen
|
||||
minWeight: number;
|
||||
maxWeight: number;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
.form-header {
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h3[card-header] {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
|
||||
<ng-template #formContent>
|
||||
<div class="form-header">
|
||||
<div class="page-header">
|
||||
<h3 card-header>Neues Produkt erstellen</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.form-header {
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h3[card-header] {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
|
||||
<ng-template #formContent>
|
||||
<div class="form-header">
|
||||
<div class="page-header">
|
||||
<h3 card-header>Produkt bearbeiten</h3>
|
||||
</div>
|
||||
<app-product-form
|
||||
|
||||
@@ -282,6 +282,11 @@ export class ProductEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
if (mainImagePreview && !this.newImageFiles.has(mainImagePreview.identifier)) {
|
||||
console.log('Setze bestehendes Bild als Main:', mainImagePreview.identifier);
|
||||
formData.append('SetMainImageId', mainImagePreview.identifier);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
app-search-bar {
|
||||
flex-grow: 1;
|
||||
max-width: 400px;
|
||||
@@ -20,7 +28,9 @@ app-search-bar {
|
||||
}
|
||||
|
||||
.column-filter-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.column-filter-dropdown {
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
<!-- /src/app/features/admin/components/products/product-list/product-list.component.html -->
|
||||
|
||||
<div class="page-container">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Produktübersicht</h1>
|
||||
<app-button buttonType="primary" (click)="onAddNew()">
|
||||
<app-icon iconName="plus"></app-icon> Neues Produkt
|
||||
</app-button>
|
||||
<div>
|
||||
<h3 class="page-header">Produktübersicht</h3>
|
||||
</div>
|
||||
|
||||
<div class="table-header">
|
||||
<app-search-bar placeholder="Produkte nach Name oder SKU suchen..." (search)="onSearch($event)"></app-search-bar>
|
||||
<app-search-bar
|
||||
placeholder="Produkte nach Name oder SKU suchen..."
|
||||
(search)="onSearch($event)"
|
||||
></app-search-bar>
|
||||
|
||||
<div class="column-filter-container">
|
||||
<app-button buttonType="stroked" (click)="toggleColumnFilter()" iconName="filter">Spalten</app-button>
|
||||
<app-button buttonType="primary" iconName="plus" (click)="onAddNew()">
|
||||
Neues Produkt
|
||||
</app-button>
|
||||
|
||||
<app-button
|
||||
buttonType="stroked"
|
||||
(click)="toggleColumnFilter()"
|
||||
iconName="filter"
|
||||
>Spalten</app-button
|
||||
>
|
||||
<div class="column-filter-dropdown" *ngIf="isColumnFilterVisible">
|
||||
<label *ngFor="let col of allTableColumns">
|
||||
<input type="checkbox" [checked]="isColumnVisible(col.key)" (change)="onColumnToggle(col, $event)">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isColumnVisible(col.key)"
|
||||
(change)="onColumnToggle(col, $event)"
|
||||
/>
|
||||
{{ col.title }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -25,6 +39,7 @@
|
||||
[data]="filteredProducts"
|
||||
[columns]="visibleTableColumns"
|
||||
(edit)="onEditProduct($event.id)"
|
||||
(delete)="onDeleteProduct($event.id)">
|
||||
(delete)="onDeleteProduct($event.id)"
|
||||
>
|
||||
</app-generic-table>
|
||||
</div>
|
||||
@@ -11,12 +11,11 @@ import { StorageService } from '../../../../core/services/storage.service';
|
||||
import { GenericTableComponent, ColumnConfig } from '../../../../shared/components/data-display/generic-table/generic-table.component';
|
||||
import { SearchBarComponent } from '../../../../shared/components/layout/search-bar/search-bar.component';
|
||||
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
|
||||
import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
|
||||
imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent],
|
||||
providers: [DatePipe],
|
||||
templateUrl: './product-list.component.html',
|
||||
styleUrl: './product-list.component.css'
|
||||
@@ -59,6 +58,8 @@ export class ProductListComponent implements OnInit {
|
||||
];
|
||||
visibleTableColumns: ColumnConfig[] = [];
|
||||
|
||||
public readonly fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNjY2NjY2MiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxjaXJjbGUgY3g9IjkiIGN5PSI5IiByPSIyIi8+PHBhdGggZD0ibTIxIDE1LTUtNWwtNSA1bC0yLTJsLTUgNSIvPjwvc3ZnPg==';
|
||||
|
||||
constructor() {
|
||||
this.loadTableSettings();
|
||||
}
|
||||
@@ -144,8 +145,8 @@ export class ProductListComponent implements OnInit {
|
||||
}
|
||||
|
||||
getMainImageUrl(images?: ProductImage[]): string {
|
||||
if (!images || images.length === 0) return 'https://via.placeholder.com/50';
|
||||
if (!images || images.length === 0) return this.fallbackImage;
|
||||
const mainImage = images.find(img => img.isMainImage);
|
||||
return mainImage?.url || images[0]?.url || 'https://via.placeholder.com/50';
|
||||
return mainImage?.url || images[0]?.url || this.fallbackImage;
|
||||
}
|
||||
}
|
||||
@@ -22,27 +22,51 @@
|
||||
<input type="number" formControlName="cost" placeholder="Kosten" />
|
||||
</div>
|
||||
|
||||
<!-- +++ NEUE FELDER HINZUGEFÜGT +++ -->
|
||||
<div>
|
||||
<label>Minimale Liefertage:</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="minDeliveryDays"
|
||||
placeholder="z.B. 1"
|
||||
/>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<div>
|
||||
<label>Minimale Liefertage:</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="minDeliveryDays"
|
||||
placeholder="z.B. 1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Maximale Liefertage:</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="maxDeliveryDays"
|
||||
placeholder="z.B. 3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Maximale Liefertage:</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="maxDeliveryDays"
|
||||
placeholder="z.B. 3"
|
||||
/>
|
||||
<!-- +++ NEUE GEWICHTS FELDER +++ -->
|
||||
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||||
<div>
|
||||
<label>Gewicht von (kg):</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="minWeight"
|
||||
placeholder="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Gewicht bis (kg):</label>
|
||||
<input
|
||||
type="number"
|
||||
formControlName="maxWeight"
|
||||
placeholder="10"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- +++ ENDE NEU +++ -->
|
||||
|
||||
<div>
|
||||
<div style="margin-top: 10px;">
|
||||
<label><input type="checkbox" formControlName="isActive" /> Aktiv</label>
|
||||
</div>
|
||||
|
||||
@@ -55,16 +79,26 @@
|
||||
Abbrechen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Bestehende Methoden</h2>
|
||||
<ul>
|
||||
<li *ngFor="let method of methods$ | async">
|
||||
{{ method.name }}
|
||||
({{ method.cost | currency : "EUR" }}) - Lieferzeit:
|
||||
{{ method.minDeliveryDays }}-{{ method.maxDeliveryDays }} Tage - Aktiv:
|
||||
{{ method.isActive }}
|
||||
<button (click)="selectMethod(method)">Bearbeiten</button>
|
||||
<button (click)="onDelete(method.id)">Löschen</button>
|
||||
<li *ngFor="let method of methods$ | async" style="margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">
|
||||
<strong>{{ method.name }}</strong> ({{ method.cost | currency : "EUR" }}) <br/>
|
||||
|
||||
<!-- Anzeige der Details -->
|
||||
<small>
|
||||
Lieferzeit: {{ method.minDeliveryDays }}-{{ method.maxDeliveryDays }} Tage |
|
||||
<!-- NEU: Gewichtsanzeige -->
|
||||
Gewicht: {{ method.minWeight }}kg - {{ method.maxWeight }}kg |
|
||||
Aktiv: {{ method.isActive ? 'Ja' : 'Nein' }}
|
||||
</small>
|
||||
|
||||
<div style="margin-top: 5px;">
|
||||
<button (click)="selectMethod(method)">Bearbeiten</button>
|
||||
<button (click)="onDelete(method.id)">Löschen</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -26,7 +26,10 @@ export class ShippingMethodListComponent implements OnInit {
|
||||
cost: [0, [Validators.required, Validators.min(0)]],
|
||||
isActive: [true],
|
||||
minDeliveryDays: [1, [Validators.required, Validators.min(0)]],
|
||||
maxDeliveryDays: [3, [Validators.required, Validators.min(0)]]
|
||||
maxDeliveryDays: [3, [Validators.required, Validators.min(0)]],
|
||||
// NEU: Validierung für Gewicht
|
||||
minWeight: [0, [Validators.required, Validators.min(0)]],
|
||||
maxWeight: [10, [Validators.required, Validators.min(0)]]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,15 +49,16 @@ export class ShippingMethodListComponent implements OnInit {
|
||||
cost: 0,
|
||||
isActive: true,
|
||||
minDeliveryDays: 1,
|
||||
maxDeliveryDays: 3
|
||||
maxDeliveryDays: 3,
|
||||
// NEU: Reset Werte
|
||||
minWeight: 0,
|
||||
maxWeight: 10
|
||||
});
|
||||
}
|
||||
|
||||
// --- KORREKTUR: onSubmit sendet jetzt direkt das Formularwert-Objekt als JSON ---
|
||||
onSubmit(): void {
|
||||
if (this.methodForm.invalid) return;
|
||||
|
||||
// Das Formular-Objekt hat bereits die richtige Struktur, die das Backend erwartet.
|
||||
const dataToSend: ShippingMethod = {
|
||||
id: this.selectedMethodId || '00000000-0000-0000-0000-000000000000',
|
||||
...this.methodForm.value
|
||||
@@ -66,7 +70,6 @@ export class ShippingMethodListComponent implements OnInit {
|
||||
this.shippingMethodService.create(dataToSend).subscribe(() => this.reset());
|
||||
}
|
||||
}
|
||||
// --- ENDE KORREKTUR ---
|
||||
|
||||
onDelete(id: string): void {
|
||||
if (confirm('Versandmethode wirklich löschen?')) {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
|
||||
<ng-container *ngSwitchCase="'image-text'">
|
||||
<div class="user-cell">
|
||||
<img [src]="getProperty(item, col.imageKey!) || 'https://via.placeholder.com/40'"
|
||||
[alt]="'Bild von ' + getProperty(item, col.key)" />
|
||||
<img [src]="getProperty(item, col.imageKey!) || fallbackImage"
|
||||
alt="{{ item.name }}" />
|
||||
<div>
|
||||
<div class="user-name">{{ getProperty(item, col.key) }}</div>
|
||||
<div class="user-email">{{ getProperty(item, col.subKey!) }}</div>
|
||||
@@ -45,8 +45,8 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'image'">
|
||||
<img [src]="getProperty(item, col.key) || 'https://via.placeholder.com/50'"
|
||||
alt="Bild"
|
||||
<img [src]="getProperty(item, col.key) || fallbackImage"
|
||||
alt="{{ item.name }}"
|
||||
style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class GenericTableComponent implements OnChanges, OnInit {
|
||||
|
||||
public displayedData: any[] = [];
|
||||
public currentPage = 1;
|
||||
public readonly fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNjY2NjY2MiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxjaXJjbGUgY3g9IjkiIGN5PSI5IiByPSIyIi8+PHBhdGggZD0ibTIxIDE1LTUtNWwtNSA1bC0yLTJsLTUgNSIvPjwvc3ZnPg==';
|
||||
|
||||
ngOnInit(): void { this.updatePagination(); }
|
||||
ngOnChanges(changes: SimpleChanges): void { if (changes['data']) { this.currentPage = 1; this.updatePagination(); } }
|
||||
@@ -47,6 +48,7 @@ export class GenericTableComponent implements OnChanges, OnInit {
|
||||
}
|
||||
|
||||
getProperty(item: any, key: string): any {
|
||||
|
||||
if (!key) return '';
|
||||
return key.split('.').reduce((obj, part) => obj && obj[part], item);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
iconName="chevron_backward"
|
||||
(click)="goToPrevious()"
|
||||
[disabled]="currentPage === 1"
|
||||
tooltip="Vorherige Seite">
|
||||
>
|
||||
|
||||
</app-button>
|
||||
<app-button
|
||||
@@ -16,7 +16,7 @@
|
||||
iconName="chevron_forward"
|
||||
(click)="goToNext()"
|
||||
[disabled]="currentPage === totalPages"
|
||||
tooltip="Nächste Seite">
|
||||
>
|
||||
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
@@ -135,11 +135,20 @@
|
||||
transform: translateX(-50%) translateY(-12px);
|
||||
}
|
||||
|
||||
|
||||
.btn.is-loading {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
}
|
||||
.btn-content span {
|
||||
display: flex;
|
||||
height: auto;
|
||||
align-content: center;
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
|
||||
.btn-content.is-hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user