428 lines
14 KiB
TypeScript
428 lines
14 KiB
TypeScript
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
|
import {
|
|
AbstractControl,
|
|
FormBuilder,
|
|
FormGroup,
|
|
FormsModule,
|
|
ReactiveFormsModule, ValidatorFn,
|
|
Validators
|
|
} from "@angular/forms";
|
|
import {MatButton} from "@angular/material/button";
|
|
import {
|
|
MatCard,
|
|
MatCardActions,
|
|
MatCardContent,
|
|
MatCardHeader,
|
|
MatCardTitle
|
|
} from "@angular/material/card";
|
|
import {MatCheckbox} from "@angular/material/checkbox";
|
|
import {MatDivider} from "@angular/material/divider";
|
|
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
|
|
import {MatInput} from "@angular/material/input";
|
|
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
|
import {MatOption, MatSelect} from '@angular/material/select';
|
|
import {Router, RouterLink} from '@angular/router';
|
|
import {Subscription} from 'rxjs';
|
|
import {BrandService} from '../../services/app/brand.service';
|
|
import {Brand} from '../../interfaces/brand';
|
|
import {PlatformService} from '../../services/app/platform.service';
|
|
import {Platform} from '../../interfaces/platform';
|
|
import {Category} from '../../interfaces/category';
|
|
import {CategoryService} from '../../services/app/category.service';
|
|
import {ConditionService} from '../../services/app/condition.service';
|
|
import {Condition} from '../../interfaces/condition';
|
|
import {ProductService} from '../../services/app/product.service';
|
|
import {CdkTextareaAutosize} from '@angular/cdk/text-field';
|
|
import {ImageService} from '../../services/app/image.service';
|
|
import {ProductImageService} from '../../services/app/product_images.service';
|
|
|
|
@Component({
|
|
selector: 'app-add-product',
|
|
standalone: true,
|
|
imports: [
|
|
FormsModule,
|
|
MatButton,
|
|
MatCard,
|
|
MatCardActions,
|
|
MatCardContent,
|
|
MatCardHeader,
|
|
MatCardTitle,
|
|
MatCheckbox,
|
|
MatDivider,
|
|
MatError,
|
|
MatFormField,
|
|
MatInput,
|
|
MatLabel,
|
|
MatProgressSpinner,
|
|
ReactiveFormsModule,
|
|
MatSelect,
|
|
MatOption,
|
|
RouterLink,
|
|
CdkTextareaAutosize
|
|
],
|
|
templateUrl: './add-product.component.html',
|
|
styleUrl: './add-product.component.css'
|
|
})
|
|
export class AddProductComponent implements OnInit, OnDestroy {
|
|
|
|
addProductForm: FormGroup;
|
|
isSubmitted = false;
|
|
isLoading = false;
|
|
|
|
imageFile: File | null = null;
|
|
imagePreview: string | null = null;
|
|
private imageUploadSubscription: Subscription | null = null;
|
|
private imageLinkSubscription: Subscription | null = null;
|
|
|
|
brands: Brand[] = [];
|
|
platforms: Platform[] = [];
|
|
categories: Category[] = [];
|
|
conditions: Condition[] = [];
|
|
|
|
filteredBrands: Brand[] = [];
|
|
filteredPlatforms: Platform[] = [];
|
|
|
|
private addProductSubscription: Subscription | null = null;
|
|
|
|
private brandControlSubscription: Subscription | null = null;
|
|
private platformControlSubscription: Subscription | null = null;
|
|
|
|
private brandSubscription: Subscription | null = null;
|
|
private platformSubscription: Subscription | null = null;
|
|
private categorySubscription: Subscription | null = null;
|
|
private conditionSubscription: Subscription | null = null;
|
|
|
|
private readonly brandService: BrandService = inject(BrandService);
|
|
private readonly platformService = inject(PlatformService);
|
|
private readonly categoryService = inject(CategoryService);
|
|
private readonly conditionService = inject(ConditionService);
|
|
private readonly imageService = inject(ImageService)
|
|
private readonly productService = inject(ProductService);
|
|
private readonly productImageService = inject(ProductImageService);
|
|
|
|
private readonly router: Router = inject(Router);
|
|
|
|
constructor(private readonly formBuilder: FormBuilder) {
|
|
this.addProductForm = this.formBuilder.group({
|
|
title: ['', [
|
|
Validators.required,
|
|
Validators.minLength(3),
|
|
Validators.maxLength(50),
|
|
Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
|
|
]],
|
|
description: ['', [
|
|
Validators.required,
|
|
Validators.minLength(10),
|
|
Validators.maxLength(255),
|
|
Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
|
|
]],
|
|
category: ['', [
|
|
Validators.required
|
|
]],
|
|
condition: ['', [
|
|
Validators.required
|
|
]],
|
|
// stocker des ids (string|number) dans les controls
|
|
brand: ['', [
|
|
Validators.required
|
|
]],
|
|
platform: ['', [
|
|
Validators.required
|
|
]],
|
|
complete: [true],
|
|
manual: [true],
|
|
price: ['', [
|
|
Validators.required,
|
|
Validators.pattern(/^\d+([.,]\d{1,2})?$/),
|
|
this.priceRangeValidator(0, 9999)
|
|
]],
|
|
quantity: ['', [
|
|
Validators.required,
|
|
Validators.min(1),
|
|
Validators.max(999),
|
|
Validators.pattern(/^\d+$/)
|
|
]]
|
|
},
|
|
);
|
|
}
|
|
|
|
private normalizeIds<T extends Record<string, any>>(items: T[] | undefined, idKey = 'id'): T[] {
|
|
return (items || []).map((it, i) => ({
|
|
...it,
|
|
[idKey]: (it[idKey] ?? i)
|
|
}));
|
|
}
|
|
|
|
private getPlatformBrandId(platform: any): string | number | undefined {
|
|
if (!platform) return undefined;
|
|
const maybe = platform.brand ?? platform['brand_id'] ?? platform['brandId'];
|
|
if (maybe == null) return undefined;
|
|
|
|
if (typeof maybe === 'object') {
|
|
if (maybe.id != null) return maybe.id;
|
|
if (maybe.name != null) {
|
|
const found = this.brands.find(b =>
|
|
String(b.name).toLowerCase() === String(maybe.name).toLowerCase()
|
|
|| String(b.id) === String(maybe.name)
|
|
);
|
|
return found?.id;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const asStr = String(maybe);
|
|
const match = this.brands.find(b =>
|
|
String(b.id) === asStr || String(b.name).toLowerCase() === asStr.toLowerCase()
|
|
);
|
|
return match?.id ?? maybe;
|
|
}
|
|
|
|
private priceRangeValidator(min: number, max: number): ValidatorFn {
|
|
return (control: AbstractControl) => {
|
|
const val = control.value;
|
|
if (val === null || val === undefined || val === '') return null;
|
|
const normalized = String(val).replace(',', '.').trim();
|
|
const num = Number.parseFloat(normalized);
|
|
if (Number.isNaN(num)) return {pattern: true};
|
|
return (num < min || num > max) ? {range: {min, max, actual: num}} : null;
|
|
};
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
|
|
this.brandSubscription = this.brandService.getAll().subscribe({
|
|
next: (brands: Brand[]) => {
|
|
this.brands = this.normalizeIds(brands, 'id');
|
|
this.filteredBrands = [...this.brands];
|
|
},
|
|
error: (error: any) => {
|
|
console.error('Error fetching brands:', error);
|
|
},
|
|
complete: () => {
|
|
console.log('Finished fetching brands:', this.brands);
|
|
}
|
|
});
|
|
|
|
this.platformSubscription = this.platformService.getAll().subscribe({
|
|
next: (platforms: Platform[]) => {
|
|
this.platforms = this.normalizeIds(platforms, 'id');
|
|
this.filteredPlatforms = [...this.platforms];
|
|
},
|
|
error: (error: any) => {
|
|
console.error('Error fetching platforms:', error);
|
|
},
|
|
complete: () => {
|
|
console.log('Finished fetching platforms:', this.platforms);
|
|
}
|
|
});
|
|
|
|
this.categorySubscription = this.categoryService.getAll().subscribe({
|
|
next: (categories: Category[]) => {
|
|
this.categories = this.normalizeIds(categories, 'id');
|
|
},
|
|
error: (error: any) => {
|
|
console.error('Error fetching categories:', error);
|
|
},
|
|
complete: () => {
|
|
console.log('Finished fetching categories:', this.categories);
|
|
}
|
|
});
|
|
|
|
this.conditionSubscription = this.conditionService.getAll().subscribe({
|
|
next: (conditions: Condition[]) => {
|
|
this.conditions = this.normalizeIds(conditions, 'id');
|
|
},
|
|
error: (error) => {
|
|
console.error('Error fetching conditions:', error);
|
|
},
|
|
complete: () => {
|
|
console.log('Finished fetching conditions:', this.conditions);
|
|
}
|
|
});
|
|
|
|
const brandControl = this.addProductForm.get('brand');
|
|
const platformControl = this.addProductForm.get('platform');
|
|
|
|
this.brandControlSubscription = brandControl?.valueChanges.subscribe((brandId) => {
|
|
if (brandId != null && brandId !== '') {
|
|
const brandIdStr = String(brandId);
|
|
this.filteredPlatforms = this.platforms.filter(p => {
|
|
const pBid = this.getPlatformBrandId(p);
|
|
return pBid != null && String(pBid) === brandIdStr;
|
|
});
|
|
const curPlatformId = platformControl?.value;
|
|
if (curPlatformId != null && !this.filteredPlatforms.some(p => String(p.id) === String(curPlatformId))) {
|
|
platformControl?.setValue(null);
|
|
}
|
|
} else {
|
|
this.filteredPlatforms = [...this.platforms];
|
|
}
|
|
}) ?? null;
|
|
|
|
this.platformControlSubscription = platformControl?.valueChanges.subscribe((platformId) => {
|
|
if (platformId != null && platformId !== '') {
|
|
const platformObj = this.platforms.find(p => String(p.id) === String(platformId));
|
|
const pBrandId = this.getPlatformBrandId(platformObj);
|
|
if (pBrandId != null) {
|
|
const pBrandIdStr = String(pBrandId);
|
|
this.filteredBrands = this.brands.filter(b => String(b.id) === pBrandIdStr);
|
|
const curBrandId = brandControl?.value;
|
|
if (curBrandId != null && String(curBrandId) !== pBrandIdStr) {
|
|
brandControl?.setValue(null);
|
|
}
|
|
} else {
|
|
this.filteredBrands = [...this.brands];
|
|
}
|
|
} else {
|
|
this.filteredBrands = [...this.brands];
|
|
}
|
|
}) ?? null;
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.addProductSubscription?.unsubscribe();
|
|
this.brandControlSubscription?.unsubscribe();
|
|
this.platformControlSubscription?.unsubscribe();
|
|
this.brandSubscription?.unsubscribe();
|
|
this.platformSubscription?.unsubscribe();
|
|
this.categorySubscription?.unsubscribe();
|
|
this.conditionSubscription?.unsubscribe();
|
|
this.imageUploadSubscription?.unsubscribe();
|
|
this.imageLinkSubscription?.unsubscribe();
|
|
}
|
|
|
|
onFileSelected(event: Event) {
|
|
const input = event.target as HTMLInputElement;
|
|
if (!input.files || input.files.length === 0) {
|
|
this.imageFile = null;
|
|
this.imagePreview = null;
|
|
return;
|
|
}
|
|
const file = input.files[0];
|
|
this.imageFile = file;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
this.imagePreview = String(reader.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
onProductAdd() {
|
|
this.isSubmitted = true;
|
|
|
|
if (!this.addProductForm.valid) return;
|
|
|
|
this.isLoading = true;
|
|
const raw = this.addProductForm.value;
|
|
|
|
// parsing price/quantity etc (même logique que l'original)
|
|
const priceStr = raw.price ?? '';
|
|
const priceNum = Number(String(priceStr).replace(',', '.').trim());
|
|
if (Number.isNaN(priceNum)) {
|
|
this.isLoading = false;
|
|
this.addProductForm.get('price')?.setErrors({pattern: true});
|
|
return;
|
|
}
|
|
const quantityNum = Number(raw.quantity);
|
|
|
|
const brandId = raw.brand;
|
|
const brandObj = this.brands.find(b => String(b.id) === String(brandId)) ?? {id: brandId, name: undefined};
|
|
|
|
const platformId = raw.platform;
|
|
const foundPlatform = this.platforms.find(p => String(p.id) === String(platformId));
|
|
const platformObj = {
|
|
...(foundPlatform ?? {id: platformId, name: undefined}),
|
|
brand: foundPlatform?.brand ? (typeof foundPlatform.brand === 'object' ? foundPlatform.brand : (this.brands.find(b => String(b.id) === String(foundPlatform.brand)) ?? brandObj)) : brandObj
|
|
};
|
|
|
|
const payload = {
|
|
...raw,
|
|
price: priceNum,
|
|
quantity: quantityNum,
|
|
brand: brandObj,
|
|
platform: platformObj
|
|
};
|
|
|
|
// 1) créer le produit
|
|
this.addProductSubscription = this.productService.add(payload).subscribe({
|
|
next: (createdProduct: any) => {
|
|
const productId = createdProduct?.id;
|
|
if (!this.imageFile) {
|
|
// pas d'image => fin
|
|
this.afterSuccessfulAdd();
|
|
return;
|
|
}
|
|
|
|
// 2) upload de l'image
|
|
this.imageUploadSubscription = this.imageService.add(this.imageFile).subscribe({
|
|
next: (uploadedImage: { id: any; }) => {
|
|
const imageId = uploadedImage?.id;
|
|
if (!productId || imageId == null) {
|
|
console.error('Missing productId or imageId after upload');
|
|
this.afterSuccessfulAdd(); // navigation quand même ou gérer l'erreur
|
|
return;
|
|
}
|
|
// 3) lier image <-> product
|
|
this.imageLinkSubscription = this.productImageService.link(productId, imageId).subscribe({
|
|
next: () => {
|
|
this.afterSuccessfulAdd();
|
|
},
|
|
error: (error: any) => {
|
|
console.error('Error linking image to product:', error);
|
|
alert('Produit ajouté, mais la liaison de l\'image a échoué.');
|
|
this.afterSuccessfulAdd();
|
|
}
|
|
});
|
|
},
|
|
error: (error: any) => {
|
|
console.error('Error uploading image:', error);
|
|
alert('Produit ajouté, mais l\'upload de l\'image a échoué.');
|
|
this.afterSuccessfulAdd();
|
|
}
|
|
});
|
|
},
|
|
error: (error: any) => {
|
|
console.error("Error adding product:", error);
|
|
alert("Une erreur est survenue lors de l'ajout du produit.");
|
|
this.isLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private afterSuccessfulAdd() {
|
|
this.addProductForm.reset();
|
|
this.imageFile = null;
|
|
this.imagePreview = null;
|
|
this.isSubmitted = false;
|
|
this.isLoading = false;
|
|
alert("Produit ajouté avec succès !");
|
|
this.router.navigate(['/products']).then();
|
|
}
|
|
|
|
isFieldInvalid(fieldName: string): boolean {
|
|
const field = this.addProductForm.get(fieldName);
|
|
return Boolean(field && field.invalid && (field.dirty || field.touched || this.isSubmitted));
|
|
}
|
|
|
|
getFieldError(fieldName: string): string {
|
|
const field = this.addProductForm.get(fieldName);
|
|
|
|
if (field && field.errors) {
|
|
if (field.errors['required']) return `Ce champ est obligatoire`;
|
|
if (field.errors['email']) return `Format d'email invalide`;
|
|
if (field.errors['minlength']) return `Minimum ${field.errors['minlength'].requiredLength} caractères`;
|
|
if (field.errors['maxlength']) return `Maximum ${field.errors['maxlength'].requiredLength} caractères`;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
compareById = (a: any, b: any) => {
|
|
if (a == null || b == null) return a === b;
|
|
if (typeof a !== 'object' || typeof b !== 'object') {
|
|
return String(a) === String(b);
|
|
}
|
|
return String(a.id ?? a) === String(b.id ?? b);
|
|
};
|
|
}
|