From 7511596b11d66a31cfaf72b19fb61a3322c98221 Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Thu, 6 Nov 2025 16:23:39 +0100 Subject: [PATCH] good i guess --- .../product-category-dropdown.component.css | 114 +++++++++++ .../product-category-dropdown.component.html | 40 ++++ .../product-category-dropdown.component.ts | 81 ++++++++ .../product-create.component.ts | 10 +- .../product-edit/product-edit.component.ts | 187 +++++++++++------- .../product-form/product-form.component.html | 28 ++- .../product-form/product-form.component.ts | 110 ++++------- .../product-list/product-list.component.ts | 2 +- .../ui/status-pill/status-pill.component.ts | 3 +- 9 files changed, 402 insertions(+), 173 deletions(-) create mode 100644 src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.css create mode 100644 src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.html create mode 100644 src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.ts diff --git a/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.css b/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.css new file mode 100644 index 0000000..6227400 --- /dev/null +++ b/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.css @@ -0,0 +1,114 @@ +/* /src/app/shared/components/form/multi-select-dropdown/multi-select-dropdown.component.css */ + +:host { + display: block; +} + +.custom-select-wrapper { + position: relative; +} + +.select-display { + width: 100%; + text-align: left; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 2.5rem 0.5rem 1rem; + min-height: 54px; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + background-color: var(--color-surface); + cursor: pointer; + transition: border-color var(--transition-speed); +} + +.select-display:focus { + outline: none; + border-color: var(--color-primary); +} + +.selected-pills { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.placeholder-text { + color: var(--color-text-light); +} + +.form-label { + position: absolute; + top: 50%; + left: 1rem; + transform: translateY(-50%); + color: var(--color-text-light); + background-color: var(--color-surface); + padding: 0 0.25rem; + transition: all 0.2s ease-out; + pointer-events: none; +} + +.select-display:focus ~ .form-label, +.form-label.has-value { + top: 0; + font-size: 0.8rem; + color: var(--color-primary); +} + +.select-display::after { + content: ""; + position: absolute; + top: 50%; + right: 1rem; + transform: translateY(-50%); + width: 0.8em; + height: 0.5em; + background-color: var(--color-text-light); + clip-path: polygon(100% 0%, 0 0%, 50% 100%); + transition: transform 0.2s ease-out; +} + +.custom-select-wrapper.is-open .select-display::after { + transform: translateY(-50%) rotate(180deg); +} + +.options-list { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + box-shadow: var(--box-shadow-md); + z-index: 1000; + padding: 0.5rem; + animation: fadeInDown 0.2s ease-out forwards; +} + +.category-checkbox-group { + max-height: 220px; + overflow-y: auto; +} + +.category-checkbox-group label { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: background-color 0.15s ease-out; +} + +.category-checkbox-group label:hover { + background-color: var(--color-body-bg-hover); +} + +@keyframes fadeInDown { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} \ No newline at end of file diff --git a/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.html b/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.html new file mode 100644 index 0000000..16e98f4 --- /dev/null +++ b/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.html @@ -0,0 +1,40 @@ + + +
+ + + + + + + +
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.ts b/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.ts new file mode 100644 index 0000000..d47fbd7 --- /dev/null +++ b/src/app/features/components/products/product-category-dropdown/product-category-dropdown.component.ts @@ -0,0 +1,81 @@ +// /src/app/shared/components/form/product-category-dropdown/product-category-dropdown.component.ts + +import { Component, Input, forwardRef, HostListener, ElementRef, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { StatusPillComponent } from '../../../../shared/components/ui/status-pill/status-pill.component'; + +// Eine wiederverwendbare Schnittstelle für die Optionen +export interface SelectOption { + value: any; + label: string; +} + +@Component({ + selector: 'app-product-category-dropdown', + standalone: true, + imports: [CommonModule, StatusPillComponent], + templateUrl: './product-category-dropdown.component.html', + styleUrls: ['./product-category-dropdown.component.css'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ProductCategoryDropdownComponent), + multi: true, + }, + ], +}) +export class ProductCategoryDropdownComponent implements ControlValueAccessor { + @Input() label = ''; + @Input() options: SelectOption[] = []; + @Input() placeholder = 'Bitte wählen...'; + + public isOpen = false; + public selectedValues: any[] = []; + private elementRef = inject(ElementRef); + + onChange: (value: any[]) => void = () => {}; + onTouched: () => void = () => {}; + + writeValue(values: any[]): void { this.selectedValues = Array.isArray(values) ? values : []; } + registerOnChange(fn: any): void { this.onChange = fn; } + registerOnTouched(fn: any): void { this.onTouched = fn; } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: Event): void { + if (this.isOpen && !this.elementRef.nativeElement.contains(event.target)) { + this.closeDropdown(); + } + } + + toggleDropdown(event: Event): void { + event.stopPropagation(); + this.isOpen = !this.isOpen; + if (!this.isOpen) { this.onTouched(); } + } + + closeDropdown(): void { + if (this.isOpen) { + this.isOpen = false; + this.onTouched(); + } + } + + onOptionToggle(value: any): void { + const index = this.selectedValues.indexOf(value); + if (index > -1) { + this.selectedValues.splice(index, 1); + } else { + this.selectedValues.push(value); + } + this.onChange([...this.selectedValues]); // Wichtig: Neue Array-Instanz senden + } + + onPillRemove(value: any, event: any): void { // Typ korrigiert + event.stopPropagation(); + this.onOptionToggle(value); + } + + isSelected(value: any): boolean { return this.selectedValues.includes(value); } + getLabelForValue(value: any): string { return this.options.find(opt => opt.value === value)?.label || ''; } +} \ No newline at end of file diff --git a/src/app/features/components/products/product-create/product-create.component.ts b/src/app/features/components/products/product-create/product-create.component.ts index 1ee3554..4d84de1 100644 --- a/src/app/features/components/products/product-create/product-create.component.ts +++ b/src/app/features/components/products/product-create/product-create.component.ts @@ -9,6 +9,7 @@ import { FormArray, ReactiveFormsModule, Validators, + FormControl, } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { Observable, Subscription } from 'rxjs'; @@ -25,7 +26,6 @@ import { ProductService } from '../../../services/product.service'; import { CategoryService } from '../../../services/category.service'; import { SupplierService } from '../../../services/supplier.service'; import { SnackbarService } from '../../../../shared/services/snackbar.service'; -import { CardComponent } from '../../../../shared/components/ui/card/card.component'; import { ImagePreview, ProductFormComponent, @@ -38,7 +38,7 @@ import { SelectOption } from '../../../../shared/components/form/form-select/for imports: [ CommonModule, ReactiveFormsModule, - CardComponent, + ProductFormComponent, ], templateUrl: './product-create.component.html', @@ -81,7 +81,7 @@ export class ProductCreateComponent implements OnInit, OnDestroy { isFeatured: [false], featuredDisplayOrder: [0], supplierId: [null], - categorieIds: this.fb.array([]), + categorieIds: new FormControl([]), }); } @@ -125,7 +125,7 @@ export class ProductCreateComponent implements OnInit, OnDestroy { this.productService.create(formData).subscribe({ next: () => { this.snackbarService.show('Produkt erfolgreich erstellt'); - this.router.navigate(['/admin/products']); + this.router.navigate(['/shop/products']); }, error: (err) => { this.snackbarService.show('Ein Fehler ist aufgetreten.'); @@ -135,7 +135,7 @@ export class ProductCreateComponent implements OnInit, OnDestroy { } cancel(): void { - this.router.navigate(['/admin/products']); + this.router.navigate(['/shop/products']); } onFilesSelected(files: File[]): void { diff --git a/src/app/features/components/products/product-edit/product-edit.component.ts b/src/app/features/components/products/product-edit/product-edit.component.ts index 32306e6..e35e218 100644 --- a/src/app/features/components/products/product-edit/product-edit.component.ts +++ b/src/app/features/components/products/product-edit/product-edit.component.ts @@ -3,26 +3,45 @@ import { Component, OnInit, OnDestroy, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; -import { FormBuilder, FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + FormBuilder, + FormGroup, + FormArray, + FormControl, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { DomSanitizer } from '@angular/platform-browser'; import { Observable, Subscription } from 'rxjs'; -import { map, startWith, debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators'; +import { + map, + startWith, + debounceTime, + distinctUntilChanged, + finalize, +} from 'rxjs/operators'; // Models, Services und UI-Komponenten -import { AdminProduct, ProductImage } from '../../../../core/models/product.model'; +import { + AdminProduct, + ProductImage, +} from '../../../../core/models/product.model'; import { Category } from '../../../../core/models/category.model'; import { ProductService } from '../../../services/product.service'; import { CategoryService } from '../../../services/category.service'; import { SupplierService } from '../../../services/supplier.service'; import { SnackbarService } from '../../../../shared/services/snackbar.service'; import { CardComponent } from '../../../../shared/components/ui/card/card.component'; -import { ImagePreview, ProductFormComponent } from '../product-form/product-form.component'; +import { + ImagePreview, + ProductFormComponent, +} from '../product-form/product-form.component'; import { SelectOption } from '../../../../shared/components/form/form-select/form-select.component'; @Component({ selector: 'app-product-edit', standalone: true, - imports: [ CommonModule, ReactiveFormsModule, CardComponent, ProductFormComponent ], + imports: [CommonModule, ReactiveFormsModule, ProductFormComponent], templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'], }) @@ -49,7 +68,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { existingImages: ProductImage[] = []; newImageFiles: File[] = []; mainImageIdentifier: string | null = null; // Hält die ID oder den Dateinamen des Hauptbildes - allImagesForForm: ImagePreview[] = []; // Die kombinierte Liste für die Child-Komponente + allImagesForForm: ImagePreview[] = []; // Die kombinierte Liste für die Child-Komponente constructor() { this.productForm = this.fb.group({ @@ -67,7 +86,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { isFeatured: [false], featuredDisplayOrder: [0], supplierId: [null], - categorieIds: this.fb.array([]), + categorieIds: new FormControl([]), imagesToDelete: this.fb.array([]), }); } @@ -80,7 +99,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { const id = this.route.snapshot.paramMap.get('id'); if (!id) { this.snackbarService.show('Produkt-ID fehlt.'); - this.router.navigate(['/admin/products']); + this.router.navigate(['/shop/products']); return; } this.productId = id; @@ -91,8 +110,9 @@ export class ProductEditComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.nameChangeSubscription?.unsubscribe(); - this.allImagesForForm.forEach(image => { - if (typeof image.url === 'object') { // Nur Object-URLs von neuen Bildern freigeben + this.allImagesForForm.forEach((image) => { + if (typeof image.url === 'object') { + // Nur Object-URLs von neuen Bildern freigeben URL.revokeObjectURL(image.url as string); } }); @@ -101,44 +121,55 @@ export class ProductEditComponent implements OnInit, OnDestroy { loadDropdownData(): void { this.allCategories$ = this.categoryService.getAll(); this.supplierOptions$ = this.supplierService.getAll().pipe( - map(suppliers => suppliers.map(s => ({ value: s.id, label: s.name || 'Unbenannt' }))), + map((suppliers) => + suppliers.map((s) => ({ value: s.id, label: s.name || 'Unbenannt' })) + ), startWith([]) ); } loadProductData(): void { this.isLoading = true; - this.productService.getById(this.productId).pipe( - finalize(() => { this.isLoading = false; }) - ).subscribe({ - next: (product) => { - if (product) { - this.populateForm(product); - } else { - this.snackbarService.show('Produkt nicht gefunden.'); - this.router.navigate(['/admin/products']); - } - }, - error: (err) => { - this.snackbarService.show('Fehler beim Laden des Produkts.'); - console.error('Fehler beim Laden der Produktdaten:', err); - this.router.navigate(['/admin/products']); - }, - }); + this.productService + .getById(this.productId) + .pipe( + finalize(() => { + this.isLoading = false; + }) + ) + .subscribe({ + next: (product) => { + if (product) { + this.populateForm(product); + } else { + this.snackbarService.show('Produkt nicht gefunden.'); + this.router.navigate(['/shop/products']); + } + }, + error: (err) => { + this.snackbarService.show('Fehler beim Laden des Produkts.'); + console.error('Fehler beim Laden der Produktdaten:', err); + this.router.navigate(['/shop/products']); + }, + }); } - populateForm(product: AdminProduct): void { - this.productForm.patchValue(product); - const categories = this.productForm.get('categorieIds') as FormArray; - categories.clear(); - product.categorieIds?.forEach(id => categories.push(new FormControl(id))); +// AFTER (CORRECT) +populateForm(product: AdminProduct): void { + // This single line handles ALL form controls, including setting + // the 'categorieIds' FormControl to the array from the product object. + this.productForm.patchValue(product); - this.existingImages = product.images || []; - const mainImage = this.existingImages.find(img => img.isMainImage); - this.mainImageIdentifier = mainImage?.id || (this.existingImages.length > 0 ? this.existingImages[0].id : null); - - this.rebuildAllImagesForForm(); - } + // The obsolete FormArray logic has been removed. + + this.existingImages = product.images || []; + const mainImage = this.existingImages.find((img) => img.isMainImage); + this.mainImageIdentifier = + mainImage?.id || + (this.existingImages.length > 0 ? this.existingImages[0].id : null); + + this.rebuildAllImagesForForm(); +} onSubmit(): void { if (this.productForm.invalid) { @@ -151,7 +182,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { this.productService.update(this.productId, formData).subscribe({ next: () => { this.snackbarService.show('Produkt erfolgreich aktualisiert'); - this.router.navigate(['/admin/products']); + this.router.navigate(['/shop/products']); }, error: (err) => { this.snackbarService.show('Ein Fehler ist aufgetreten.'); @@ -160,7 +191,9 @@ export class ProductEditComponent implements OnInit, OnDestroy { }); } - cancel(): void { this.router.navigate(['/admin/products']); } + cancel(): void { + this.router.navigate(['/shop/products']); + } // --- NEUE EVENT-HANDLER FÜR BILDER --- onFilesSelected(files: File[]): void { @@ -177,17 +210,23 @@ export class ProductEditComponent implements OnInit, OnDestroy { } onDeleteImage(identifier: string): void { - const isExisting = this.existingImages.some(img => img.id === identifier); + const isExisting = this.existingImages.some((img) => img.id === identifier); if (isExisting) { this.imagesToDelete.push(new FormControl(identifier)); - this.existingImages = this.existingImages.filter(img => img.id !== identifier); + this.existingImages = this.existingImages.filter( + (img) => img.id !== identifier + ); } else { - this.newImageFiles = this.newImageFiles.filter(file => file.name !== identifier); + this.newImageFiles = this.newImageFiles.filter( + (file) => file.name !== identifier + ); } if (this.mainImageIdentifier === identifier) { - const firstExisting = this.existingImages.length > 0 ? this.existingImages[0].id : null; - const firstNew = this.newImageFiles.length > 0 ? this.newImageFiles[0].name : null; + const firstExisting = + this.existingImages.length > 0 ? this.existingImages[0].id : null; + const firstNew = + this.newImageFiles.length > 0 ? this.newImageFiles[0].name : null; this.mainImageIdentifier = firstExisting || firstNew; } this.rebuildAllImagesForForm(); @@ -195,19 +234,28 @@ export class ProductEditComponent implements OnInit, OnDestroy { // --- Private Helfermethoden --- private rebuildAllImagesForForm(): void { - this.allImagesForForm.forEach(image => { - if (typeof image.url === 'object') URL.revokeObjectURL(image.url as string); + this.allImagesForForm.forEach((image) => { + if (typeof image.url === 'object') + URL.revokeObjectURL(image.url as string); }); const combined: ImagePreview[] = []; this.existingImages - .filter(img => !!img.url) // Stellt sicher, dass nur Bilder mit URL verwendet werden - .forEach(img => { - combined.push({ identifier: img.id, url: img.url!, isMainImage: img.id === this.mainImageIdentifier }); + .filter((img) => !!img.url) // Stellt sicher, dass nur Bilder mit URL verwendet werden + .forEach((img) => { + combined.push({ + identifier: img.id, + url: img.url!, + isMainImage: img.id === this.mainImageIdentifier, + }); + }); + + this.newImageFiles.forEach((file) => { + combined.push({ + identifier: file.name, + url: this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(file)), + isMainImage: file.name === this.mainImageIdentifier, }); - - this.newImageFiles.forEach(file => { - combined.push({ identifier: file.name, url: this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(file)), isMainImage: file.name === this.mainImageIdentifier }); }); this.allImagesForForm = combined; @@ -220,27 +268,30 @@ export class ProductEditComponent implements OnInit, OnDestroy { Object.keys(formValue).forEach((key) => { const value = formValue[key]; if (key === 'categorieIds' || key === 'imagesToDelete') { - (value as string[]).forEach((id) => formData.append(this.capitalizeFirstLetter(key), id)); + (value as string[]).forEach((id) => + formData.append(this.capitalizeFirstLetter(key), id) + ); } else if (value !== null && value !== undefined && value !== '') { formData.append(this.capitalizeFirstLetter(key), value.toString()); } }); - const mainImageFile = this.newImageFiles.find(f => f.name === this.mainImageIdentifier); + const mainImageFile = this.newImageFiles.find( + (f) => f.name === this.mainImageIdentifier + ); if (mainImageFile) { formData.append('MainImageFile', mainImageFile); } else if (this.mainImageIdentifier) { formData.append('MainImageId', this.mainImageIdentifier); } - + this.newImageFiles - .filter(f => f.name !== this.mainImageIdentifier) - .forEach(file => formData.append('AdditionalImageFiles', file)); - + .filter((f) => f.name !== this.mainImageIdentifier) + .forEach((file) => formData.append('AdditionalImageFiles', file)); + return formData; } - // Private Helfermethoden private prepareSubmissionData(): void { const nameControl = this.productForm.get('name'); @@ -259,7 +310,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { } } - private subscribeToNameChanges(): void { + private subscribeToNameChanges(): void { const nameControl = this.productForm.get('name'); const slugControl = this.productForm.get('slug'); @@ -274,7 +325,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { } } - private generateSlug(name: string): string { + private generateSlug(name: string): string { return name .toLowerCase() .replace(/\s+/g, '-') @@ -286,7 +337,7 @@ export class ProductEditComponent implements OnInit, OnDestroy { .replace(/-+/g, '-'); } - private generateSkuValue(name: string): string { + private generateSkuValue(name: string): string { const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, ''); const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase(); return `${prefix}-${randomPart}`; @@ -295,12 +346,4 @@ export class ProductEditComponent implements OnInit, OnDestroy { private capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); } - - - - - - - - } diff --git a/src/app/features/components/products/product-form/product-form.component.html b/src/app/features/components/products/product-form/product-form.component.html index b276925..4b0c1ab 100644 --- a/src/app/features/components/products/product-form/product-form.component.html +++ b/src/app/features/components/products/product-form/product-form.component.html @@ -86,24 +86,18 @@ + + + + + -
-
- - - Keine ausgewählt -
-
- -
-
+ diff --git a/src/app/features/components/products/product-form/product-form.component.ts b/src/app/features/components/products/product-form/product-form.component.ts index 38be0ac..d08cecf 100644 --- a/src/app/features/components/products/product-form/product-form.component.ts +++ b/src/app/features/components/products/product-form/product-form.component.ts @@ -1,18 +1,12 @@ -// /src/app/features/admin/components/products/product-form/product-form.component.ts +// /src/app/features/admin/components/products/product-form/product-form.component.ts (CORRECTED) -import { Component, Input, Output, EventEmitter, inject } from '@angular/core'; +import { Component, Input, Output, EventEmitter, inject, SimpleChanges } from '@angular/core'; import { CommonModule, NgClass } from '@angular/common'; -import { - FormGroup, - FormArray, - ReactiveFormsModule, - FormControl, -} from '@angular/forms'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; // FormArray and FormControl no longer needed here import { SafeUrl } from '@angular/platform-browser'; // Models & UI Components import { Category } from '../../../../core/models/category.model'; -import { CardComponent } from '../../../../shared/components/ui/card/card.component'; import { ButtonComponent } from '../../../../shared/components/ui/button/button.component'; import { IconComponent } from '../../../../shared/components/ui/icon/icon.component'; import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component'; @@ -24,11 +18,10 @@ import { FormTextareaComponent } from '../../../../shared/components/form/form-t import { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component'; import { SnackbarService } from '../../../../shared/services/snackbar.service'; import { FormGroupComponent } from '../../../../shared/components/form/form-group/form-group.component'; -import { StatusPillComponent } from '../../../../shared/components/ui/status-pill/status-pill.component'; +import { ProductCategoryDropdownComponent } from '../product-category-dropdown/product-category-dropdown.component'; -// Interface für eine einheitliche Bild-Datenstruktur, die von der Elternkomponente kommt export interface ImagePreview { - identifier: string; // Eindeutiger Bezeichner (ID für existierende, Dateiname für neue) + identifier: string; url: string | SafeUrl; isMainImage: boolean; } @@ -37,64 +30,58 @@ export interface ImagePreview { selector: 'app-product-form', standalone: true, imports: [ - CommonModule, - NgClass, - ReactiveFormsModule, - CardComponent, - ButtonComponent, - IconComponent, - FormFieldComponent, - FormSelectComponent, - FormTextareaComponent, - SlideToggleComponent, - FormGroupComponent, - StatusPillComponent + CommonModule, NgClass, ReactiveFormsModule, ButtonComponent, IconComponent, + FormFieldComponent, FormSelectComponent, FormTextareaComponent, + SlideToggleComponent, FormGroupComponent, ProductCategoryDropdownComponent ], templateUrl: './product-form.component.html', styleUrls: ['./product-form.component.css'], }) -export class ProductFormComponent { - // --- Inputs für Daten --- +export class ProductFormComponent { + // --- Inputs --- @Input() productForm!: FormGroup; @Input() allCategories: Category[] = []; @Input() supplierOptions: SelectOption[] = []; @Input() isLoading = false; @Input() submitButtonText = 'Speichern'; - - // NEU: Empfängt eine einzige, kombinierte Bildliste von der Elternkomponente @Input() allImages: ImagePreview[] = []; - - // --- Outputs für Aktionen --- + + // --- Outputs --- @Output() formSubmit = new EventEmitter(); @Output() formCancel = new EventEmitter(); - @Output() filesSelected = new EventEmitter(); // Sendet alle neu ausgewählten Dateien - @Output() setMainImage = new EventEmitter(); // Sendet den 'identifier' des Bildes - @Output() deleteImage = new EventEmitter(); // Sendet den 'identifier' des Bildes + @Output() filesSelected = new EventEmitter(); + @Output() setMainImage = new EventEmitter(); + @Output() deleteImage = new EventEmitter(); private snackbarService = inject(SnackbarService); + public categoryOptions: SelectOption[] = []; - // GETTER FÜR DAS TEMPLATE - get categorieIds(): FormArray { - return this.productForm.get('categorieIds') as FormArray; - } + // --- GETTER --- get hasImages(): boolean { return this.allImages && this.allImages.length > 0; } - // --- EVENT-HANDLER --- - onSubmit(): void { - this.formSubmit.emit(); - } - cancel(): void { - this.formCancel.emit(); + // --- LIFECYCLE HOOKS --- + ngOnChanges(changes: SimpleChanges): void { + if (changes['allCategories']) { + this.categoryOptions = this.allCategories + .filter(cat => !!cat.name) + .map(cat => ({ + value: cat.id, + label: cat.name! + })); + } } + // --- EVENT-HANDLERS --- + onSubmit(): void { this.formSubmit.emit(); } + cancel(): void { this.formCancel.emit(); } + onFilesSelected(event: Event): void { const files = (event.target as HTMLInputElement).files; if (files && files.length > 0) { this.filesSelected.emit(Array.from(files)); } - // Wichtig: Input-Wert zurücksetzen, damit die gleichen Dateien erneut ausgewählt werden können (event.target as HTMLInputElement).value = ''; } @@ -103,11 +90,10 @@ export class ProductFormComponent { } requestImageDeletion(identifier: string, event: MouseEvent): void { - event.stopPropagation(); // Verhindert, dass gleichzeitig setAsMainImage gefeuert wird + event.stopPropagation(); this.deleteImage.emit(identifier); } - // --- Helfermethoden (unverändert) --- generateSku(): void { const name = this.productForm.get('name')?.value || 'PROD'; const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, ''); @@ -117,33 +103,5 @@ export class ProductFormComponent { this.snackbarService.show('Neue SKU generiert!'); } - onCategoryChange(event: Event): void { - const checkbox = event.target as HTMLInputElement; - const categoryId = checkbox.value; - if (checkbox.checked) { - if (!this.categorieIds.value.includes(categoryId)) { - this.categorieIds.push(new FormControl(categoryId)); - } - } else { - const index = this.categorieIds.controls.findIndex( - (x) => x.value === categoryId - ); - if (index !== -1) this.categorieIds.removeAt(index); - } - } - - isCategorySelected(categoryId: string): boolean { - return this.categorieIds.value.includes(categoryId); - } - - getCategoryName(categoryId: string): string { - return this.allCategories.find((c) => c.id === categoryId)?.name || ''; - } - - removeCategoryById(categoryId: string): void { - const index = this.categorieIds.controls.findIndex( - (x) => x.value === categoryId - ); - if (index !== -1) this.categorieIds.removeAt(index); - } -} + // ALL OBSOLETE CATEGORY HELPER FUNCTIONS HAVE BEEN REMOVED +} \ No newline at end of file diff --git a/src/app/features/components/products/product-list/product-list.component.ts b/src/app/features/components/products/product-list/product-list.component.ts index 1a30056..ab600e2 100644 --- a/src/app/features/components/products/product-list/product-list.component.ts +++ b/src/app/features/components/products/product-list/product-list.component.ts @@ -16,7 +16,7 @@ import { IconComponent } from '../../../../shared/components/ui/icon/icon.compon @Component({ selector: 'app-product-list', standalone: true, - imports: [CommonModule, CurrencyPipe, DatePipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent], + imports: [CommonModule, CurrencyPipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent], providers: [DatePipe], templateUrl: './product-list.component.html', styleUrl: './product-list.component.css' diff --git a/src/app/shared/components/ui/status-pill/status-pill.component.ts b/src/app/shared/components/ui/status-pill/status-pill.component.ts index f78888b..0109a57 100644 --- a/src/app/shared/components/ui/status-pill/status-pill.component.ts +++ b/src/app/shared/components/ui/status-pill/status-pill.component.ts @@ -2,12 +2,11 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule, NgClass } from '@angular/common'; -import { IconComponent } from '../icon/icon.component'; // IconComponent importieren @Component({ selector: 'app-status-pill', standalone: true, - imports: [CommonModule, NgClass, IconComponent], // IconComponent hinzufügen + imports: [CommonModule, NgClass], // IconComponent hinzufügen templateUrl: './status-pill.component.html', styleUrls: ['./status-pill.component.css'] })