-
Lade Produktdaten...
+
+
-
-
-
-
-
\ No newline at end of file
+
+
+
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 1c38ec3..a6f00a8 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
@@ -1,16 +1,29 @@
-// /src/app/features/admin/components/products/product-edit/product-edit.component.ts
-
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, SafeUrl } from '@angular/platform-browser';
import { Observable, Subscription } from 'rxjs';
-import { debounceTime, distinctUntilChanged, map, startWith, tap } from 'rxjs/operators';
+import {
+ map,
+ startWith,
+ debounceTime,
+ distinctUntilChanged,
+} from 'rxjs/operators';
// Models
-import { AdminProduct, ProductImage } from '../../../../core/models/product.model';
+import {
+ AdminProduct,
+ ProductImage,
+} from '../../../../core/models/product.model';
import { Category } from '../../../../core/models/category.model';
-import { Supplier } from '../../../../core/models/supplier.model';
// Services
import { ProductService } from '../../../services/product.service';
@@ -18,23 +31,25 @@ import { CategoryService } from '../../../services/category.service';
import { SupplierService } from '../../../services/supplier.service';
import { SnackbarService } from '../../../../shared/services/snackbar.service';
-// Wiederverwendbare UI- & Form-Komponenten
+// UI Components
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';
-import { FormSelectComponent, SelectOption } from '../../../../shared/components/form/form-select/form-select.component';
-import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component';
-import { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component';
+import { 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, ButtonComponent, IconComponent, FormFieldComponent, FormSelectComponent, FormTextareaComponent, SlideToggleComponent ],
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ CardComponent,
+ ProductFormComponent,
+ ],
templateUrl: './product-edit.component.html',
- styleUrl: './product-edit.component.css'
+ styleUrls: ['./product-edit.component.css'],
})
export class ProductEditComponent implements OnInit, OnDestroy {
+ // Service-Injektionen
private route = inject(ActivatedRoute);
private router = inject(Router);
private productService = inject(ProductService);
@@ -42,53 +57,65 @@ export class ProductEditComponent implements OnInit, OnDestroy {
private supplierService = inject(SupplierService);
private fb = inject(FormBuilder);
private snackbarService = inject(SnackbarService);
+ private sanitizer = inject(DomSanitizer);
- productId: string | null = null;
- isEditMode = false;
+ // Komponenten-Status
+ productId!: string;
isLoading = true;
productForm: FormGroup;
-
+ private nameChangeSubscription?: Subscription;
+
+ // Daten für Dropdowns und Formular
allCategories$!: Observable
;
supplierOptions$!: Observable;
- allCategories: Category[] = [];
- private nameChangeSubscription?: Subscription;
+ // State für Bild-Management
existingImages: ProductImage[] = [];
mainImageFile: File | null = null;
additionalImageFiles: File[] = [];
+ mainImagePreview: string | SafeUrl | null = null;
+ additionalImagesPreview: { name: string; url: SafeUrl }[] = [];
constructor() {
this.productForm = this.fb.group({
- name: ['', Validators.required], slug: ['', Validators.required], sku: ['', Validators.required],
- description: [''], price: [0, [Validators.required, Validators.min(0)]],
- oldPrice: [null, [Validators.min(0)]], purchasePrice: [null, [Validators.min(0)]],
- stockQuantity: [0, [Validators.required, Validators.min(0)]], weight: [null, [Validators.min(0)]],
- isActive: [true], isFeatured: [false], featuredDisplayOrder: [0],
- supplierId: [null], categorieIds: this.fb.array([]), imagesToDelete: this.fb.array([])
+ id: ['', Validators.required], // ID ist für das Update zwingend erforderlich
+ name: ['', Validators.required],
+ slug: ['', Validators.required],
+ sku: ['', Validators.required],
+ price: [0, [Validators.required, Validators.min(0)]],
+ stockQuantity: [0, [Validators.required, Validators.min(0)]],
+ description: [''],
+ oldPrice: [null, [Validators.min(0)]],
+ purchasePrice: [null, [Validators.min(0)]],
+ weight: [null, [Validators.min(0)]],
+ isActive: [true],
+ isFeatured: [false],
+ featuredDisplayOrder: [0],
+ supplierId: [null],
+ categorieIds: this.fb.array([]),
+ imagesToDelete: this.fb.array([]), // Wird für das Backend gefüllt
});
}
- get categorieIds(): FormArray { return this.productForm.get('categorieIds') as FormArray; }
- get imagesToDelete(): FormArray { return this.productForm.get('imagesToDelete') as FormArray; }
+ get categorieIds(): FormArray {
+ return this.productForm.get('categorieIds') as FormArray;
+ }
+ get imagesToDelete(): FormArray {
+ return this.productForm.get('imagesToDelete') as FormArray;
+ }
ngOnInit(): void {
- this.productId = this.route.snapshot.paramMap.get('id');
- this.isEditMode = !!this.productId;
-
- this.loadDropdownData();
- this.subscribeToNameChanges();
-
- if (this.isEditMode && this.productId) {
- this.isLoading = true;
- this.productService.getById(this.productId).subscribe(product => {
- if (product) {
- this.populateForm(product);
- }
- this.isLoading = false;
- });
- } else {
- this.isLoading = false;
+ const id = this.route.snapshot.paramMap.get('id');
+ if (!id) {
+ this.snackbarService.show('Produkt-ID fehlt. Umleitung zur Übersicht.');
+ this.router.navigate(['/admin/products']);
+ return;
}
+ this.productId = id;
+
+ this.loadDropdownData();
+ this.loadProductData();
+ this.subscribeToNameChanges();
}
ngOnDestroy(): void {
@@ -96,71 +123,46 @@ export class ProductEditComponent implements OnInit, OnDestroy {
}
loadDropdownData(): void {
- this.allCategories$ = this.categoryService.getAll().pipe(
- tap(categories => this.allCategories = categories)
- );
+ 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).subscribe({
+ next: (product) => {
+ if (product) {
+ this.populateForm(product);
+ } else {
+ this.snackbarService.show('Produkt nicht gefunden.');
+ this.router.navigate(['/admin/products']);
+ }
+ this.isLoading = false;
+ },
+ error: () => {
+ this.snackbarService.show('Fehler beim Laden des Produkts.');
+ this.isLoading = false;
+ this.router.navigate(['/admin/products']);
+ },
+ });
+ }
+
populateForm(product: AdminProduct): void {
this.productForm.patchValue(product);
+
this.categorieIds.clear();
- product.categorieIds?.forEach(id => this.categorieIds.push(this.fb.control(id)));
+ product.categorieIds?.forEach((id) =>
+ this.categorieIds.push(new FormControl(id))
+ );
+
this.existingImages = product.images || [];
- }
-
- onMainFileChange(event: Event): void {
- const file = (event.target as HTMLInputElement).files?.[0];
- if (file) this.mainImageFile = file;
- }
-
- onAdditionalFilesChange(event: Event): void {
- const files = (event.target as HTMLInputElement).files;
- if (files) this.additionalImageFiles = Array.from(files);
- }
-
- deleteExistingImage(imageId: string, event: Event): void {
- event.preventDefault();
- this.imagesToDelete.push(this.fb.control(imageId));
- this.existingImages = this.existingImages.filter(img => img.id !== imageId);
- }
-
- 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);
- }
- }
-
- generateSku(): void {
- const name = this.productForm.get('name')?.value || 'PROD';
- const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
- const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase();
- const sku = `${prefix}-${randomPart}`;
- this.productForm.get('sku')?.setValue(sku);
- this.snackbarService.show('Neue SKU generiert!');
+ const mainImage = this.existingImages.find((img) => img.isMainImage);
+ this.mainImagePreview = mainImage?.url ?? null;
}
onSubmit(): void {
@@ -169,60 +171,155 @@ export class ProductEditComponent implements OnInit, OnDestroy {
this.snackbarService.show('Bitte füllen Sie alle Pflichtfelder aus.');
return;
}
-
- const formData = new FormData();
- const formValue = this.productForm.value;
- 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));
- } else if (value !== null && value !== undefined && value !== '') {
- if (['oldPrice', 'purchasePrice', 'weight'].includes(key) && value === '') return;
- formData.append(this.capitalizeFirstLetter(key), value.toString());
- }
- });
+ this.prepareSubmissionData();
+ const formData = this.createFormData();
- if (this.mainImageFile) formData.append('MainImageFile', this.mainImageFile);
- this.additionalImageFiles.forEach((file) => formData.append('AdditionalImageFiles', file));
-
- const operation: Observable = this.isEditMode
- ? this.productService.update(this.productId!, formData)
- : this.productService.create(formData);
-
- operation.subscribe({
+ this.productService.update(this.productId, formData).subscribe({
next: () => {
- this.snackbarService.show(this.isEditMode ? 'Produkt aktualisiert' : 'Produkt erstellt');
+ this.snackbarService.show('Produkt erfolgreich aktualisiert');
this.router.navigate(['/admin/products']);
},
- error: (err: any) => {
- this.snackbarService.show('Ein Fehler ist aufgetreten.');
+ error: (err) => {
+ this.snackbarService.show(
+ 'Ein Fehler ist aufgetreten. Details siehe Konsole.'
+ );
console.error(err);
- }
+ },
});
}
cancel(): void {
this.router.navigate(['/admin/products']);
}
-
- private subscribeToNameChanges(): void {
- this.nameChangeSubscription = this.productForm.get('name')?.valueChanges.pipe(debounceTime(300), distinctUntilChanged())
- .subscribe((name) => {
- if (name && !this.productForm.get('slug')?.dirty) {
- const slug = this.generateSlug(name);
- this.productForm.get('slug')?.setValue(slug);
- }
+
+ // Handler für Bild-Events von der Form-Komponente
+ onMainFileSelected(file: File): void {
+ this.mainImageFile = file;
+ const reader = new FileReader();
+ reader.onload = () => {
+ this.mainImagePreview = this.sanitizer.bypassSecurityTrustUrl(
+ reader.result as string
+ );
+ };
+ reader.readAsDataURL(file);
+ }
+
+ onAdditionalFilesSelected(files: File[]): void {
+ this.additionalImageFiles.push(...files);
+ files.forEach((file) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ this.additionalImagesPreview.push({
+ name: file.name,
+ url: this.sanitizer.bypassSecurityTrustUrl(reader.result as string),
+ });
+ };
+ reader.readAsDataURL(file);
+ });
+ }
+
+ onExistingImageDeleted(imageId: string): void {
+ if (!this.imagesToDelete.value.includes(imageId)) {
+ this.imagesToDelete.push(new FormControl(imageId));
+ }
+ this.existingImages = this.existingImages.filter(
+ (img) => img.id !== imageId
+ );
+ // Wenn das gelöschte Bild das Hauptbild war, leeren wir die Vorschau
+ if (
+ this.mainImagePreview ===
+ this.existingImages.find((i) => i.id === imageId)?.url
+ ) {
+ this.mainImagePreview = null;
+ }
+ }
+
+ onNewImageRemoved(fileName: string): void {
+ this.additionalImageFiles = this.additionalImageFiles.filter(
+ (f) => f.name !== fileName
+ );
+ this.additionalImagesPreview = this.additionalImagesPreview.filter(
+ (p) => p.name !== fileName
+ );
+ }
+
+ // Private Helfermethoden
+ private prepareSubmissionData(): void {
+ const nameControl = this.productForm.get('name');
+ const slugControl = this.productForm.get('slug');
+ const skuControl = this.productForm.get('sku');
+
+ if (nameControl && slugControl && !slugControl.value) {
+ slugControl.setValue(this.generateSlug(nameControl.value), {
+ emitEvent: false,
});
+ }
+ if (nameControl && skuControl && !skuControl.value) {
+ skuControl.setValue(this.generateSkuValue(nameControl.value), {
+ emitEvent: false,
+ });
+ }
+ }
+
+ private createFormData(): FormData {
+ const formData = new FormData();
+ const formValue = this.productForm.getRawValue();
+
+ 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)
+ );
+ } else if (value !== null && value !== undefined && value !== '') {
+ formData.append(this.capitalizeFirstLetter(key), value.toString());
+ }
+ });
+
+ if (this.mainImageFile)
+ formData.append('MainImageFile', this.mainImageFile);
+ this.additionalImageFiles.forEach((file) =>
+ formData.append('AdditionalImageFiles', file)
+ );
+
+ return formData;
+ }
+
+ private subscribeToNameChanges(): void {
+ const nameControl = this.productForm.get('name');
+ const slugControl = this.productForm.get('slug');
+
+ if (nameControl && slugControl) {
+ this.nameChangeSubscription = nameControl.valueChanges
+ .pipe(debounceTime(400), distinctUntilChanged())
+ .subscribe((name) => {
+ if (name && !slugControl.dirty) {
+ slugControl.setValue(this.generateSlug(name));
+ }
+ });
+ }
}
private generateSlug(name: string): string {
- return name.toLowerCase().replace(/\s+/g, '-').replace(/[äöüß]/g, (char) => {
- switch (char) { case 'ä': return 'ae'; case 'ö': return 'oe'; case 'ü': return 'ue'; case 'ß': return 'ss'; default: return ''; }
- }).replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-');
+ return name
+ .toLowerCase()
+ .replace(/\s+/g, '-')
+ .replace(
+ /[äöüß]/g,
+ (char) => ({ ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }[char] || '')
+ )
+ .replace(/[^a-z0-9-]/g, '')
+ .replace(/-+/g, '-');
+ }
+
+ 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}`;
}
private capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
-}
\ No newline at end of file
+}
diff --git a/src/app/features/components/products/product-form/product-form.component.css b/src/app/features/components/products/product-form/product-form.component.css
new file mode 100644
index 0000000..947b799
--- /dev/null
+++ b/src/app/features/components/products/product-form/product-form.component.css
@@ -0,0 +1,267 @@
+/* /src/app/features/admin/components/products/product-form/product-form.component.css */
+
+/* ==========================================================================
+ Globale Variablen & Grund-Layout
+ ========================================================================== */
+
+:host {
+ --form-spacing-vertical: 1.5rem; /* Vertikaler Abstand zwischen Formular-Sektionen */
+ --form-spacing-horizontal: 1.5rem; /* Innenabstand der Sektionen */
+ --grid-gap: 1.5rem; /* Abstand zwischen den Spalten und Karten */
+ --border-radius: 8px;
+
+ --text-color-secondary: #64748b;
+ --background-color-light: #f8fafc;
+}
+
+.edit-layout {
+ display: grid;
+ grid-template-columns: 2fr 1fr; /* 2/3 für Hauptinhalt, 1/3 für Sidebar */
+ gap: var(--grid-gap);
+}
+
+.main-content,
+.sidebar-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--grid-gap);
+}
+
+/* ==========================================================================
+ Karten-Styling (app-card)
+ ========================================================================== */
+
+/* Wir stylen die app-card von außen, um ein konsistentes Layout zu gewährleisten. */
+app-card {
+ display: block; /* Stellt sicher, dass die Karte den vollen Platz einnimmt */
+ width: 100%;
+}
+
+/* Stile, die innerhalb der Karten gelten */
+.form-section {
+ padding: var(--form-spacing-horizontal);
+ display: flex;
+ flex-direction: column;
+ gap: var(--form-spacing-vertical);
+}
+
+h4[card-header] {
+ margin-bottom: 0; /* Entfernt Standard-Margin des Headers */
+}
+
+/* ==========================================================================
+ Allgemeine Formular-Elemente
+ ========================================================================== */
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.form-label {
+ font-weight: 500;
+ color: #334155;
+}
+
+.form-input,
+input[type="text"],
+input[type="number"] {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.form-input:focus,
+input[type="text"]:focus,
+input[type="number"]:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.2);
+}
+
+.form-hint {
+ font-size: 0.875rem;
+ color: var(--text-color-secondary);
+ margin-top: -0.75rem; /* Reduziert den Abstand nach oben */
+}
+
+.input-with-button {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.input-with-button .form-input {
+ flex-grow: 1;
+}
+
+/* ==========================================================================
+ Spezifisches Sektions-Styling
+ ========================================================================== */
+
+/* Preisgestaltung Grid */
+.price-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: var(--grid-gap);
+}
+
+/* Bild-Management */
+.image-upload-section {
+ padding-bottom: 1rem;
+ border-bottom: 1px solid var(--color-border);
+}
+.image-upload-section:last-of-type {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.image-gallery {
+ margin-top: 1rem;
+}
+
+.gallery-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-color-secondary);
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--color-border);
+ margin-bottom: 1rem;
+}
+
+.image-preview-container {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ border: 1px solid var(--color-border);
+}
+
+.image-preview {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.main-image-preview {
+ width: 100%;
+ height: auto;
+ max-height: 250px;
+ margin-bottom: 1rem;
+}
+
+.main-image-badge {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ background-color: var(--color-primary);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 16px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.image-preview-container app-button {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ background-color: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ --button-icon-color: white; /* Angenommen, dein Button kann dies überschreiben */
+ opacity: 0;
+ transition: opacity 0.2s ease-in-out;
+}
+
+.image-preview-container:hover app-button {
+ opacity: 1;
+}
+
+.additional-images-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+/* Kategorien-Auswahl */
+.multi-select-container {
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+}
+
+.selected-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.pill {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ background-color: var(--background-color-light);
+ border: 1px solid var(--color-border);
+ padding: 0.25rem 0.75rem;
+ border-radius: 16px;
+ font-size: 0.875rem;
+}
+
+.pill app-icon {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
+.pill app-icon:hover {
+ color: var(--color-danger);
+}
+
+.placeholder {
+ color: var(--text-color-secondary);
+}
+
+.category-checkbox-group {
+ max-height: 200px;
+ overflow-y: auto;
+ padding: 0.75rem;
+}
+
+.category-checkbox-group label {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 0;
+}
+
+/* ==========================================================================
+ Aktions-Buttons
+ ========================================================================== */
+
+.form-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1rem;
+ margin-top: var(--grid-gap);
+ padding-top: var(--grid-gap);
+ border-top: 1px solid var(--color-border);
+}
+
+/* ==========================================================================
+ Responsives Design
+ ========================================================================== */
+
+@media (max-width: 1024px) {
+ .edit-layout {
+ grid-template-columns: 1fr; /* Eine Spalte auf kleineren Bildschirmen */
+ }
+}
+
+.required-indicator {
+ color: var(--color-danger);
+ margin-left: 4px;
+}
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
new file mode 100644
index 0000000..4fbb4f5
--- /dev/null
+++ b/src/app/features/components/products/product-form/product-form.component.html
@@ -0,0 +1,201 @@
+
+
+
\ No newline at end of file
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
new file mode 100644
index 0000000..fa3f55b
--- /dev/null
+++ b/src/app/features/components/products/product-form/product-form.component.ts
@@ -0,0 +1,134 @@
+// /src/app/features/admin/components/products/product-form/product-form.component.ts
+
+import { Component, Input, Output, EventEmitter, inject, OnChanges, SimpleChanges } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormGroup, FormArray, ReactiveFormsModule, FormControl } from '@angular/forms';
+import { SafeUrl } from '@angular/platform-browser';
+
+// Models
+import { ProductImage } from '../../../../core/models/product.model';
+import { Category } from '../../../../core/models/category.model';
+
+// Services
+import { SnackbarService } from '../../../../shared/services/snackbar.service';
+
+// UI Components
+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';
+import { FormSelectComponent, SelectOption } from '../../../../shared/components/form/form-select/form-select.component';
+import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component';
+import { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component';
+
+@Component({
+ selector: 'app-product-form',
+ standalone: true,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ CardComponent,
+ ButtonComponent,
+ IconComponent,
+ FormFieldComponent,
+ FormSelectComponent,
+ FormTextareaComponent,
+ SlideToggleComponent,
+ ],
+ templateUrl: './product-form.component.html',
+ styleUrls: ['./product-form.component.css']
+})
+export class ProductFormComponent {
+ @Input() productForm!: FormGroup;
+ @Input() allCategories: Category[] = [];
+ @Input() supplierOptions: SelectOption[] = [];
+ @Input() isLoading = false;
+ @Input() submitButtonText = 'Speichern';
+
+ // Inputs & Outputs für Bilder
+ @Input() existingImages: ProductImage[] = [];
+ @Input() mainImagePreview: string | SafeUrl | null = null;
+ @Input() additionalImagesPreview: { name: string, url: string | SafeUrl }[] = [];
+
+ @Output() formSubmit = new EventEmitter();
+ @Output() formCancel = new EventEmitter();
+ @Output() mainFileSelected = new EventEmitter();
+ @Output() additionalFilesSelected = new EventEmitter();
+ @Output() existingImageDeleted = new EventEmitter();
+ @Output() newImageRemoved = new EventEmitter();
+
+ private snackbarService = inject(SnackbarService);
+
+ // GETTER FÜR DAS TEMPLATE
+ get categorieIds(): FormArray { return this.productForm.get('categorieIds') as FormArray; }
+
+ get hasImages(): boolean {
+ return !!this.mainImagePreview || this.additionalImagesPreview.length > 0 || this.existingImages.length > 0;
+ }
+
+ // NEU: Getter, der die Filter-Logik aus dem Template entfernt
+ get additionalExistingImages(): ProductImage[] {
+ return this.existingImages.filter(image => !image.isMainImage);
+ }
+
+ // FORMULAR-INTERAKTIONEN
+ onSubmit(): void { this.formSubmit.emit(); }
+ cancel(): void { this.formCancel.emit(); }
+
+ // BILD-MANAGEMENT
+ onMainFileChange(event: Event): void {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (file) this.mainFileSelected.emit(file);
+ }
+
+ onAdditionalFilesChange(event: Event): void {
+ const files = (event.target as HTMLInputElement).files;
+ if (files) this.additionalFilesSelected.emit(Array.from(files));
+ }
+
+ deleteExistingImage(imageId: string, event: Event): void {
+ event.preventDefault();
+ this.existingImageDeleted.emit(imageId);
+ }
+
+ removeNewImage(fileName: string, event: Event): void {
+ event.preventDefault();
+ this.newImageRemoved.emit(fileName);
+ }
+
+ // KATEGORIE- & SKU-HELFER
+ generateSku(): void {
+ const name = this.productForm.get('name')?.value || 'PROD';
+ const prefix = name.substring(0, 4).toUpperCase().replace(/\s+/g, '');
+ const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase();
+ const sku = `${prefix}-${randomPart}`;
+ this.productForm.get('sku')?.setValue(sku);
+ 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);
+ }
+}
\ 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 cb13706..1a30056 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
@@ -3,18 +3,12 @@
import { Component, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule, CurrencyPipe, DatePipe } from '@angular/common';
-import {
- AdminProduct,
- ProductImage,
-} from '../../../../core/models/product.model';
+import { AdminProduct, ProductImage } from '../../../../core/models/product.model';
import { ProductService } from '../../../services/product.service';
import { SupplierService } from '../../../services/supplier.service';
import { SnackbarService } from '../../../../shared/services/snackbar.service';
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 { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
@@ -22,18 +16,10 @@ 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, DatePipe, GenericTableComponent, SearchBarComponent, ButtonComponent, IconComponent],
providers: [DatePipe],
templateUrl: './product-list.component.html',
- styleUrl: './product-list.component.css',
+ styleUrl: './product-list.component.css'
})
export class ProductListComponent implements OnInit {
private productService = inject(ProductService);
@@ -42,66 +28,35 @@ export class ProductListComponent implements OnInit {
private snackbar = inject(SnackbarService);
private storageService = inject(StorageService);
private datePipe = inject(DatePipe);
-
+
private readonly TABLE_SETTINGS_KEY = 'product-table-columns';
- allProducts: (AdminProduct & {
- mainImage?: string;
- supplierName?: string;
- })[] = [];
- filteredProducts: (AdminProduct & {
- mainImage?: string;
- supplierName?: string;
- })[] = [];
+ allProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
+ filteredProducts: (AdminProduct & { mainImage?: string; supplierName?: string })[] = [];
isColumnFilterVisible = false;
readonly allTableColumns: ColumnConfig[] = [
{ key: 'mainImage', title: 'Bild', type: 'image' },
{ key: 'name', title: 'Name', type: 'text', subKey: 'sku' },
{ key: 'price', title: 'Preis', type: 'currency', cssClass: 'text-right' },
- {
- key: 'stockQuantity',
- title: 'Lager',
- type: 'text',
- cssClass: 'text-right',
- },
+ { key: 'stockQuantity', title: 'Lager', type: 'text', cssClass: 'text-right' },
{ key: 'supplierName', title: 'Lieferant', type: 'text' },
{ key: 'isActive', title: 'Aktiv', type: 'status' },
{ key: 'id', title: 'ID', type: 'text' },
{ key: 'description', title: 'Beschreibung', type: 'text' },
- {
- key: 'oldPrice',
- title: 'Alter Preis',
- type: 'currency',
- cssClass: 'text-right',
- },
- {
- key: 'purchasePrice',
- title: 'Einkaufspreis',
- type: 'currency',
- cssClass: 'text-right',
- },
+ { key: 'oldPrice', title: 'Alter Preis', type: 'currency', cssClass: 'text-right' },
+ { key: 'purchasePrice', title: 'Einkaufspreis', type: 'currency', cssClass: 'text-right' },
{ key: 'isInStock', title: 'Auf Lager', type: 'status' },
- { key: 'weight', title: 'Gewicht', type: 'number', cssClass: 'text-right' },
+ { key: 'weight', title: 'Gewicht', type: 'text', cssClass: 'text-right' },
{ key: 'slug', title: 'Slug', type: 'text' },
- { key: 'createdDate', title: 'Erstellt am', type: 'date' },
- { key: 'lastModifiedDate', title: 'Zuletzt geändert', type: 'date' },
+ { key: 'createdDate', title: 'Erstellt am', type: 'text' },
+ { key: 'lastModifiedDate', title: 'Zuletzt geändert', type: 'text' },
{ key: 'supplierId', title: 'Lieferanten-ID', type: 'text' },
{ key: 'categorieIds', title: 'Kategorie-IDs', type: 'text' },
{ key: 'isFeatured', title: 'Hervorgehoben', type: 'status' },
- {
- key: 'featuredDisplayOrder',
- title: 'Anzeigereihenfolge (hervorgehoben)',
- type: 'number',
- cssClass: 'text-right',
- },
- {
- key: 'actions',
- title: 'Aktionen',
- type: 'actions',
- cssClass: 'text-right',
- },
- ];
+ { key: 'featuredDisplayOrder', title: 'Anzeigereihenfolge (hervorgehoben)', type: 'number', cssClass: 'text-right' },
+ { key: 'actions', title: 'Aktionen', type: 'actions', cssClass: 'text-right' }
+];
visibleTableColumns: ColumnConfig[] = [];
constructor() {
@@ -113,16 +68,15 @@ export class ProductListComponent implements OnInit {
}
loadProducts(): void {
- this.productService.getAll().subscribe((products) => {
- this.supplierService.getAll().subscribe((suppliers) => {
- this.allProducts = products.map((p) => {
- const supplier = suppliers.find((s) => s.id === p.supplierId);
+ this.productService.getAll().subscribe(products => {
+ this.supplierService.getAll().subscribe(suppliers => {
+ this.allProducts = products.map(p => {
+ const supplier = suppliers.find(s => s.id === p.supplierId);
return {
...p,
mainImage: this.getMainImageUrl(p.images),
supplierName: supplier?.name || '-',
- createdDate:
- this.datePipe.transform(p.createdDate, 'dd.MM.yyyy HH:mm') || '-',
+ createdDate: this.datePipe.transform(p.createdDate, 'dd.MM.yyyy HH:mm') || '-',
};
});
this.onSearch('');
@@ -132,19 +86,18 @@ export class ProductListComponent implements OnInit {
onSearch(term: string): void {
const lowerTerm = term.toLowerCase();
- this.filteredProducts = this.allProducts.filter(
- (p) =>
- p.name?.toLowerCase().includes(lowerTerm) ||
- p.sku?.toLowerCase().includes(lowerTerm)
+ this.filteredProducts = this.allProducts.filter(p =>
+ p.name?.toLowerCase().includes(lowerTerm) ||
+ p.sku?.toLowerCase().includes(lowerTerm)
);
}
onAddNew(): void {
- this.router.navigate(['/shop/products/new']);
+ this.router.navigate(['/shop/products/create']);
}
onEditProduct(productId: string): void {
- this.router.navigate(['/shop/products', productId]);
+ this.router.navigate(['/shop/products/edit', productId]);
}
onDeleteProduct(productId: string): void {
@@ -157,26 +110,14 @@ export class ProductListComponent implements OnInit {
}
private loadTableSettings(): void {
- const savedKeys = this.storageService.getItem(
- this.TABLE_SETTINGS_KEY
- );
- const defaultKeys = [
- 'mainImage',
- 'name',
- 'price',
- 'stockQuantity',
- 'isActive',
- 'actions',
- ];
- const keysToUse =
- savedKeys && savedKeys.length > 0 ? savedKeys : defaultKeys;
- this.visibleTableColumns = this.allTableColumns.filter((c) =>
- keysToUse.includes(c.key)
- );
+ const savedKeys = this.storageService.getItem(this.TABLE_SETTINGS_KEY);
+ const defaultKeys = ['mainImage', 'name', 'price', 'stockQuantity', 'isActive', 'actions'];
+ const keysToUse = (savedKeys && savedKeys.length > 0) ? savedKeys : defaultKeys;
+ this.visibleTableColumns = this.allTableColumns.filter(c => keysToUse.includes(c.key));
}
private saveTableSettings(): void {
- const visibleKeys = this.visibleTableColumns.map((c) => c.key);
+ const visibleKeys = this.visibleTableColumns.map(c => c.key);
this.storageService.setItem(this.TABLE_SETTINGS_KEY, visibleKeys);
}
@@ -185,29 +126,26 @@ export class ProductListComponent implements OnInit {
}
isColumnVisible(columnKey: string): boolean {
- return this.visibleTableColumns.some((c) => c.key === columnKey);
+ return this.visibleTableColumns.some(c => c.key === columnKey);
}
onColumnToggle(column: ColumnConfig, event: Event): void {
const checkbox = event.target as HTMLInputElement;
if (checkbox.checked) {
this.visibleTableColumns.push(column);
- this.visibleTableColumns.sort(
- (a, b) =>
- this.allTableColumns.findIndex((c) => c.key === a.key) -
- this.allTableColumns.findIndex((c) => c.key === b.key)
+ this.visibleTableColumns.sort((a, b) =>
+ this.allTableColumns.findIndex(c => c.key === a.key) -
+ this.allTableColumns.findIndex(c => c.key === b.key)
);
} else {
- this.visibleTableColumns = this.visibleTableColumns.filter(
- (c) => c.key !== column.key
- );
+ this.visibleTableColumns = this.visibleTableColumns.filter(c => c.key !== column.key);
}
this.saveTableSettings();
}
getMainImageUrl(images?: ProductImage[]): string {
if (!images || images.length === 0) return 'https://via.placeholder.com/50';
- 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';
}
-}
+}
\ No newline at end of file
diff --git a/src/app/features/components/products/product-new/product-new.component.css b/src/app/features/components/products/product-new/product-new.component.css
deleted file mode 100644
index e69de29..0000000
diff --git a/src/app/features/components/products/product-new/product-new.component.html b/src/app/features/components/products/product-new/product-new.component.html
deleted file mode 100644
index 2ea317e..0000000
--- a/src/app/features/components/products/product-new/product-new.component.html
+++ /dev/null
@@ -1 +0,0 @@
-product-new works!
diff --git a/src/app/features/components/products/product-new/product-new.component.ts b/src/app/features/components/products/product-new/product-new.component.ts
deleted file mode 100644
index ce59b77..0000000
--- a/src/app/features/components/products/product-new/product-new.component.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'app-product-new',
- imports: [],
- templateUrl: './product-new.component.html',
- styleUrl: './product-new.component.css'
-})
-export class ProductNewComponent {
-
-}
diff --git a/src/app/features/components/products/products.routes.ts b/src/app/features/components/products/products.routes.ts
index 36ef617..107317c 100644
--- a/src/app/features/components/products/products.routes.ts
+++ b/src/app/features/components/products/products.routes.ts
@@ -3,21 +3,22 @@
import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductEditComponent } from './product-edit/product-edit.component';
+import { ProductCreateComponent } from './product-create/product-create.component';
export const PRODUCTS_ROUTES: Routes = [
- {
- path: '',
- component: ProductListComponent,
- title: 'Produktübersicht'
+ {
+ path: '',
+ component: ProductListComponent,
+ title: 'Produktübersicht',
},
- {
- path: 'new',
- component: ProductEditComponent,
- title: 'Neues Produkt erstellen'
+ {
+ path: 'create',
+ component: ProductCreateComponent,
+ title: 'Product | Create',
},
- {
- path: ':id',
- component: ProductEditComponent,
- title: 'Produkt bearbeiten'
+ {
+ path: 'edit/:id',
+ component: ProductEditComponent,
+ title: 'Product | Edit',
},
-];
\ No newline at end of file
+];
diff --git a/src/app/shared/components/form/form-field/form-field.component.css b/src/app/shared/components/form/form-field/form-field.component.css
index 475211f..cdf60d5 100644
--- a/src/app/shared/components/form/form-field/form-field.component.css
+++ b/src/app/shared/components/form/form-field/form-field.component.css
@@ -23,7 +23,7 @@
padding: 0.85rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-md);
-
+
/* Aussehen & Typografie */
background-color: var(--color-surface);
color: var(--color-text);
@@ -46,14 +46,18 @@
position: absolute;
top: 50%; /* Vertikal zentrieren (Schritt 1) */
left: 1rem; /* Linken Abstand wie beim Input-Padding halten */
- transform: translateY(-50%); /* Vertikal zentrieren (Schritt 2 - Feinjustierung) */
+ transform: translateY(
+ -50%
+ ); /* Vertikal zentrieren (Schritt 2 - Feinjustierung) */
border-radius: 4px;
/* Aussehen & Typografie */
color: var(--color-text-light);
- background-color: var(--color-surface); /* WICHTIG: Überdeckt die Input-Linie beim Schweben */
+ background-color: var(
+ --color-surface
+ ); /* WICHTIG: Überdeckt die Input-Linie beim Schweben */
padding: 0 0.25rem;
-
+
/* Verhalten */
transition: all 0.2s ease-out; /* Animiert alle Änderungen (top, font-size, color) */
pointer-events: none; /* Erlaubt Klicks "durch" das Label auf das Input-Feld darunter */
@@ -75,7 +79,9 @@
.form-input:focus ~ .form-label,
.form-input:not(:placeholder-shown) ~ .form-label {
top: 0;
- transform: translateY(-50%); /* Bleibt für die Zentrierung auf der Border-Linie wichtig */
+ transform: translateY(
+ -50%
+ ); /* Bleibt für die Zentrierung auf der Border-Linie wichtig */
font-size: 0.8rem;
color: var(--color-primary);
}
@@ -87,12 +93,12 @@
* und die richtige Farbe bekommt.
*/
.form-input:-webkit-autofill,
-.form-input:-webkit-autofill:hover,
-.form-input:-webkit-autofill:focus,
+.form-input:-webkit-autofill:hover,
+.form-input:-webkit-autofill:focus,
.form-input:-webkit-autofill:active {
- /* OPTIONAL: Überschreibt den unschönen gelben/blauen Autofill-Hintergrund */
- -webkit-box-shadow: 0 0 0 30px var(--color-surface) inset !important;
- -webkit-text-fill-color: var(--color-text) !important;
+ /* OPTIONAL: Überschreibt den unschönen gelben/blauen Autofill-Hintergrund */
+ -webkit-box-shadow: 0 0 0 30px var(--color-surface) inset !important;
+ -webkit-text-fill-color: var(--color-text) !important;
}
.form-input:-webkit-autofill ~ .form-label {
@@ -100,4 +106,18 @@
transform: translateY(-50%);
font-size: 0.8rem;
color: var(--color-primary);
-}
\ No newline at end of file
+}
+
+.required-indicator {
+ color: var(--color-danger);
+ font-weight: bold;
+ margin-left: 2px;
+}
+
+/* Styling für die Fehlermeldung */
+.error-message {
+ color: var(--color-danger);
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+ padding-left: 0.25rem;
+}
diff --git a/src/app/shared/components/form/form-field/form-field.component.html b/src/app/shared/components/form/form-field/form-field.component.html
index f7f9baf..9ca2787 100644
--- a/src/app/shared/components/form/form-field/form-field.component.html
+++ b/src/app/shared/components/form/form-field/form-field.component.html
@@ -1,13 +1,26 @@
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/app/shared/components/form/form-field/form-field.component.ts b/src/app/shared/components/form/form-field/form-field.component.ts
index 8f0cd2d..d1f06a7 100644
--- a/src/app/shared/components/form/form-field/form-field.component.ts
+++ b/src/app/shared/components/form/form-field/form-field.component.ts
@@ -2,7 +2,7 @@
import { Component, Input, forwardRef } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { AbstractControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
+import { AbstractControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators } from '@angular/forms'; // Validators importieren
@Component({
selector: 'app-form-field',
@@ -10,10 +10,10 @@ import { AbstractControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule }
imports: [
CommonModule,
FormsModule,
- ReactiveFormsModule // <-- WICHTIG: Hinzufügen, um mit AbstractControl zu arbeiten
+ ReactiveFormsModule
],
templateUrl: './form-field.component.html',
- styleUrl: './form-field.component.css',
+ styleUrls: ['./form-field.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -23,44 +23,42 @@ import { AbstractControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule }
],
})
export class FormFieldComponent {
- // --- KORREKTUR: Erweitere die erlaubten Typen ---
@Input() type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' = 'text';
@Input() label: string = '';
-
- // Neuer Input, um das FormControl für die Fehleranzeige zu erhalten
- @Input() control?: AbstractControl;
- @Input() showErrors = true; // Standardmäßig Fehler anzeigen
+ @Input() control?: AbstractControl | null;
+ @Input() showErrors = true;
controlId = `form-field-${Math.random().toString(36).substring(2, 9)}`;
-
- // --- Eigenschaften & Methoden für ControlValueAccessor ---
value: string | number = '';
onChange: (value: any) => void = () => {};
onTouched: () => void = () => {};
disabled = false;
- writeValue(value: any): void {
- this.value = value;
- }
- registerOnChange(fn: any): void {
- this.onChange = fn;
- }
- registerOnTouched(fn: any): void {
- this.onTouched = fn;
- }
- setDisabledState?(isDisabled: boolean): void {
- this.disabled = isDisabled;
+ // NEU: Getter, der automatisch prüft, ob das Feld ein Pflichtfeld ist.
+ get isRequired(): boolean {
+ if (!this.control) {
+ return false;
+ }
+ // hasValidator prüft, ob ein bestimmter Validator auf dem Control gesetzt ist.
+ return this.control.hasValidator(Validators.required);
}
- // Hilfsfunktion für das Template, um Fehler zu finden
get errorMessage(): string | null {
if (!this.control || !this.control.errors || (!this.control.touched && !this.control.dirty)) {
return null;
}
- if (this.control.hasError('required')) return 'Dieses Feld ist erforderlich.';
- if (this.control.hasError('email')) return 'Bitte geben Sie eine gültige E-Mail-Adresse ein.';
- if (this.control.hasError('min')) return `Der Wert muss mindestens ${this.control.errors['min'].min} sein.`;
- // ... weitere Fehlermeldungen hier
+ const errors = this.control.errors;
+ if (errors['required']) return 'Dieses Feld ist erforderlich.';
+ if (errors['email']) return 'Bitte geben Sie eine gültige E-Mail-Adresse ein.';
+ if (errors['min']) return `Der Wert muss mindestens ${errors['min'].min} sein.`;
+ if (errors['max']) return `Der Wert darf maximal ${errors['max'].max} sein.`;
+ if (errors['minlength']) return `Mindestens ${errors['minlength'].requiredLength} Zeichen erforderlich.`;
+
return 'Ungültige Eingabe.';
}
+
+ writeValue(value: any): void { this.value = value; }
+ registerOnChange(fn: any): void { this.onChange = fn; }
+ registerOnTouched(fn: any): void { this.onTouched = fn; }
+ setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; }
}
\ No newline at end of file
diff --git a/src/app/shared/components/form/form-textarea/form-textarea.component.css b/src/app/shared/components/form/form-textarea/form-textarea.component.css
index 89e3a9b..821d37d 100644
--- a/src/app/shared/components/form/form-textarea/form-textarea.component.css
+++ b/src/app/shared/components/form/form-textarea/form-textarea.component.css
@@ -76,4 +76,18 @@ border-radius: 4px;
top: 0;
font-size: 0.8rem;
color: var(--color-primary);
-}
\ No newline at end of file
+}
+
+.required-indicator {
+ color: var(--color-danger);
+ font-weight: bold;
+ margin-left: 2px;
+}
+
+/* Styling für die Fehlermeldung */
+.error-message {
+ color: var(--color-danger);
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+ padding-left: 0.25rem;
+}
diff --git a/src/app/shared/components/form/form-textarea/form-textarea.component.html b/src/app/shared/components/form/form-textarea/form-textarea.component.html
index 980ed7e..ff64793 100644
--- a/src/app/shared/components/form/form-textarea/form-textarea.component.html
+++ b/src/app/shared/components/form/form-textarea/form-textarea.component.html
@@ -1,13 +1,24 @@
-