good i guess

This commit is contained in:
Tizian.Breuch
2025-11-06 16:23:39 +01:00
parent 8df2420aa0
commit 7511596b11
9 changed files with 402 additions and 173 deletions

View File

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

View File

@@ -0,0 +1,40 @@
<!-- /src/app/shared/components/form/multi-select-dropdown/multi-select-dropdown.component.html -->
<div class="custom-select-wrapper" [class.is-open]="isOpen">
<!-- Der klickbare Bereich, der die Pills anzeigt -->
<button
type="button"
class="form-input select-display"
(click)="toggleDropdown($event)">
<div class="selected-pills">
<app-status-pill
*ngFor="let value of selectedValues"
[text]="getLabelForValue(value)"
status="info"
[removable]="true"
(remove)="onPillRemove(value, $event)">
</app-status-pill>
<span *ngIf="selectedValues.length === 0" class="placeholder-text">{{ placeholder }}</span>
</div>
</button>
<!-- Das schwebende Label -->
<label class="form-label" [class.has-value]="selectedValues.length > 0">
{{ label }}
</label>
<!-- Die aufklappbare Liste mit den Checkboxen -->
<div *ngIf="isOpen" class="options-list">
<div class="category-checkbox-group">
<label *ngFor="let option of options">
<input
type="checkbox"
[value]="option.value"
[checked]="isSelected(option.value)"
(change)="onOptionToggle(option.value)" />
{{ option.label }}
</label>
</div>
</div>
</div>

View File

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

View File

@@ -9,6 +9,7 @@ import {
FormArray, FormArray,
ReactiveFormsModule, ReactiveFormsModule,
Validators, Validators,
FormControl,
} from '@angular/forms'; } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
@@ -25,7 +26,6 @@ import { ProductService } from '../../../services/product.service';
import { CategoryService } from '../../../services/category.service'; import { CategoryService } from '../../../services/category.service';
import { SupplierService } from '../../../services/supplier.service'; import { SupplierService } from '../../../services/supplier.service';
import { SnackbarService } from '../../../../shared/services/snackbar.service'; import { SnackbarService } from '../../../../shared/services/snackbar.service';
import { CardComponent } from '../../../../shared/components/ui/card/card.component';
import { import {
ImagePreview, ImagePreview,
ProductFormComponent, ProductFormComponent,
@@ -38,7 +38,7 @@ import { SelectOption } from '../../../../shared/components/form/form-select/for
imports: [ imports: [
CommonModule, CommonModule,
ReactiveFormsModule, ReactiveFormsModule,
CardComponent,
ProductFormComponent, ProductFormComponent,
], ],
templateUrl: './product-create.component.html', templateUrl: './product-create.component.html',
@@ -81,7 +81,7 @@ export class ProductCreateComponent implements OnInit, OnDestroy {
isFeatured: [false], isFeatured: [false],
featuredDisplayOrder: [0], featuredDisplayOrder: [0],
supplierId: [null], supplierId: [null],
categorieIds: this.fb.array([]), categorieIds: new FormControl([]),
}); });
} }
@@ -125,7 +125,7 @@ export class ProductCreateComponent implements OnInit, OnDestroy {
this.productService.create(formData).subscribe({ this.productService.create(formData).subscribe({
next: () => { next: () => {
this.snackbarService.show('Produkt erfolgreich erstellt'); this.snackbarService.show('Produkt erfolgreich erstellt');
this.router.navigate(['/admin/products']); this.router.navigate(['/shop/products']);
}, },
error: (err) => { error: (err) => {
this.snackbarService.show('Ein Fehler ist aufgetreten.'); this.snackbarService.show('Ein Fehler ist aufgetreten.');
@@ -135,7 +135,7 @@ export class ProductCreateComponent implements OnInit, OnDestroy {
} }
cancel(): void { cancel(): void {
this.router.navigate(['/admin/products']); this.router.navigate(['/shop/products']);
} }
onFilesSelected(files: File[]): void { onFilesSelected(files: File[]): void {

View File

@@ -3,26 +3,45 @@
import { Component, OnInit, OnDestroy, inject } from '@angular/core'; import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; 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 { DomSanitizer } from '@angular/platform-browser';
import { Observable, Subscription } from 'rxjs'; 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 // 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 { Category } from '../../../../core/models/category.model';
import { ProductService } from '../../../services/product.service'; import { ProductService } from '../../../services/product.service';
import { CategoryService } from '../../../services/category.service'; import { CategoryService } from '../../../services/category.service';
import { SupplierService } from '../../../services/supplier.service'; import { SupplierService } from '../../../services/supplier.service';
import { SnackbarService } from '../../../../shared/services/snackbar.service'; import { SnackbarService } from '../../../../shared/services/snackbar.service';
import { CardComponent } from '../../../../shared/components/ui/card/card.component'; 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'; import { SelectOption } from '../../../../shared/components/form/form-select/form-select.component';
@Component({ @Component({
selector: 'app-product-edit', selector: 'app-product-edit',
standalone: true, standalone: true,
imports: [ CommonModule, ReactiveFormsModule, CardComponent, ProductFormComponent ], imports: [CommonModule, ReactiveFormsModule, ProductFormComponent],
templateUrl: './product-edit.component.html', templateUrl: './product-edit.component.html',
styleUrls: ['./product-edit.component.css'], styleUrls: ['./product-edit.component.css'],
}) })
@@ -67,7 +86,7 @@ export class ProductEditComponent implements OnInit, OnDestroy {
isFeatured: [false], isFeatured: [false],
featuredDisplayOrder: [0], featuredDisplayOrder: [0],
supplierId: [null], supplierId: [null],
categorieIds: this.fb.array([]), categorieIds: new FormControl([]),
imagesToDelete: this.fb.array([]), imagesToDelete: this.fb.array([]),
}); });
} }
@@ -80,7 +99,7 @@ export class ProductEditComponent implements OnInit, OnDestroy {
const id = this.route.snapshot.paramMap.get('id'); const id = this.route.snapshot.paramMap.get('id');
if (!id) { if (!id) {
this.snackbarService.show('Produkt-ID fehlt.'); this.snackbarService.show('Produkt-ID fehlt.');
this.router.navigate(['/admin/products']); this.router.navigate(['/shop/products']);
return; return;
} }
this.productId = id; this.productId = id;
@@ -91,8 +110,9 @@ export class ProductEditComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.nameChangeSubscription?.unsubscribe(); this.nameChangeSubscription?.unsubscribe();
this.allImagesForForm.forEach(image => { this.allImagesForForm.forEach((image) => {
if (typeof image.url === 'object') { // Nur Object-URLs von neuen Bildern freigeben if (typeof image.url === 'object') {
// Nur Object-URLs von neuen Bildern freigeben
URL.revokeObjectURL(image.url as string); URL.revokeObjectURL(image.url as string);
} }
}); });
@@ -101,44 +121,55 @@ export class ProductEditComponent implements OnInit, OnDestroy {
loadDropdownData(): void { loadDropdownData(): void {
this.allCategories$ = this.categoryService.getAll(); this.allCategories$ = this.categoryService.getAll();
this.supplierOptions$ = this.supplierService.getAll().pipe( 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([]) startWith([])
); );
} }
loadProductData(): void { loadProductData(): void {
this.isLoading = true; this.isLoading = true;
this.productService.getById(this.productId).pipe( this.productService
finalize(() => { this.isLoading = false; }) .getById(this.productId)
).subscribe({ .pipe(
finalize(() => {
this.isLoading = false;
})
)
.subscribe({
next: (product) => { next: (product) => {
if (product) { if (product) {
this.populateForm(product); this.populateForm(product);
} else { } else {
this.snackbarService.show('Produkt nicht gefunden.'); this.snackbarService.show('Produkt nicht gefunden.');
this.router.navigate(['/admin/products']); this.router.navigate(['/shop/products']);
} }
}, },
error: (err) => { error: (err) => {
this.snackbarService.show('Fehler beim Laden des Produkts.'); this.snackbarService.show('Fehler beim Laden des Produkts.');
console.error('Fehler beim Laden der Produktdaten:', err); console.error('Fehler beim Laden der Produktdaten:', err);
this.router.navigate(['/admin/products']); this.router.navigate(['/shop/products']);
}, },
}); });
} }
populateForm(product: AdminProduct): void { // 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.productForm.patchValue(product);
const categories = this.productForm.get('categorieIds') as FormArray;
categories.clear(); // The obsolete FormArray logic has been removed.
product.categorieIds?.forEach(id => categories.push(new FormControl(id)));
this.existingImages = product.images || []; this.existingImages = product.images || [];
const mainImage = this.existingImages.find(img => img.isMainImage); const mainImage = this.existingImages.find((img) => img.isMainImage);
this.mainImageIdentifier = mainImage?.id || (this.existingImages.length > 0 ? this.existingImages[0].id : null); this.mainImageIdentifier =
mainImage?.id ||
(this.existingImages.length > 0 ? this.existingImages[0].id : null);
this.rebuildAllImagesForForm(); this.rebuildAllImagesForForm();
} }
onSubmit(): void { onSubmit(): void {
if (this.productForm.invalid) { if (this.productForm.invalid) {
@@ -151,7 +182,7 @@ export class ProductEditComponent implements OnInit, OnDestroy {
this.productService.update(this.productId, formData).subscribe({ this.productService.update(this.productId, formData).subscribe({
next: () => { next: () => {
this.snackbarService.show('Produkt erfolgreich aktualisiert'); this.snackbarService.show('Produkt erfolgreich aktualisiert');
this.router.navigate(['/admin/products']); this.router.navigate(['/shop/products']);
}, },
error: (err) => { error: (err) => {
this.snackbarService.show('Ein Fehler ist aufgetreten.'); 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 --- // --- NEUE EVENT-HANDLER FÜR BILDER ---
onFilesSelected(files: File[]): void { onFilesSelected(files: File[]): void {
@@ -177,17 +210,23 @@ export class ProductEditComponent implements OnInit, OnDestroy {
} }
onDeleteImage(identifier: string): void { onDeleteImage(identifier: string): void {
const isExisting = this.existingImages.some(img => img.id === identifier); const isExisting = this.existingImages.some((img) => img.id === identifier);
if (isExisting) { if (isExisting) {
this.imagesToDelete.push(new FormControl(identifier)); 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 { } else {
this.newImageFiles = this.newImageFiles.filter(file => file.name !== identifier); this.newImageFiles = this.newImageFiles.filter(
(file) => file.name !== identifier
);
} }
if (this.mainImageIdentifier === identifier) { if (this.mainImageIdentifier === identifier) {
const firstExisting = this.existingImages.length > 0 ? this.existingImages[0].id : null; const firstExisting =
const firstNew = this.newImageFiles.length > 0 ? this.newImageFiles[0].name : null; 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.mainImageIdentifier = firstExisting || firstNew;
} }
this.rebuildAllImagesForForm(); this.rebuildAllImagesForForm();
@@ -195,19 +234,28 @@ export class ProductEditComponent implements OnInit, OnDestroy {
// --- Private Helfermethoden --- // --- Private Helfermethoden ---
private rebuildAllImagesForForm(): void { private rebuildAllImagesForForm(): void {
this.allImagesForForm.forEach(image => { this.allImagesForForm.forEach((image) => {
if (typeof image.url === 'object') URL.revokeObjectURL(image.url as string); if (typeof image.url === 'object')
URL.revokeObjectURL(image.url as string);
}); });
const combined: ImagePreview[] = []; const combined: ImagePreview[] = [];
this.existingImages this.existingImages
.filter(img => !!img.url) // Stellt sicher, dass nur Bilder mit URL verwendet werden .filter((img) => !!img.url) // Stellt sicher, dass nur Bilder mit URL verwendet werden
.forEach(img => { .forEach((img) => {
combined.push({ identifier: img.id, url: img.url!, isMainImage: img.id === this.mainImageIdentifier }); combined.push({
identifier: img.id,
url: img.url!,
isMainImage: img.id === this.mainImageIdentifier,
});
}); });
this.newImageFiles.forEach(file => { this.newImageFiles.forEach((file) => {
combined.push({ identifier: file.name, url: this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(file)), isMainImage: file.name === this.mainImageIdentifier }); combined.push({
identifier: file.name,
url: this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(file)),
isMainImage: file.name === this.mainImageIdentifier,
});
}); });
this.allImagesForForm = combined; this.allImagesForForm = combined;
@@ -220,13 +268,17 @@ export class ProductEditComponent implements OnInit, OnDestroy {
Object.keys(formValue).forEach((key) => { Object.keys(formValue).forEach((key) => {
const value = formValue[key]; const value = formValue[key];
if (key === 'categorieIds' || key === 'imagesToDelete') { 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 !== '') { } else if (value !== null && value !== undefined && value !== '') {
formData.append(this.capitalizeFirstLetter(key), value.toString()); 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) { if (mainImageFile) {
formData.append('MainImageFile', mainImageFile); formData.append('MainImageFile', mainImageFile);
} else if (this.mainImageIdentifier) { } else if (this.mainImageIdentifier) {
@@ -234,13 +286,12 @@ export class ProductEditComponent implements OnInit, OnDestroy {
} }
this.newImageFiles this.newImageFiles
.filter(f => f.name !== this.mainImageIdentifier) .filter((f) => f.name !== this.mainImageIdentifier)
.forEach(file => formData.append('AdditionalImageFiles', file)); .forEach((file) => formData.append('AdditionalImageFiles', file));
return formData; return formData;
} }
// Private Helfermethoden // Private Helfermethoden
private prepareSubmissionData(): void { private prepareSubmissionData(): void {
const nameControl = this.productForm.get('name'); const nameControl = this.productForm.get('name');
@@ -295,12 +346,4 @@ export class ProductEditComponent 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);
} }
} }

View File

@@ -87,23 +87,17 @@
<app-form-select label="Lieferant" [options]="supplierOptions" formControlName="supplierId"></app-form-select> <app-form-select label="Lieferant" [options]="supplierOptions" formControlName="supplierId"></app-form-select>
<div class="multi-select-container"> <app-product-category-dropdown
<div class="selected-pills"> label="Kategorien"
<app-status-pill
*ngFor="let catId of categorieIds.value" [options]="categoryOptions"
[text]="getCategoryName(catId)" formControlName="categorieIds"
status="info" >
(remove)="removeCategoryById(catId)">
</app-status-pill>
<span *ngIf="categorieIds.length === 0" class="placeholder">Keine ausgewählt</span> </app-product-category-dropdown>
</div>
<div class="category-checkbox-group">
<label *ngFor="let category of allCategories">
<input type="checkbox" [value]="category.id" [checked]="isCategorySelected(category.id)" (change)="onCategoryChange($event)" />
{{ category.name }}
</label>
</div>
</div>
</app-form-group> </app-form-group>

View File

@@ -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 { CommonModule, NgClass } from '@angular/common';
import { import { FormGroup, ReactiveFormsModule } from '@angular/forms'; // FormArray and FormControl no longer needed here
FormGroup,
FormArray,
ReactiveFormsModule,
FormControl,
} from '@angular/forms';
import { SafeUrl } from '@angular/platform-browser'; import { SafeUrl } from '@angular/platform-browser';
// Models & UI Components // Models & UI Components
import { Category } from '../../../../core/models/category.model'; 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 { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { IconComponent } from '../../../../shared/components/ui/icon/icon.component'; import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.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 { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component';
import { SnackbarService } from '../../../../shared/services/snackbar.service'; import { SnackbarService } from '../../../../shared/services/snackbar.service';
import { FormGroupComponent } from '../../../../shared/components/form/form-group/form-group.component'; 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 { export interface ImagePreview {
identifier: string; // Eindeutiger Bezeichner (ID für existierende, Dateiname für neue) identifier: string;
url: string | SafeUrl; url: string | SafeUrl;
isMainImage: boolean; isMainImage: boolean;
} }
@@ -37,64 +30,58 @@ export interface ImagePreview {
selector: 'app-product-form', selector: 'app-product-form',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule, NgClass, ReactiveFormsModule, ButtonComponent, IconComponent,
NgClass, FormFieldComponent, FormSelectComponent, FormTextareaComponent,
ReactiveFormsModule, SlideToggleComponent, FormGroupComponent, ProductCategoryDropdownComponent
CardComponent,
ButtonComponent,
IconComponent,
FormFieldComponent,
FormSelectComponent,
FormTextareaComponent,
SlideToggleComponent,
FormGroupComponent,
StatusPillComponent
], ],
templateUrl: './product-form.component.html', templateUrl: './product-form.component.html',
styleUrls: ['./product-form.component.css'], styleUrls: ['./product-form.component.css'],
}) })
export class ProductFormComponent { export class ProductFormComponent {
// --- Inputs für Daten --- // --- Inputs ---
@Input() productForm!: FormGroup; @Input() productForm!: FormGroup;
@Input() allCategories: Category[] = []; @Input() allCategories: Category[] = [];
@Input() supplierOptions: SelectOption[] = []; @Input() supplierOptions: SelectOption[] = [];
@Input() isLoading = false; @Input() isLoading = false;
@Input() submitButtonText = 'Speichern'; @Input() submitButtonText = 'Speichern';
// NEU: Empfängt eine einzige, kombinierte Bildliste von der Elternkomponente
@Input() allImages: ImagePreview[] = []; @Input() allImages: ImagePreview[] = [];
// --- Outputs für Aktionen --- // --- Outputs ---
@Output() formSubmit = new EventEmitter<void>(); @Output() formSubmit = new EventEmitter<void>();
@Output() formCancel = new EventEmitter<void>(); @Output() formCancel = new EventEmitter<void>();
@Output() filesSelected = new EventEmitter<File[]>(); // Sendet alle neu ausgewählten Dateien @Output() filesSelected = new EventEmitter<File[]>();
@Output() setMainImage = new EventEmitter<string>(); // Sendet den 'identifier' des Bildes @Output() setMainImage = new EventEmitter<string>();
@Output() deleteImage = new EventEmitter<string>(); // Sendet den 'identifier' des Bildes @Output() deleteImage = new EventEmitter<string>();
private snackbarService = inject(SnackbarService); private snackbarService = inject(SnackbarService);
public categoryOptions: SelectOption[] = [];
// GETTER FÜR DAS TEMPLATE // --- GETTER ---
get categorieIds(): FormArray {
return this.productForm.get('categorieIds') as FormArray;
}
get hasImages(): boolean { get hasImages(): boolean {
return this.allImages && this.allImages.length > 0; return this.allImages && this.allImages.length > 0;
} }
// --- EVENT-HANDLER --- // --- LIFECYCLE HOOKS ---
onSubmit(): void { ngOnChanges(changes: SimpleChanges): void {
this.formSubmit.emit(); if (changes['allCategories']) {
this.categoryOptions = this.allCategories
.filter(cat => !!cat.name)
.map(cat => ({
value: cat.id,
label: cat.name!
}));
} }
cancel(): void {
this.formCancel.emit();
} }
// --- EVENT-HANDLERS ---
onSubmit(): void { this.formSubmit.emit(); }
cancel(): void { this.formCancel.emit(); }
onFilesSelected(event: Event): void { onFilesSelected(event: Event): void {
const files = (event.target as HTMLInputElement).files; const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) { if (files && files.length > 0) {
this.filesSelected.emit(Array.from(files)); 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 = ''; (event.target as HTMLInputElement).value = '';
} }
@@ -103,11 +90,10 @@ export class ProductFormComponent {
} }
requestImageDeletion(identifier: string, event: MouseEvent): void { requestImageDeletion(identifier: string, event: MouseEvent): void {
event.stopPropagation(); // Verhindert, dass gleichzeitig setAsMainImage gefeuert wird event.stopPropagation();
this.deleteImage.emit(identifier); this.deleteImage.emit(identifier);
} }
// --- Helfermethoden (unverändert) ---
generateSku(): void { generateSku(): void {
const name = this.productForm.get('name')?.value || 'PROD'; const name = this.productForm.get('name')?.value || 'PROD';
const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, ''); const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
@@ -117,33 +103,5 @@ export class ProductFormComponent {
this.snackbarService.show('Neue SKU generiert!'); this.snackbarService.show('Neue SKU generiert!');
} }
onCategoryChange(event: Event): void { // ALL OBSOLETE CATEGORY HELPER FUNCTIONS HAVE BEEN REMOVED
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);
}
} }

View File

@@ -16,7 +16,7 @@ import { IconComponent } from '../../../../shared/components/ui/icon/icon.compon
@Component({ @Component({
selector: 'app-product-list', selector: 'app-product-list',
standalone: true, standalone: true,
imports: [CommonModule, CurrencyPipe, DatePipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent], imports: [CommonModule, CurrencyPipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
providers: [DatePipe], providers: [DatePipe],
templateUrl: './product-list.component.html', templateUrl: './product-list.component.html',
styleUrl: './product-list.component.css' styleUrl: './product-list.component.css'

View File

@@ -2,12 +2,11 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule, NgClass } from '@angular/common'; import { CommonModule, NgClass } from '@angular/common';
import { IconComponent } from '../icon/icon.component'; // IconComponent importieren
@Component({ @Component({
selector: 'app-status-pill', selector: 'app-status-pill',
standalone: true, standalone: true,
imports: [CommonModule, NgClass, IconComponent], // IconComponent hinzufügen imports: [CommonModule, NgClass], // IconComponent hinzufügen
templateUrl: './status-pill.component.html', templateUrl: './status-pill.component.html',
styleUrls: ['./status-pill.component.css'] styleUrls: ['./status-pill.component.css']
}) })