461 lines
15 KiB
TypeScript
461 lines
15 KiB
TypeScript
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<boolean>();
|
|
|
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
@ViewChild(MatSort) sort!: MatSort;
|
|
|
|
// Table configuration
|
|
displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];
|
|
dataSource = new MatTableDataSource<any>();
|
|
|
|
// 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<void>();
|
|
|
|
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<void> {
|
|
|
|
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<any[]> {
|
|
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+$/, '')}`;
|
|
}
|
|
}
|