Compare commits

...

2 Commits

Author SHA1 Message Date
Tizian.Breuch
dfe631edf6 shipping methods 2025-11-28 11:18:42 +01:00
Tizian.Breuch
ac42f8b1b9 main image set 2025-11-26 08:26:28 +01:00
15 changed files with 140 additions and 62 deletions

View File

@@ -6,4 +6,7 @@ export interface ShippingMethod {
isActive: boolean; isActive: boolean;
minDeliveryDays: number; minDeliveryDays: number;
maxDeliveryDays: number; maxDeliveryDays: number;
// NEU: Gewichtsgrenzen
minWeight: number;
maxWeight: number;
} }

View File

@@ -1,10 +1,8 @@
.form-header { .page-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-bottom: 1rem; margin-bottom: 1rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
} }
h3[card-header] { h3[card-header] {

View File

@@ -4,7 +4,7 @@
</div> </div>
<ng-template #formContent> <ng-template #formContent>
<div class="form-header"> <div class="page-header">
<h3 card-header>Neues Produkt erstellen</h3> <h3 card-header>Neues Produkt erstellen</h3>
</div> </div>

View File

@@ -1,10 +1,8 @@
.form-header { .page-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-bottom: 1rem; margin-bottom: 1rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
} }
h3[card-header] { h3[card-header] {

View File

@@ -5,7 +5,7 @@
</div> </div>
<ng-template #formContent> <ng-template #formContent>
<div class="form-header"> <div class="page-header">
<h3 card-header>Produkt bearbeiten</h3> <h3 card-header>Produkt bearbeiten</h3>
</div> </div>
<app-product-form <app-product-form

View File

@@ -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; return formData;
} }

View File

@@ -8,6 +8,14 @@
gap: 1rem; gap: 1rem;
} }
.page-header {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
}
app-search-bar { app-search-bar {
flex-grow: 1; flex-grow: 1;
max-width: 400px; max-width: 400px;
@@ -20,7 +28,9 @@ app-search-bar {
} }
.column-filter-container { .column-filter-container {
display: flex;
position: relative; position: relative;
gap: 1.5rem;
} }
.column-filter-dropdown { .column-filter-dropdown {
@@ -52,4 +62,4 @@ app-search-bar {
.column-filter-dropdown label:hover { .column-filter-dropdown label:hover {
background-color: var(--color-body-bg-hover); background-color: var(--color-body-bg-hover);
} }

View File

@@ -1,20 +1,34 @@
<!-- /src/app/features/admin/components/products/product-list/product-list.component.html --> <!-- /src/app/features/admin/components/products/product-list/product-list.component.html -->
<div class="page-container"> <div class="page-container">
<div class="header"> <div>
<h1 class="page-title">Produktübersicht</h1> <h3 class="page-header">Produktübersicht</h3>
<app-button buttonType="primary" (click)="onAddNew()">
<app-icon iconName="plus"></app-icon> Neues Produkt
</app-button>
</div> </div>
<div class="table-header"> <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"> <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"> <div class="column-filter-dropdown" *ngIf="isColumnFilterVisible">
<label *ngFor="let col of allTableColumns"> <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 }} {{ col.title }}
</label> </label>
</div> </div>
@@ -25,6 +39,7 @@
[data]="filteredProducts" [data]="filteredProducts"
[columns]="visibleTableColumns" [columns]="visibleTableColumns"
(edit)="onEditProduct($event.id)" (edit)="onEditProduct($event.id)"
(delete)="onDeleteProduct($event.id)"> (delete)="onDeleteProduct($event.id)"
>
</app-generic-table> </app-generic-table>
</div> </div>

View File

@@ -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 { GenericTableComponent, ColumnConfig } from '../../../../shared/components/data-display/generic-table/generic-table.component';
import { SearchBarComponent } from '../../../../shared/components/layout/search-bar/search-bar.component'; import { SearchBarComponent } from '../../../../shared/components/layout/search-bar/search-bar.component';
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
@Component({ @Component({
selector: 'app-product-list', selector: 'app-product-list',
standalone: true, standalone: true,
imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent], imports: [CommonModule, GenericTableComponent, SearchBarComponent, ButtonComponent],
providers: [DatePipe], providers: [DatePipe],
templateUrl: './product-list.component.html', templateUrl: './product-list.component.html',
styleUrl: './product-list.component.css' styleUrl: './product-list.component.css'
@@ -59,6 +58,8 @@ export class ProductListComponent implements OnInit {
]; ];
visibleTableColumns: ColumnConfig[] = []; visibleTableColumns: ColumnConfig[] = [];
public readonly fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNjY2NjY2MiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxjaXJjbGUgY3g9IjkiIGN5PSI5IiByPSIyIi8+PHBhdGggZD0ibTIxIDE1LTUtNWwtNSA1bC0yLTJsLTUgNSIvPjwvc3ZnPg==';
constructor() { constructor() {
this.loadTableSettings(); this.loadTableSettings();
} }
@@ -144,8 +145,8 @@ export class ProductListComponent implements OnInit {
} }
getMainImageUrl(images?: ProductImage[]): string { 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); 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;
} }
} }

View File

@@ -22,27 +22,51 @@
<input type="number" formControlName="cost" placeholder="Kosten" /> <input type="number" formControlName="cost" placeholder="Kosten" />
</div> </div>
<!-- +++ NEUE FELDER HINZUGEFÜGT +++ --> <div style="display: flex; gap: 10px;">
<div> <div>
<label>Minimale Liefertage:</label> <label>Minimale Liefertage:</label>
<input <input
type="number" type="number"
formControlName="minDeliveryDays" formControlName="minDeliveryDays"
placeholder="z.B. 1" placeholder="z.B. 1"
/> />
</div>
<div>
<label>Maximale Liefertage:</label>
<input
type="number"
formControlName="maxDeliveryDays"
placeholder="z.B. 3"
/>
</div>
</div> </div>
<div> <!-- +++ NEUE GEWICHTS FELDER +++ -->
<label>Maximale Liefertage:</label> <div style="display: flex; gap: 10px; margin-top: 10px;">
<input <div>
type="number" <label>Gewicht von (kg):</label>
formControlName="maxDeliveryDays" <input
placeholder="z.B. 3" 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> </div>
<!-- +++ ENDE NEU +++ --> <!-- +++ ENDE NEU +++ -->
<div> <div style="margin-top: 10px;">
<label><input type="checkbox" formControlName="isActive" /> Aktiv</label> <label><input type="checkbox" formControlName="isActive" /> Aktiv</label>
</div> </div>
@@ -55,16 +79,26 @@
Abbrechen Abbrechen
</button> </button>
</form> </form>
<hr /> <hr />
<h2>Bestehende Methoden</h2> <h2>Bestehende Methoden</h2>
<ul> <ul>
<li *ngFor="let method of methods$ | async"> <li *ngFor="let method of methods$ | async" style="margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">
{{ method.name }} <strong>{{ method.name }}</strong> ({{ method.cost | currency : "EUR" }}) <br/>
({{ method.cost | currency : "EUR" }}) - Lieferzeit:
{{ method.minDeliveryDays }}-{{ method.maxDeliveryDays }} Tage - Aktiv: <!-- Anzeige der Details -->
{{ method.isActive }} <small>
<button (click)="selectMethod(method)">Bearbeiten</button> Lieferzeit: {{ method.minDeliveryDays }}-{{ method.maxDeliveryDays }} Tage |
<button (click)="onDelete(method.id)">Löschen</button> <!-- 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> </li>
</ul> </ul>
</div> </div>

View File

@@ -26,7 +26,10 @@ export class ShippingMethodListComponent implements OnInit {
cost: [0, [Validators.required, Validators.min(0)]], cost: [0, [Validators.required, Validators.min(0)]],
isActive: [true], isActive: [true],
minDeliveryDays: [1, [Validators.required, Validators.min(0)]], 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, cost: 0,
isActive: true, isActive: true,
minDeliveryDays: 1, 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 { onSubmit(): void {
if (this.methodForm.invalid) return; if (this.methodForm.invalid) return;
// Das Formular-Objekt hat bereits die richtige Struktur, die das Backend erwartet.
const dataToSend: ShippingMethod = { const dataToSend: ShippingMethod = {
id: this.selectedMethodId || '00000000-0000-0000-0000-000000000000', id: this.selectedMethodId || '00000000-0000-0000-0000-000000000000',
...this.methodForm.value ...this.methodForm.value
@@ -66,7 +70,6 @@ export class ShippingMethodListComponent implements OnInit {
this.shippingMethodService.create(dataToSend).subscribe(() => this.reset()); this.shippingMethodService.create(dataToSend).subscribe(() => this.reset());
} }
} }
// --- ENDE KORREKTUR ---
onDelete(id: string): void { onDelete(id: string): void {
if (confirm('Versandmethode wirklich löschen?')) { if (confirm('Versandmethode wirklich löschen?')) {

View File

@@ -35,8 +35,8 @@
<ng-container *ngSwitchCase="'image-text'"> <ng-container *ngSwitchCase="'image-text'">
<div class="user-cell"> <div class="user-cell">
<img [src]="getProperty(item, col.imageKey!) || 'https://via.placeholder.com/40'" <img [src]="getProperty(item, col.imageKey!) || fallbackImage"
[alt]="'Bild von ' + getProperty(item, col.key)" /> alt="{{ item.name }}" />
<div> <div>
<div class="user-name">{{ getProperty(item, col.key) }}</div> <div class="user-name">{{ getProperty(item, col.key) }}</div>
<div class="user-email">{{ getProperty(item, col.subKey!) }}</div> <div class="user-email">{{ getProperty(item, col.subKey!) }}</div>
@@ -45,8 +45,8 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'image'"> <ng-container *ngSwitchCase="'image'">
<img [src]="getProperty(item, col.key) || 'https://via.placeholder.com/50'" <img [src]="getProperty(item, col.key) || fallbackImage"
alt="Bild" alt="{{ item.name }}"
style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;"> style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
</ng-container> </ng-container>

View File

@@ -35,6 +35,7 @@ export class GenericTableComponent implements OnChanges, OnInit {
public displayedData: any[] = []; public displayedData: any[] = [];
public currentPage = 1; public currentPage = 1;
public readonly fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNjY2NjY2MiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxjaXJjbGUgY3g9IjkiIGN5PSI5IiByPSIyIi8+PHBhdGggZD0ibTIxIDE1LTUtNWwtNSA1bC0yLTJsLTUgNSIvPjwvc3ZnPg==';
ngOnInit(): void { this.updatePagination(); } ngOnInit(): void { this.updatePagination(); }
ngOnChanges(changes: SimpleChanges): void { if (changes['data']) { this.currentPage = 1; 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 { getProperty(item: any, key: string): any {
if (!key) return ''; if (!key) return '';
return key.split('.').reduce((obj, part) => obj && obj[part], item); return key.split('.').reduce((obj, part) => obj && obj[part], item);
} }

View File

@@ -8,7 +8,7 @@
iconName="chevron_backward" iconName="chevron_backward"
(click)="goToPrevious()" (click)="goToPrevious()"
[disabled]="currentPage === 1" [disabled]="currentPage === 1"
tooltip="Vorherige Seite"> >
</app-button> </app-button>
<app-button <app-button
@@ -16,7 +16,7 @@
iconName="chevron_forward" iconName="chevron_forward"
(click)="goToNext()" (click)="goToNext()"
[disabled]="currentPage === totalPages" [disabled]="currentPage === totalPages"
tooltip="Nächste Seite"> >
</app-button> </app-button>
</div> </div>

View File

@@ -135,11 +135,20 @@
transform: translateX(-50%) translateY(-12px); transform: translateX(-50%) translateY(-12px);
} }
.btn.is-loading { .btn.is-loading {
cursor: wait; cursor: wait;
} }
.btn-content {
display: flex;
}
.btn-content span {
display: flex;
height: auto;
align-content: center;
flex-wrap: wrap-reverse;
}
.btn-content.is-hidden { .btn-content.is-hidden {
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;