import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core'; import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { GoodsService } from '../../core/services/carnet/goods.service'; import { NotificationService } from '../../core/services/common/notification.service'; import * as XLSX from 'xlsx'; import { CustomPaginator } from '../../shared/custom-paginator'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginatorIntl, MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; import { UserPreferences } from '../../core/models/user-preference'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { Country } from '../../core/models/country'; import { UnitOfMeasure } from '../../core/models/unitofmeasure'; import { CommonService } from '../../core/services/common/common.service'; import { finalize, Subject, takeUntil } from 'rxjs'; import { Goods, GoodsItem } from '../../core/models/carnet/goods'; @Component({ selector: 'app-goods', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule], templateUrl: './goods.component.html', styleUrl: './goods.component.scss', providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], }) export class GoodsComponent { @Input() headerid: number = 0; @Input() isViewMode = false; @Input() userPreferences: UserPreferences = {}; @Input() isEditMode: boolean = false; @Output() completed = new EventEmitter(); @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; // Table configuration displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions']; dataSource = new MatTableDataSource(); // Form controls goodsForm: FormGroup; itemForm: FormGroup; // UI state isEditing = false; currentItem: GoodsItem | null = null; isLoading = false; changeInProgress = false; showItemForm = false; fileToUpload: File | null = null; isProcessing = false; // Data countries: Country[] = []; unitsOfMeasures: UnitOfMeasure[] = []; private destroy$ = new Subject(); private fb = inject(FormBuilder); private goodsService = inject(GoodsService); private notificationService = inject(NotificationService); private dialog = inject(MatDialog); private errorHandler = inject(ApiErrorHandlerService); private commonService = inject(CommonService); constructor() { // Main form for goods section this.goodsForm = this.fb.group({ commercialSample: [false], professionalEquipment: [false], exhibitionsFair: [false], roadVehiclesUsed: [false], horseUsed: [false], authorizedRepresentatives: ['', Validators.required] }, { validator: this.atLeastOneRequired } ); // Form for individual items this.itemForm = this.fb.group({ itemNumber: ['', Validators.required], description: ['', Validators.required], pieces: [0, [Validators.required, Validators.min(1)]], weight: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,4})?$/)]], unitOfMeasure: ['', Validators.required], value: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,2})?$/)]], countryOfOrigin: ['US', Validators.required] }); } ngAfterViewInit() { this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } ngOnInit(): void { this.loadCountries(); this.loadUnitOfMeasures(); if (this.headerid > 0) { this.isLoading = true; this.goodsService.getGoodDetailsByHeaderId(this.headerid).pipe(finalize(() => { this.isLoading = false; })).subscribe({ next: (goodsDetail: Goods) => { if (goodsDetail) { this.patchFormData(goodsDetail); this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); } }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load good details'); this.notificationService.showError(errorMessage); console.error('Error loading good details:', error); } }); this.loadGoodsItems(); } } loadGoodsItems(): void { this.isLoading = true; this.goodsService.getGoodsItemsByHeaderId(this.headerid).pipe(finalize(() => { this.isLoading = false; })).subscribe({ next: (items: GoodsItem[]) => { this.dataSource.data = items; this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load goods items'); this.notificationService.showError(errorMessage); console.error('Error loading goods items:', error); } }); } loadCountries(): void { this.commonService.getCountries() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (countries) => { this.countries = countries; }, error: (error) => { console.error('Failed to load countries', error); } }); } loadUnitOfMeasures(): void { this.commonService.getUnitOfMeasures() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (units) => { this.unitsOfMeasures = units; }, error: (error) => { console.error('Failed to load unit of measures', error); } }); } patchFormData(data: Goods): void { this.goodsForm.patchValue({ commercialSample: data.commercialSample || false, professionalEquipment: data.professionalEquipment || false, exhibitionsFair: data.exhibitionsFair || false, roadVehiclesUsed: data.roadVehiclesUsed || false, horseUsed: data.horseUsed || false, authorizedRepresentatives: data.authorizedRepresentatives || '' }); } // Add new item to the table addNewItem(): void { this.showItemForm = true; this.isEditing = false; this.currentItem = null; this.itemForm.reset({ pieces: 0, weight: 0, value: 0, countryOfOrigin: 'US', }); this.itemForm.markAsUntouched(); } // Edit existing item editItem(item: GoodsItem): void { this.showItemForm = true; this.isEditing = true; this.currentItem = item; this.itemForm.patchValue({ itemNumber: item.itemNumber, description: item.description, pieces: item.pieces, weight: item.weight, unitOfMeasure: item.unitOfMeasure || 'kg', value: item.value, countryOfOrigin: item.countryOfOrigin || '' }); } saveItem(): void { if (this.itemForm.invalid || this.goodsForm.invalid) { this.itemForm.markAllAsTouched(); this.goodsForm.markAllAsTouched(); return; } this.onSubmit(); const itemData: GoodsItem = this.itemForm.value; // create a goods items array with a single item let itemDataArray: GoodsItem[] = []; itemDataArray.push(itemData); this.changeInProgress = true; const saveObservable = this.isEditing && this.currentItem ? this.goodsService.updateGoodsItem(this.headerid, itemDataArray) : this.goodsService.addGoodsItem(this.headerid, itemDataArray); saveObservable.pipe(finalize(() => { this.changeInProgress = false; })).subscribe({ next: () => { this.notificationService.showSuccess(`Goods ${this.isEditing ? 'updated' : 'added'} successfully`); this.loadGoodsItems(); this.cancelEdit(); this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} goods items`); this.notificationService.showError(errorMessage); console.error('Error saving goods item:', error); } }); } // Delete item with confirmation deleteItem(item: any): void { this.currentItem = item; const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '350px', data: { title: 'Confirm Delete', message: 'Are you sure you want to delete this item?', confirmText: 'Delete', cancelText: 'Cancel' } }); dialogRef.afterClosed().subscribe(result => { if (result) { this.goodsService.deleteGoodsItem(this.headerid, this.currentItem!).subscribe({ next: () => { this.notificationService.showSuccess('Item deleted successfully'); this.loadGoodsItems(); this.cancelEdit(); }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete item'); this.notificationService.showError(errorMessage); console.error('Error deleting item:', error); } }); } }); } // Handle file selection for upload fileUpload(event: any): void { const file: File = event.target.files[0]; this.processExcelFile(file) // .then(response => { // // console.log('File uploaded successfully:', response); // // Handle success (e.g., update UI) // }) .catch(error => { console.error('Error processing file:', error); this.notificationService.showError('Failed to upload file'); }); } async processExcelFile(file: File): Promise { if (file) { const validTypes = [ 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv' ]; if (!validTypes.includes(file.type)) { this.notificationService.showError('Please upload only Excel (XLSX, XLS) or CSV files'); return; } this.fileToUpload = file; if (!this.fileToUpload || !this.headerid) return; this.isProcessing = true; try { const data = await this.readExcelFile(this.fileToUpload); const items = this.parseExcelData(data); if (items.length === 0) { this.notificationService.showError('No valid items found in the file'); this.isProcessing = false; return; } // items.map(item => { // item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin); // item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure); // }); this.changeInProgress = true; this.goodsService.addGoodsItem(this.headerid, items).pipe(finalize(() => { this.changeInProgress = false; })).subscribe({ next: () => { this.notificationService.showSuccess(`Goods uploaded successfully`); this.loadGoodsItems(); this.cancelEdit(); this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); this.fileToUpload = null; }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, `Failed to save the upload goods items`); this.notificationService.showError(errorMessage); console.error('Error saving goods items:', error); this.fileToUpload = null; } }); } catch (error) { throw new Error(error instanceof Error ? error.message : 'Failed to process file'); } finally { this.isProcessing = false; } } } // Read Excel file private readExcelFile(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e: any) => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet); resolve(jsonData); } catch (error) { reject(error); } }; reader.onerror = (error) => reject(error); reader.readAsArrayBuffer(file); }); } // Parse Excel data into our format private parseExcelData(data: any[]): any[] { return data.map(row => ({ itemNumber: row['Item Number'] || row['itemNumber'] || '', description: row['Description'] || row['description'] || '', pieces: Number(row['Pieces'] || row['pieces'] || 0), weight: Number(row['Weight'] || row['weight'] || 0), unitOfMeasure: row['Unit of Measure'] || row['unit of measure'] || row['unitofmeasure'] || row['UnitOfMeasure'], value: Number(row['Value'] || row['value'] || 0), countryOfOrigin: row['Country Of Origin'] || row['Country of Origin'] || row['country of origin'] || row['countryoforigin'] || row['CountryOfOrigin'] || '' })).filter(item => item.itemNumber && item.description && item.pieces > 0 ); } // Save all goods data onSubmit(): void { if (this.goodsForm.invalid) { this.goodsForm.markAllAsTouched(); return; } const formData = this.goodsForm.value; this.changeInProgress = true; this.goodsService.saveGoodsData(this.headerid, formData).pipe(finalize(() => { this.changeInProgress = false; })).subscribe({ next: () => { this.notificationService.showSuccess('Goods information saved successfully'); this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save goods information'); this.notificationService.showError(errorMessage); console.error('Error saving goods:', error); } }); } // Cancel item editing cancelEdit(): void { this.showItemForm = false; this.isEditing = false; this.currentItem = null; this.itemForm.reset(); } private atLeastOneRequired(formGroup: FormGroup) { return (formGroup.get('commercialSample')?.value || formGroup.get('professionalEquipment')?.value || formGroup.get('exhibitionsFair')?.value) ? null : { atLeastOneRequired: true }; } getUnitOfMeasureLabel(value: string): string { const unit = this.unitsOfMeasures.find(u => u.value === value); return unit ? unit.name : value; } getCountryLabel(value: string): string { const country = this.countries.find(c => c.value === value); return country ? country.name : value; } getUnitOfMeasureValue(name: string): string { const unit = this.unitsOfMeasures.find(u => u.name === name); return unit ? unit.value : name; } getCountryValue(name: string): string { const country = this.countries.find(c => c.name === name); return country ? country.value : name; } formatDecimalDisplay(value: number, decimalPoints: number): string { if (value === null || value === undefined) return ''; // Format to show up to n decimal places, removing trailing zeros const parts = value.toString().split('.'); if (parts.length === 1) return parts[0]; // No decimals return `${parts[0]}.${parts[1].substring(0, decimalPoints).replace(/0+$/, '')}`; } }