extend carnet and feedback updates

This commit is contained in:
Cyril Joseph 2025-08-16 20:36:28 -03:00
parent f484131cb7
commit 6a628b9dc7
22 changed files with 475 additions and 36 deletions

View File

@ -17,6 +17,7 @@ export const serverRoutes: ServerRoute[] = [
{ path: ':appId/process-extend-carnet/:headerid', renderMode: RenderMode.Client },
{ path: ':appId/process-additional-sets/:headerid', renderMode: RenderMode.Client },
{ path: ':appId/view-carnet/:headerid', renderMode: RenderMode.Client },
{ path: ':appId/view-extend-carnet/:headerid', renderMode: RenderMode.Client },
{ path: ':appId/add-holder', renderMode: RenderMode.Client },
{ path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client },
{

View File

@ -26,6 +26,7 @@ export const routes: Routes = [
{ path: 'process-extend-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent), canDeactivate: [CarnetCleanupGuard] },
{ path: 'process-additional-sets/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent), canDeactivate: [CarnetCleanupGuard] },
{ path: 'view-carnet/:headerid', loadComponent: () => import('./carnet/view/view-carnet.component').then(m => m.ViewCarnetComponent), canDeactivate: [CarnetCleanupGuard] },
{ path: 'view-extend-carnet/:headerid', loadComponent: () => import('./carnet/view/view-carnet.component').then(m => m.ViewCarnetComponent), canDeactivate: [CarnetCleanupGuard] },
{ path: 'add-holder', loadComponent: () => import('./holder/add/add-holder.component').then(m => m.AddHolderComponent), canDeactivate: [CarnetCleanupGuard] },
{ path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit-holder.component').then(m => m.EditHolderComponent), canDeactivate: [CarnetCleanupGuard] },
{ path: '', redirectTo: 'home', pathMatch: 'full' }

View File

@ -1 +0,0 @@
<p>additional-set works!</p>

View File

@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-additional-set',
imports: [],
templateUrl: './additional-set.component.html',
styleUrl: './additional-set.component.scss'
})
export class AdditionalSetComponent {
}

View File

@ -1 +0,0 @@
<p>duplicate works!</p>

View File

@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-duplicate',
imports: [],
templateUrl: './duplicate.component.html',
styleUrl: './duplicate.component.scss'
})
export class DuplicateComponent {
}

View File

@ -3,6 +3,15 @@
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
[selectedIndex]="currentStep">
<!-- Replacement Application Step -->
<mat-step *ngIf="applicationType === 'extend'" [completed]="stepsCompleted.extension"
[editable]="stepsCompleted.extension">
<ng-template matStepLabel>Extension Application</ng-template>
<app-replacement-set (completed)="onExtensionApplicationSaved($event)" [headerid]="headerid"
[isEditMode]="isEditMode && applicationType === 'extend'">
</app-replacement-set>
</mat-step>
<!-- Application Name Step -->
<mat-step [completed]="stepsCompleted.applicationDetail" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Application Name</ng-template>

View File

@ -11,11 +11,12 @@ import { ShippingComponent } from '../shipping/shipping.component';
import { TravelPlanComponent } from '../travel-plan/travel-plan.component';
import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service';
import { ReplacementSetComponent } from '../replacement-set/replacement-set.component';
@Component({
selector: 'app-edit-carnet',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent],
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent, ReplacementSetComponent],
templateUrl: './edit-carnet.component.html',
styleUrl: './edit-carnet.component.scss'
})
@ -38,7 +39,8 @@ export class EditCarnetComponent {
holderSelection: false,
goodsSection: false,
travelPlan: false,
shipping: false
shipping: false,
extension: false
};
private route = inject(ActivatedRoute);
@ -70,6 +72,8 @@ export class EditCarnetComponent {
if (returnTo === 'holder') {
this.currentStep = 1;
} else if (returnTo === 'shipping' && this.applicationType === 'extend') {
this.currentStep = 5;
} else if (returnTo === 'shipping') {
this.currentStep = 4;
} else if (this.applicationType === 'additional') {
@ -115,9 +119,15 @@ export class EditCarnetComponent {
this.shippingComponent?.refreshInsuranceData(data);
}
onExtensionApplicationSaved(completed: boolean): void {
this.stepsCompleted.extension = completed;
this.isAllSectionsCompleted();
}
isAllSectionsCompleted() {
this.allSectionsCompleted = this.stepsCompleted.applicationDetail
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan;
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan
&& (this.applicationType !== 'extend' || this.stepsCompleted.extension);
}
}

View File

@ -33,8 +33,15 @@ export class GoodsComponent {
@Input() isEditMode: boolean = false;
@Output() completed = new EventEmitter<boolean>();
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort, { static: false })
set sort(value: MatSort) {
this.dataSource.sort = value;
}
// Table configuration
displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];

View File

@ -1 +1,98 @@
<p>replacement-set works!</p>
<!-- extension-section.component.html -->
<div class="extension-container">
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<!-- General Section -->
<div class="general-section">
<h3>General</h3>
<div class="detail-row">
<label>Carnet No:</label>
<span>{{ replacementDetails?.replacementCarnetNumber || 'N/A' }}</span>
</div>
<div class="detail-row">
<label>Issue Date:</label>
<span>{{ replacementDetails?.issueDate | date:'mediumDate':'UTC' }}</span>
</div>
<div class="detail-row">
<label>Preparer Name:</label>
<span>{{ replacementDetails?.preparerName || 'N/A' }}</span>
</div>
<div class="detail-row">
<label>Address:</label>
<span>{{ getAddressLabel() || 'N/A' }}</span>
</div>
</div>
<!-- Extension Section -->
<div class="extension-section">
<h3>Extension</h3>
<form [formGroup]="extensionForm" (ngSubmit)="onSubmit()">
<div class="form-row">
<mat-form-field appearance="outline" class="current-port">
<mat-label>Current port where the goods are located</mat-label>
<input matInput formControlName="portName" required>
<mat-error *ngIf="f['portName'].errors?.['required']">
Port location is required
</mat-error>
<mat-error *ngIf="f['portName'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="country">
<mat-label>Country</mat-label>
<mat-select formControlName="portCountry" required>
<mat-option *ngFor="let country of countries" [value]="country.value">
{{ country.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['portCountry'].errors?.['required']">
Port Country is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="reason">
<mat-label>Reason for Extension</mat-label>
<mat-select formControlName="reasonForExtension" required>
<mat-option *ngFor="let reason of extensionReasons" [value]="reason.value">
{{ reason.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['reasonForExtension'].errors?.['required']">
Reason is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="duration">
<mat-label>Duration</mat-label>
<input matInput type="number" formControlName="duration" required>
<span matTextSuffix>Month(s)</span>
<mat-error *ngIf="f['duration'].errors?.['required']">
Duration is required
</mat-error>
<mat-error *ngIf="f['duration'].errors?.['min']">
Minimum 1 month required
</mat-error>
<mat-error *ngIf="f['duration'].errors?.['max']">
Maximum 24 months allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode"
[disabled]="extensionForm.invalid || changeInProgress">
Save
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,51 @@
.extension-container {
padding: 0.5rem;
.general-section,
.extension-section {
margin-bottom: 24px;
h3 {
margin: 0;
margin-bottom: 0.5rem;
color: var(--mat-sys-primary);
font-weight: 500;
}
}
.detail-row {
font-size: 0.875rem;
margin-bottom: 0.5rem;
label {
display: inline-block;
width: 120px;
}
span {
font-weight: 500;
}
}
.form-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
align-items: start;
mat-form-field {
flex: 1;
}
.current-port {
grid-column: span 2;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}

View File

@ -1,11 +1,173 @@
import { Component } from '@angular/core';
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { finalize, Subject, takeUntil } from 'rxjs';
import { Country } from '../../core/models/country';
import { CommonService } from '../../core/services/common/common.service';
import { Replacement } from '../../core/models/carnet/replacement';
import { ReplacementService } from '../../core/services/carnet/replacement.service';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service';
import { CommonModule } from '@angular/common';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { ExtensionReason } from '../../core/models/extension-reason';
@Component({
selector: 'app-replacement-set',
imports: [],
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './replacement-set.component.html',
styleUrl: './replacement-set.component.scss'
})
export class ReplacementSetComponent {
@Input() headerid: number = 0;
@Input() isViewMode = false;
@Input() isEditMode: boolean = false;
@Output() completed = new EventEmitter<boolean>();
}
extensionForm: FormGroup;
countries: Country[] = [];
extensionReasons: ExtensionReason[] = [];
replacementDetails: Replacement | undefined;
// UI state
isEditing = false;
isLoading = false;
changeInProgress = false;
private destroy$ = new Subject<void>();
private fb = inject(FormBuilder);
private commonService = inject(CommonService);
private notificationService = inject(NotificationService);
private replacementService = inject(ReplacementService);
private errorHandler = inject(ApiErrorHandlerService);
constructor(
) {
this.extensionForm = this.createForm();
}
ngOnInit(): void {
this.loadCountries();
this.loadExtensionReasons();
if (this.headerid > 0) {
this.isLoading = true;
this.replacementService.getExtensionData(this.headerid).pipe(finalize(() => {
this.isLoading = false;
})).subscribe({
next: (replacementData: Replacement) => {
if (replacementData) {
this.patchFormData(replacementData);
this.replacementDetails = replacementData;
this.completed.emit(this.extensionForm.valid || this.extensionForm.disabled);
this.completed.emit(true);
}
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load extension details');
this.notificationService.showError(errorMessage);
console.error('Error loading extension details:', error);
}
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
patchFormData(data: Replacement): void {
this.extensionForm.patchValue({
portName: data.portName,
portCountry: data.portCountry,
reasonForExtension: data.reasonForExtension,
duration: data.duration
});
if (this.isViewMode) {
this.extensionForm.disable();
}
}
createForm(): FormGroup {
return this.fb.group({
portName: ['', [Validators.required, Validators.maxLength(100)]],
portCountry: ['', Validators.required],
reasonForExtension: ['', Validators.required],
duration: [1, [Validators.required, Validators.min(1), Validators.max(24)]]
});
}
loadCountries(): void {
this.commonService.getCountries()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (countries) => {
this.countries = countries;
},
error: (error) => {
console.error('Failed to load countries', error);
}
});
}
loadExtensionReasons(): void {
this.commonService.getExtensionReasons()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (extensionReasons) => {
this.extensionReasons = extensionReasons;
},
error: (error) => {
console.error('Failed to load extension reasons', error);
}
});
}
onSubmit(): void {
if (this.extensionForm.invalid) {
this.extensionForm.markAllAsTouched();
return;
}
const formData = this.extensionForm.value;
this.changeInProgress = true;
this.replacementService.saveExtensionDetails(this.headerid, formData).pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => {
this.notificationService.showSuccess('Extension details saved successfully');
this.completed.emit(this.extensionForm.valid);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save extension information');
this.notificationService.showError(errorMessage);
console.error('Error saving extension information:', error);
}
});
}
get f() {
return this.extensionForm.controls;
}
getAddressLabel(): string {
if (!this.replacementDetails) {
return 'No contact information available';
}
// Build name parts, filtering out null/undefined/empty strings
const allParts = [
this.replacementDetails.address1,
this.replacementDetails.address2,
this.replacementDetails.city,
this.replacementDetails.state,
this.replacementDetails.country,
this.replacementDetails.zip
].filter(part => part != null && part.trim() !== '');
return allParts.join(', ') || '';
}
}

View File

@ -180,6 +180,11 @@ export class ShippingComponent {
}
}
if (this.applicationType === 'extend' && this.shippingFromDB) {
shippingData.needsLostDocProtection = this.shippingFromDB.needsLostDocProtection;
shippingData.formOfSecurity = this.shippingFromDB.formOfSecurity;
}
this.shippingService.saveShippingDetails(this.headerid, shippingData).pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
@ -483,9 +488,13 @@ export class ShippingComponent {
this.shippingForm.disable();
}
if (this.applicationType === 'duplicate' || this.applicationType === 'additional') {
if (this.applicationType === 'duplicate' || this.applicationType === 'additional'
|| this.applicationType === 'extend') {
this.shippingForm.get('formOfSecurity')?.disable();
this.shippingForm.get('needsLostDocProtection')?.disable();
}
if (this.applicationType === 'duplicate' || this.applicationType === 'additional') {
this.shippingForm.get('needsInsurance')?.disable();
}
}

View File

@ -3,6 +3,15 @@
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
</div>
<mat-accordion class="carnet-headers-align" multi>
<mat-expansion-panel *ngIf="applicationType==='extend'">
<mat-expansion-panel-header>
<mat-panel-title> Extension Application </mat-panel-title>
</mat-expansion-panel-header>
<app-replacement-set [headerid]=" headerid" [isViewMode]="isViewMode">
</app-replacement-set>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Application Name </mat-panel-title>

View File

@ -11,11 +11,12 @@ import { GoodsComponent } from '../goods/goods.component';
import { HolderComponent } from '../holder/holder.component';
import { ShippingComponent } from '../shipping/shipping.component';
import { TravelPlanComponent } from '../travel-plan/travel-plan.component';
import { ReplacementSetComponent } from '../replacement-set/replacement-set.component';
@Component({
selector: 'app-view-carnet',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent],
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent, ReplacementSetComponent],
templateUrl: './view-carnet.component.html',
styleUrl: './view-carnet.component.scss'
})
@ -25,12 +26,20 @@ export class ViewCarnetComponent {
headerid: number = 0;
applicationName: string = '';
userPreferences: UserPreferences;
applicationType: 'new' | 'edit' | 'additional' | 'duplicate' | 'extend' | null = 'edit';
private route = inject(ActivatedRoute);
constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
// set application type based on the route url
const urlSegments = this.route.snapshot.url.map(segment => segment.path);
if (urlSegments.includes('view-extend-carnet')) {
this.applicationType = 'extend';
}
afterNextRender(() => {
// Open all panels
this.accordion().openAll();

View File

@ -0,0 +1,15 @@
export interface Replacement {
replacementCarnetNumber: string;
issueDate: Date;
preparerName: string;
address1: string;
address2?: string | null;
city: string;
state: string;
country: string;
zip: string;
portName: string;
portCountry: string;
reasonForExtension: string;
duration: number;
}

View File

@ -0,0 +1,5 @@
export interface ExtensionReason {
name: string;
id: string;
value: string;
}

View File

@ -0,0 +1,59 @@
import { inject, Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs';
import { UserService } from '../common/user.service';
import { Replacement } from '../../models/carnet/replacement';
@Injectable({
providedIn: 'root'
})
export class ReplacementService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
private http = inject(HttpClient);
private userService = inject(UserService);
getExtensionData(headerid: number): Replacement | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetExtendedSection/${this.userService.getUserSpid()}/${headerid}`).pipe(
map(response => this.mapToExtensionData(response)));
}
private mapToExtensionData(extensionDataCollection: any): Replacement {
let extensionData = extensionDataCollection?.[0];
let replacementData: Replacement = {
replacementCarnetNumber: extensionData.REPLACECARNETNO,
issueDate: extensionData.ISSUEDATE,
preparerName: extensionData.PREPARERNAME,
address1: extensionData.ADDRESS1,
address2: extensionData.ADDRESS2,
city: extensionData.CITY,
state: extensionData.STATE,
country: extensionData.COUNTRY,
zip: extensionData.ZIP,
portName: extensionData.GOODSPORT,
portCountry: extensionData.GOODSCOUNTRY,
reasonForExtension: extensionData.REASONCODE,
duration: extensionData.EXTENSIONPERIOD,
};
return replacementData;
}
saveExtensionDetails(headerid: number, extensionData: Replacement): Observable<any> {
const extensionDetails = {
P_HEADERID: headerid,
P_GOODSPORT: extensionData.portName,
P_GOODSCOUNTRY: extensionData.portCountry,
P_REASONCODE: extensionData.reasonForExtension,
P_EXTENSIONPERIOD: extensionData.duration,
P_USERID: this.userService.getUser(),
P_SPID: this.userService.getUserSpid()
}
return this.http.patch(`${this.apiUrl}/${this.apiDb}/SaveExtensionApplication`, extensionDetails);
}
}

View File

@ -17,6 +17,7 @@ import { DeliveryMethod } from '../../models/delivery-method';
import { FormOfSecurity } from '../../models/formofsecurity';
import { PaymentType } from '../../models/payment-type';
import { UnitOfMeasure } from '../../models/unitofmeasure';
import { ExtensionReason } from '../../models/extension-reason';
@Injectable({
providedIn: 'root'
@ -205,6 +206,18 @@ export class CommonService {
);
}
getExtensionReasons(): Observable<ExtensionReason[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=008&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE,
}))
)
);
}
formatUSDate(datetime: Date): string {
const date = new Date(datetime);
const month = String(date.getUTCMonth() + 1).padStart(2, '0');

View File

@ -198,7 +198,13 @@ export class HomeComponent {
this.storageService.set("carnet-clientid", item.clientid);
this.storageService.set("carnet-locationid", item.locationid);
this.navigateTo(['view-carnet', item.headerid], {
let route: string = 'view-carnet';
if (item.orderType === 'REPLACE') {
route = 'view-extend-carnet';
}
this.navigateTo([route, item.headerid], {
queryParams: { applicationname: item.applicationName }
});
} else {