travel plan and terms & conditions page updates

This commit is contained in:
Cyril Joseph 2025-07-22 21:32:12 -03:00
parent 377b062ace
commit e89f968786
21 changed files with 968 additions and 31 deletions

View File

@ -9,6 +9,7 @@ export const serverRoutes: ServerRoute[] = [
{ path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client }, { path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client },
{ path: ':appId/add-holder', renderMode: RenderMode.Client }, { path: ':appId/add-holder', renderMode: RenderMode.Client },
{ path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client }, { path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client },
{ path: ':appId/terms', renderMode: RenderMode.Client },
{ {
path: '**', path: '**',
renderMode: RenderMode.Prerender renderMode: RenderMode.Prerender

View File

@ -17,6 +17,7 @@ export const routes: Routes = [
{ path: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) }, { path: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
{ path: 'add-holder', loadComponent: () => import('./holder/add/add-holder.component').then(m => m.AddHolderComponent) }, { path: 'add-holder', loadComponent: () => import('./holder/add/add-holder.component').then(m => m.AddHolderComponent) },
{ path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit-holder.component').then(m => m.EditHolderComponent) }, { path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit-holder.component').then(m => m.EditHolderComponent) },
{ path: 'terms', loadComponent: () => import('./carnet/terms-conditions/terms-conditions.component').then(m => m.TermsConditionsComponent) },
{ path: '', redirectTo: 'home', pathMatch: 'full' } { path: '', redirectTo: 'home', pathMatch: 'full' }
], ],
canActivate: [AuthGuard, AppIdGuard] canActivate: [AuthGuard, AppIdGuard]

View File

@ -16,7 +16,7 @@
[editable]="stepsCompleted.applicationDetail"> [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Holder Selection</ng-template> <ng-template matStepLabel>Holder Selection</ng-template>
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid" <app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid"
(updated)="onHolderSelectionUpdated($event)"> [applicationName]="applicationName" (updated)="onHolderSelectionUpdated($event)">
</app-holder> </app-holder>
</mat-step> </mat-step>
@ -41,8 +41,14 @@
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail"> <mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping & Payment</ng-template> <ng-template matStepLabel>Shipping & Payment</ng-template>
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid" <app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid"
[applicationName]="applicationName" [enableSubmitButton]="allSectionsCompleted"
[refreshShippingData]="refreshShippingData"> [refreshShippingData]="refreshShippingData">
</app-shipping> </app-shipping>
</mat-step> </mat-step>
<!-- Icon overrides. -->
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
</mat-stepper> </mat-stepper>
</div> </div>

View File

@ -25,8 +25,10 @@ export class AddCarnetComponent {
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new'; applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
isEditMode = false; isEditMode = false;
headerid: number = 0; headerid: number = 0;
applicationName: string = '';
userPreferences: UserPreferences; userPreferences: UserPreferences;
refreshShippingData: boolean = false; refreshShippingData: boolean = false;
allSectionsCompleted: boolean = false;
// Track completion of each step // Track completion of each step
stepsCompleted = { stepsCompleted = {
@ -47,12 +49,14 @@ export class AddCarnetComponent {
onApplicationDetailCreated(data: { headerid: number, applicationName: string }): void { onApplicationDetailCreated(data: { headerid: number, applicationName: string }): void {
this.headerid = data.headerid; this.headerid = data.headerid;
this.applicationName = data.applicationName;
this.stepsCompleted.applicationDetail = true; this.stepsCompleted.applicationDetail = true;
this.isLinear = false; // Disable linear mode after application detail is created this.isLinear = false; // Disable linear mode after application detail is created
} }
onHolderSelectionSaved(completed: boolean): void { onHolderSelectionSaved(completed: boolean): void {
this.stepsCompleted.holderSelection = completed; this.stepsCompleted.holderSelection = completed;
this.isAllSectionsCompleted();
} }
onHolderSelectionUpdated(updated: boolean): void { onHolderSelectionUpdated(updated: boolean): void {
@ -61,13 +65,22 @@ export class AddCarnetComponent {
onGoodsSectionSaved(completed: boolean): void { onGoodsSectionSaved(completed: boolean): void {
this.stepsCompleted.goodsSection = completed; this.stepsCompleted.goodsSection = completed;
this.isAllSectionsCompleted();
} }
onTravelPlanSaved(completed: boolean): void { onTravelPlanSaved(completed: boolean): void {
this.stepsCompleted.travelPlan = completed; this.stepsCompleted.travelPlan = completed;
this.isAllSectionsCompleted();
} }
onShippingSaved(completed: boolean): void { onShippingSaved(completed: boolean): void {
this.stepsCompleted.shipping = completed; this.stepsCompleted.shipping = completed;
this.isAllSectionsCompleted();
}
isAllSectionsCompleted(): void {
this.allSectionsCompleted = this.stepsCompleted.applicationDetail
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan;
} }
} }

View File

@ -41,8 +41,14 @@
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail"> <mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping & Payment</ng-template> <ng-template matStepLabel>Shipping & Payment</ng-template>
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode" <app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode"
[applicationName]="applicationName" [enableSubmitButton]="allSectionsCompleted"
[refreshShippingData]="refreshShippingData"> [refreshShippingData]="refreshShippingData">
</app-shipping> </app-shipping>
</mat-step> </mat-step>
<!-- Icon overrides. -->
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
</mat-stepper> </mat-stepper>
</div> </div>

View File

@ -28,6 +28,7 @@ export class EditCarnetComponent {
userPreferences: UserPreferences; userPreferences: UserPreferences;
applicationName: string = ''; applicationName: string = '';
refreshShippingData: boolean = false; refreshShippingData: boolean = false;
allSectionsCompleted: boolean = false;
// Track completion of each step // Track completion of each step
stepsCompleted = { stepsCompleted = {
@ -54,6 +55,8 @@ export class EditCarnetComponent {
if (returnTo === 'holder') { if (returnTo === 'holder') {
this.currentStep = 1; this.currentStep = 1;
} else if (returnTo === 'shipping') {
this.currentStep = 4;
} }
} }
@ -63,21 +66,32 @@ export class EditCarnetComponent {
onHolderSelectionSaved(completed: boolean): void { onHolderSelectionSaved(completed: boolean): void {
this.stepsCompleted.holderSelection = completed; this.stepsCompleted.holderSelection = completed;
this.isAllSectionsCompleted();
} }
onGoodsSectionSaved(completed: boolean): void { onGoodsSectionSaved(completed: boolean): void {
this.stepsCompleted.goodsSection = completed; this.stepsCompleted.goodsSection = completed;
this.isAllSectionsCompleted();
} }
onHolderSelectionUpdated(updated: boolean): void { onHolderSelectionUpdated(updated: boolean): void {
this.refreshShippingData = updated; this.refreshShippingData = updated;
this.isAllSectionsCompleted();
} }
onTravelPlanSaved(completed: boolean): void { onTravelPlanSaved(completed: boolean): void {
this.stepsCompleted.travelPlan = completed; this.stepsCompleted.travelPlan = completed;
this.isAllSectionsCompleted();
} }
onShippingSaved(completed: boolean): void { onShippingSaved(completed: boolean): void {
this.stepsCompleted.shipping = completed; this.stepsCompleted.shipping = completed;
this.isAllSectionsCompleted();
}
isAllSectionsCompleted() {
this.allSectionsCompleted = this.stepsCompleted.applicationDetail
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan;
} }
} }

View File

@ -102,6 +102,7 @@ export class GoodsComponent {
next: (goodsDetail: Goods) => { next: (goodsDetail: Goods) => {
if (goodsDetail) { if (goodsDetail) {
this.patchFormData(goodsDetail); this.patchFormData(goodsDetail);
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
} }
this.isLoading = false; this.isLoading = false;
}, },
@ -114,7 +115,6 @@ export class GoodsComponent {
}); });
this.loadGoodsItems(); this.loadGoodsItems();
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
} }
} }
@ -125,6 +125,7 @@ export class GoodsComponent {
next: (items: GoodsItem[]) => { next: (items: GoodsItem[]) => {
this.dataSource.data = items; this.dataSource.data = items;
this.isLoading = false; this.isLoading = false;
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load goods items'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load goods items');

View File

@ -327,6 +327,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-raised-button color="primary" type="button" (click)="submitApplication()"
[disabled]="!enableSubmitButton">
<span>Submit Application</span>
</button>
<button mat-raised-button color="primary" type="submit" [disabled]="shippingForm.invalid || isLoading"> <button mat-raised-button color="primary" type="submit" [disabled]="shippingForm.invalid || isLoading">
<span *ngIf="!isLoading">Save</span> <span *ngIf="!isLoading">Save</span>
<mat-spinner *ngIf="isLoading" diameter="24"></mat-spinner> <mat-spinner *ngIf="isLoading" diameter="24"></mat-spinner>

View File

@ -21,6 +21,8 @@ import { ShippingAddress } from '../../core/models/carnet/shipping-address';
import { ContactDialogComponent } from './contact-dialog.component'; import { ContactDialogComponent } from './contact-dialog.component';
import { HolderService } from '../../core/services/carnet/holder.service'; import { HolderService } from '../../core/services/carnet/holder.service';
import { Holder } from '../../core/models/carnet/holder'; import { Holder } from '../../core/models/carnet/holder';
import { NavigationService } from '../../core/services/common/navigation.service';
import { StorageService } from '../../core/services/common/storage.service';
@Component({ @Component({
selector: 'app-shipping', selector: 'app-shipping',
@ -32,6 +34,9 @@ export class ShippingComponent {
@Input() headerid: number = 0; @Input() headerid: number = 0;
@Input() isEditMode = false; @Input() isEditMode = false;
@Input() refreshShippingData = false; @Input() refreshShippingData = false;
@Input() enableSubmitButton = false;
@Input() applicationName: string = '';
@Output() completed = new EventEmitter<boolean>(); @Output() completed = new EventEmitter<boolean>();
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
@ -42,6 +47,8 @@ export class ShippingComponent {
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
private commonService = inject(CommonService); private commonService = inject(CommonService);
private holderService = inject(HolderService); private holderService = inject(HolderService);
private navigationService = inject(NavigationService);
private storageService = inject(StorageService);
shippingForm: FormGroup; shippingForm: FormGroup;
isLoading = false; isLoading = false;
@ -400,6 +407,8 @@ export class ShippingComponent {
notes: shipping.contact?.notes, notes: shipping.contact?.notes,
}) })
} }
this.completed.emit(this.shippingForm.valid);
} }
updateShippingValidation(shipTo: string): void { updateShippingValidation(shipTo: string): void {
@ -585,4 +594,13 @@ export class ShippingComponent {
} }
}); });
} }
submitApplication(): void {
if (this.headerid) {
let currentApplicationDetails = { headerid: this.headerid, applicationName: this.applicationName }
this.storageService.set("currentapplication", currentApplicationDetails);
}
this.navigationService.navigate(["terms"])
}
} }

View File

@ -0,0 +1,104 @@
<div class="terms-container">
<mat-card class="terms-card">
<mat-card-header>
<mat-card-title>ATA Carnet Terms and Conditions</mat-card-title>
</mat-card-header>
<mat-divider></mat-divider>
<mat-card-content class="terms-content">
<!-- Comprehensive Carnet Terms Content -->
<h3>1. Definitions</h3>
<p><strong>ATA Carnet:</strong> An international customs document that permits the tax-free and duty-free
temporary export and import of goods for up to one year.</p>
<p><strong>Holder:</strong> The individual or entity in whose name the carnet is issued and who is
responsible for complying with all terms.</p>
<p><strong>Goods:</strong> Merchandise, equipment, or products covered by the carnet.</p>
<h3>2. Carnet Usage</h3>
<p>2.1 The carnet may be used for:</p>
<ul>
<li>Commercial samples</li>
<li>Professional equipment</li>
<li>Goods for exhibitions and fairs</li>
<li>Goods for scientific, educational, or cultural purposes</li>
</ul>
<p>2.2 The carnet cannot be used for:</p>
<ul>
<li>Consumable or disposable items</li>
<li>Goods for processing or repair</li>
<li>Goods intended for sale or permanent export</li>
<li>Prohibited or restricted items under any applicable laws</li>
</ul>
<h3>3. Holder Responsibilities</h3>
<p>3.1 The holder must:</p>
<ul>
<li>Present the carnet to customs when crossing borders</li>
<li>Ensure all goods listed in the carnet are returned by the expiration date</li>
<li>Pay all applicable duties and taxes if goods are not re-exported</li>
<li>Notify the issuing association immediately of any lost or stolen carnets</li>
</ul>
<h3>4. Validity Period</h3>
<p>4.1 The carnet is valid for one year from the date of issue.</p>
<p>4.2 Goods must be re-exported before the carnet expires.</p>
<p>4.3 Extensions may be granted in exceptional circumstances with approval from all relevant customs
authorities.</p>
<h3>5. Customs Procedures</h3>
<p>5.1 The holder must present the carnet to customs:</p>
<ul>
<li>When first exporting goods from the home country</li>
<li>When entering each foreign country</li>
<li>When re-exporting goods from each foreign country</li>
<li>When finally re-importing goods to the home country</li>
</ul>
<h3>6. Security Requirements</h3>
<p>6.1 The issuing association may require security up to 40% of the total value of goods.</p>
<p>6.2 Security will be refunded when the carnet is fully discharged.</p>
<p>6.3 The security may be forfeited if terms are violated.</p>
<h3>7. Liability and Insurance</h3>
<p>7.1 The holder is solely responsible for:</p>
<ul>
<li>All customs duties and taxes if goods are not re-exported</li>
<li>Any damage to goods while in transit</li>
<li>Compliance with all import/export regulations</li>
</ul>
<p>7.2 We recommend obtaining comprehensive insurance coverage for all goods.</p>
<h3>8. Fees and Charges</h3>
<p>8.1 The following fees apply:</p>
<ul>
<li>Issuance fee: 1.5% of total goods value (minimum $250)</li>
<li>Processing fee: $75 per application</li>
<li>Amendment fee: $50 per change after issuance</li>
<li>Late discharge fee: $100 if not returned within 30 days of expiration</li>
</ul>
<h3>9. Dispute Resolution</h3>
<p>9.1 Any disputes shall be resolved through arbitration in the jurisdiction of the issuing association.
</p>
<p>9.2 The holder agrees to be bound by the arbitration decision.</p>
<h3>10. Governing Law</h3>
<p>These terms shall be governed by the laws of the country where the carnet was issued.</p>
<mat-checkbox [(ngModel)]="hasReadAllTerms" color="primary" class="read-checkbox">
I have read and agree to all terms and conditions above
</mat-checkbox>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions align="end">
<button mat-raised-button color="primary" (click)="onAccept()" [disabled]="!hasReadAllTerms">
Accept
</button>
<button mat-button (click)="onDecline()">Decline</button>
</mat-card-actions>
</mat-card>
</div>

View File

@ -0,0 +1,83 @@
.terms-container {
display: flex;
justify-content: center;
align-items: center;
}
.terms-card {
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
}
mat-card-header {
justify-content: center;
padding: 16px 0;
}
.terms-content {
flex: 1;
overflow-y: auto;
padding: 16px;
h3 {
color: var(--mat-sys-primary);
margin-top: 24px;
margin-bottom: 12px;
}
p {
padding-left: 16px;
}
ul {
padding-left: 56px;
}
li {
margin-bottom: 8px;
}
strong {
font-weight: 500;
}
}
.read-checkbox {
margin: 24px 0;
display: block;
padding: 8px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
// Custom scrollbar
.terms-content::-webkit-scrollbar {
width: 8px;
}
.terms-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.terms-content::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.terms-content::-webkit-scrollbar-thumb:hover {
background: #555;
}
// Responsive adjustments
@media (max-width: 768px) {
.terms-container {
padding: 12px;
}
.terms-card {
max-height: 95vh;
}
}

View File

@ -0,0 +1,57 @@
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { CommonService } from '../../core/services/common/common.service';
import { NavigationService } from '../../core/services/common/navigation.service';
import { NotificationService } from '../../core/services/common/notification.service';
import { StorageService } from '../../core/services/common/storage.service';
@Component({
selector: 'app-terms-conditions',
imports: [MatCardModule,
MatButtonModule,
MatDividerModule,
MatCheckboxModule,
CommonModule,
FormsModule],
templateUrl: './terms-conditions.component.html',
styleUrl: './terms-conditions.component.scss'
})
export class TermsConditionsComponent {
currentDate = new Date();
hasReadAllTerms = false;
currentApplicationDetails: { headerid: number, applicationName: string } | null = null;
private notificationService = inject(NotificationService);
private commonService = inject(CommonService);
private errorHandler = inject(ApiErrorHandlerService);
private storageService = inject(StorageService);
private navigationService = inject(NavigationService);
constructor() {
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string }>('currentapplication')
}
onAccept(): void {
console.log('Terms accepted');
}
onDecline(): void {
this.storageService.removeItem('currentapplication')
this.navigationService.navigate(["edit-carnet", this.currentApplicationDetails?.headerid],
{
state: { isEditMode: true },
queryParams: {
applicationname: this.currentApplicationDetails?.applicationName,
return: 'shipping'
}
})
}
}

View File

@ -1 +1,149 @@
<p>travel-plan works!</p> <div class="travel-plan-container">
<form [formGroup]="travelForm" (ngSubmit)="onSubmit()">
<!-- USA Entries -->
<div class="form-row">
<mat-form-field appearance="outline" class="usa-entries">
<mat-label>No of times entering & leaving USA</mat-label>
<input matInput type="number" formControlName="usaEntries" min="1" max="99">
<mat-error *ngIf="travelForm.get('usaEntries')?.hasError('required')">
Required
</mat-error>
<mat-error
*ngIf="travelForm.get('usaEntries')?.hasError('min') || travelForm.get('usaEntries')?.hasError('max')">
Must be between 1-99
</mat-error>
</mat-form-field>
</div>
<div class="country-selection-container">
<!-- Available Countries Listbox -->
<div class="available-countries">
<h3>Visit Countries</h3>
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
<mat-list-option *ngFor="let country of countries" [value]="country"
[disabled]="isVisitCountrySelected(country)"
(click)="onAvailableVisitCountrySelection(visitavailableoption)" #visitavailableoption>
{{country.name}}
</mat-list-option>
</mat-selection-list>
</div>
<!-- Controls -->
<div class="controls">
<mat-form-field appearance="outline">
<mat-label>No of times</mat-label>
<input matInput type="number" formControlName="visitsInput" min="1" max="99"
[(ngModel)]="visitsCount">
<mat-error
*ngIf="travelForm.get('visitsInput')?.hasError('min') || travelForm.get('visitsInput')?.hasError('max')">
Must be between 1-99
</mat-error>
</mat-form-field>
<div class="action-buttons">
<button mat-raised-button color="primary" type="button" (click)="addVisitCountry()"
class="icon-end">
<mat-icon>keyboard_double_arrow_right</mat-icon>
<span> Add</span>
</button>
<button mat-raised-button type="button" (click)="removeVisitCountry()">
<mat-icon>keyboard_double_arrow_left</mat-icon>
<span> Remove</span>
</button>
</div>
</div>
<!-- Selected Countries -->
<div class="selected-countries">
<div class="visit-countries">
<h3>Selected</h3>
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
<mat-list-option *ngFor="let country of selectedVisitCountries" [value]="country"
(click)="onVisitCountrySelection(visitoption)" #visitoption>
{{country.name}} ({{country.visits}})
</mat-list-option>
<mat-list-item *ngIf="selectedVisitCountries.length === 0">
<span class="empty-message">No visit countries selected</span>
</mat-list-item>
</mat-selection-list>
</div>
</div>
</div>
<div class="country-selection-container">
<!-- Available Countries Listbox -->
<div class="available-countries">
<h3>Transit Countries</h3>
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
<mat-list-option *ngFor="let country of countries" [value]="country"
[disabled]="isTransitCountrySelected(country)"
(click)="onAvailableTransitCountrySelection(transitavailableoption)" #transitavailableoption>
{{country.name}}
</mat-list-option>
</mat-selection-list>
</div>
<!-- Controls -->
<div class="controls">
<mat-form-field appearance="outline">
<mat-label>No of times</mat-label>
<input matInput type="number" formControlName="transitsInput" min="1" max="99"
[(ngModel)]="transitsCount">
<mat-error
*ngIf="travelForm.get('transitsInput')?.hasError('min') || travelForm.get('transitsInput')?.hasError('max')">
Must be between 1-99
</mat-error>
</mat-form-field>
<div class="action-buttons">
<button mat-raised-button color="primary" type="button" (click)="addTransitCountry()"
class="icon-end">
<mat-icon>keyboard_double_arrow_right</mat-icon>
<span> Add</span>
</button>
<button mat-raised-button type="button" (click)="removeTransitCountry()">
<mat-icon>keyboard_double_arrow_left</mat-icon>
<span> Remove</span>
</button>
</div>
</div>
<!-- Selected Countries -->
<div class="selected-countries">
<div class="transit-countries">
<h3>Selected</h3>
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
<mat-list-option *ngFor="let country of selectedTransitCountries" [value]="country"
(click)="onTransitCountrySelection(transitoption)" #transitoption>
{{country.name}} ({{country.visits}})
</mat-list-option>
<mat-list-item *ngIf="selectedTransitCountries.length === 0">
<span class="empty-message">No transit countries selected</span>
</mat-list-item>
</mat-selection-list>
</div>
</div>
</div>
<div class="form-actions">
<!-- Totals -->
<div class="totals-section">
<div class="total-item">
<span>Total Visits:</span>
<span class="total-value">{{totalVisits}}</span>
</div>
<div class="total-item">
<span>Total Transits:</span>
<span class="total-value">{{totalTransits}}</span>
</div>
</div>
<button mat-raised-button color="primary" type="submit" [disabled]="isLoading">
<span *ngIf="!isLoading">Save</span>
<mat-spinner *ngIf="isLoading" diameter="24"></mat-spinner>
</button>
</div>
</form>
</div>

View File

@ -0,0 +1,127 @@
.travel-plan-container {
padding: 8px;
// max-width: 1200px;
margin: 0 auto;
// .form-row {
// margin-bottom: 24px;
// max-width: 300px;
// }
.usa-entries {
width: 300px;
}
.country-selection-container {
display: flex;
gap: 24px;
margin-bottom: 12px;
@media (max-width: 768px) {
flex-direction: column;
}
}
.available-countries,
.selected-countries {
flex: 1;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 16px;
overflow: hidden;
h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 500;
}
mat-selection-list {
height: 150px;
overflow-y: auto;
padding: 0;
mat-list-item {
.empty-message {
font-style: italic;
}
}
}
}
.controls {
display: flex;
flex-direction: column;
gap: 16px;
width: 150px;
padding: 16px;
.action-buttons {
display: flex;
flex-direction: column;
gap: 8px;
button {
width: 100%;
display: flex;
// gap: 8px;
align-items: center;
}
mat-icon {
margin: 0;
}
button.icon-end {
flex-direction: row-reverse;
}
}
}
.selected-countries {
display: flex;
flex-direction: column;
gap: 16px;
.visit-countries,
.transit-countries {
flex: 1;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
justify-content: space-between;
.totals-section {
display: flex;
gap: 24px;
.total-item {
display: flex;
gap: 8px;
align-items: center;
.total-value {
font-weight: 500;
}
}
}
}
}
@media (max-width: 768px) {
.travel-container {
.form-row {
flex-direction: column;
gap: 8px;
mat-form-field {
width: 100%;
}
}
}
}

View File

@ -1,12 +1,284 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { forkJoin } from 'rxjs';
import { TravelPlanService } from '../../core/services/carnet/travel-plan.service';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service';
import { Country, TravelEntry, TravelPlan } from '../../core/models/carnet/travel-plan';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { MatListModule } from '@angular/material/list';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
@Component({ @Component({
selector: 'app-travel-plan', selector: 'app-travel-plan',
imports: [], imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule, MatListModule],
templateUrl: './travel-plan.component.html', templateUrl: './travel-plan.component.html',
styleUrl: './travel-plan.component.scss' styleUrl: './travel-plan.component.scss'
}) })
export class TravelPlanComponent { export class TravelPlanComponent {
@Input() headerid: number = 0; @Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>(); @Output() completed = new EventEmitter<boolean>();
private fb = inject(FormBuilder);
private dialog = inject(MatDialog);
private travelPlanService = inject(TravelPlanService);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
travelForm: FormGroup;
isLoading = false;
visitsCount = 1;
transitsCount = 1;
countries: Country[] = [];
selectedVisitCountries: TravelEntry[] = [];
selectedTransitCountries: TravelEntry[] = [];
// Currently selected country in the listbox
selectedAvailableVisitCountry: Country | null = null;
selectedAvailableTransitCountry: Country | null = null;
selectedVisitCountry: Country | null = null;
selectedTransitCountry: Country | null = null;
constructor() {
this.travelForm = this.fb.group({
usaEntries: ['', [Validators.required, Validators.min(1), Validators.max(99)]],
visitsInput: ['', [Validators.min(1), Validators.max(99)]],
transitsInput: ['', [Validators.min(1), Validators.max(99)]],
});
}
ngOnInit(): void {
if (this.headerid) {
this.loadTravelPlan();
}
}
onAvailableVisitCountrySelection(event: any) {
this.selectedAvailableVisitCountry = event.value;
}
onVisitCountrySelection(event: any) {
this.selectedVisitCountry = event.value;
}
onAvailableTransitCountrySelection(event: any) {
this.selectedAvailableTransitCountry = event.value;
}
onTransitCountrySelection(event: any) {
this.selectedTransitCountry = event.value;
}
// Add selected country to visits
addVisitCountry(): void {
if (!this.selectedAvailableVisitCountry) return;
if (this.selectedAvailableVisitCountry.actionType === 'FYI') {
this.fyiAction(this.selectedAvailableVisitCountry);
} else if (this.selectedAvailableVisitCountry.actionType === 'WARNING') {
this.warningAction(this.selectedAvailableVisitCountry);
} else if (this.selectedAvailableVisitCountry.actionType === 'ACTION') {
this.takeAction(this.selectedAvailableVisitCountry);
} else {
this.addVisitCountryToCollection();
}
}
addVisitCountryToCollection(): void {
if (!this.selectedAvailableVisitCountry) return;
const existingIndex = this.selectedVisitCountries.findIndex(
c => c.value === this.selectedAvailableVisitCountry!.value
);
if (existingIndex === -1) {
this.selectedVisitCountries.push({
...this.selectedAvailableVisitCountry,
visits: this.visitsCount
});
}
}
fyiAction(country: Country): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '560px',
data: {
title: 'For your information',
message: country.actionMessage,
confirmText: 'Ok',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.addVisitCountryToCollection();
}
});
}
warningAction(country: Country): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '560px',
data: {
title: 'Warning',
message: country.actionMessage,
confirmText: 'Ok',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.addVisitCountryToCollection();
}
});
}
takeAction(country: Country): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '560px',
data: {
title: 'Act',
message: country.actionMessage,
confirmText: 'Ok',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.addVisitCountryToCollection();
}
});
}
// Add selected country to transits
addTransitCountry(): void {
if (!this.selectedAvailableTransitCountry) return;
this.addTransitCountryToCollection();
}
addTransitCountryToCollection(): void {
if (!this.selectedAvailableTransitCountry) return;
const existingIndex = this.selectedTransitCountries.findIndex(
c => c.value === this.selectedAvailableTransitCountry!.value
);
if (existingIndex === -1) {
this.selectedTransitCountries.push({
...this.selectedAvailableTransitCountry,
visits: this.transitsCount
});
}
}
// Remove country from visits
removeVisitCountry(): void {
if (!this.selectedVisitCountry) return;
this.selectedVisitCountries = this.selectedVisitCountries.filter(
c => c.value !== this.selectedVisitCountry!.value
);
}
// Remove country from transits
removeTransitCountry(): void {
if (!this.selectedTransitCountry) return;
this.selectedTransitCountries = this.selectedTransitCountries.filter(
c => c.value !== this.selectedTransitCountry!.value
);
}
// Calculate total visits
get totalVisits(): number {
return this.selectedVisitCountries.reduce((sum, country) => sum + country.visits, 0);
}
// Calculate total transits
get totalTransits(): number {
return this.selectedTransitCountries.reduce((sum, country) => sum + country.visits, 0);
}
// Check if a country is already selected (visit)
isVisitCountrySelected(country: Country): boolean {
return this.selectedVisitCountries.some(c => c.value === country.value)
}
// Check if a country is already selected (transit)
isTransitCountrySelected(country: Country): boolean {
return this.selectedTransitCountries.some(c => c.value === country.value);
}
loadTravelPlan(): void {
this.isLoading = true;
forkJoin({
countries: this.travelPlanService.getCountriesAndMessages(),
travelPlanData: this.travelPlanService.getTravelPlan(this.headerid)
}).subscribe({
next: (results) => {
this.countries = results.countries as Country[];
this.patchTravelPlanData(results.travelPlanData);
this.isLoading = false;
},
error: (error) => {
console.error('Error loading travel plan data', error);
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load travel plan data');
this.notificationService.showError(errorMessage);
this.isLoading = false;
}
});
}
private patchTravelPlanData(plan: TravelPlan): void {
this.selectedVisitCountries = plan?.entries;
this.selectedTransitCountries = plan?.transits;
// Patch form values
this.travelForm.patchValue({
usaEntries: plan?.usSets,
visitsInput: 1,
transitsInput: 1 // Reset to default
});
this.completed.emit(this.travelForm.valid);
}
onSubmit(): void {
if (this.travelForm.invalid) {
this.travelForm.markAllAsTouched();
return;
}
const travelPlan: TravelPlan = {
usSets: this.travelForm.value.usaEntries,
entries: this.selectedVisitCountries,
transits: this.selectedTransitCountries
};
this.isLoading = true;
this.travelPlanService.saveTravelPlan(this.headerid, travelPlan).subscribe({
next: () => {
this.notificationService.showSuccess('Travel plan saved successfully');
this.completed.emit(true);
this.isLoading = false;
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save travel plan');
this.notificationService.showError(errorMessage);
console.error('Error saving travel plan data : ', error);
this.isLoading = false;
}
});
}
} }

View File

@ -1,2 +1,17 @@
export interface TravelPlan { export interface TravelPlan {
usSets?: number;
entries: TravelEntry[];
transits: TravelEntry[];
}
export interface Country {
name?: string;
value: string;
actionMessage?: string | null;
actionType?: string | null;
action?: string | null;
}
export interface TravelEntry extends Country {
visits: number;
} }

View File

@ -3,7 +3,7 @@ import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
import { ApplicationDetail } from '../../models/carnet/application-detail'; import { ApplicationDetail } from '../../models/carnet/application-detail';
import { filter, map, Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -15,21 +15,6 @@ export class ApplicationDetailService {
private http = inject(HttpClient); private http = inject(HttpClient);
private userService = inject(UserService); private userService = inject(UserService);
// getApplicationDetailsById(id: number): ApplicationDetail | any {
// return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid/${this.userService.getUserSpid()}/${id}`).pipe(
// filter(response => response.length > 0),
// map(response => this.mapToApplicationDetail(response?.[0])));
// }
// private mapToApplicationDetail(applicationDetails: any): ApplicationDetail {
// return {
// clientid: applicationDetails.CLIENTID,
// spid: applicationDetails.SPID,
// name: applicationDetails.P_APPLICATIONNAME,
// applicationId: applicationDetails.P_HEADERID
// };
// }
createApplicationDetails(data: ApplicationDetail): Observable<any> { createApplicationDetails(data: ApplicationDetail): Observable<any> {
const userDetails = this.userService.getUserDetails(); const userDetails = this.userService.getUserDetails();

View File

@ -3,7 +3,7 @@ import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
import { Goods, GoodsItem } from '../../models/carnet/goods'; import { Goods, GoodsItem } from '../../models/carnet/goods';
import { filter, map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -17,8 +17,7 @@ export class GoodsService {
getGoodDetailsByHeaderId(headerid: number): Goods | any { getGoodDetailsByHeaderId(headerid: number): Goods | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
filter(response => response.length > 0), map(response => this.mapToGoods(response)));
map(response => this.mapToGoods(response?.[0])));
} }
private mapToGoods(goodDetails: any): Goods { private mapToGoods(goodDetails: any): Goods {

View File

@ -3,7 +3,7 @@ import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
import { Holder } from '../../models/carnet/holder'; import { Holder } from '../../models/carnet/holder';
import { filter, map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -17,8 +17,7 @@ export class HolderService {
getHolder(id: number): Observable<Holder> { getHolder(id: number): Observable<Holder> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${id}`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${id}`).pipe(
filter(response => response.length > 0), map(response => this.mapToHolder(response)));
map(response => this.mapToHolder(response?.[0])));
} }
private mapToHolder(holder: any): Holder { private mapToHolder(holder: any): Holder {

View File

@ -19,8 +19,7 @@ export class ShippingService {
getShippingData(headerid: number): Shipping | any { getShippingData(headerid: number): Shipping | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetShipPaymentDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetShipPaymentDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
filter(response => response.length > 0), map(response => this.mapToShippingData(response)));
map(response => this.mapToShippingData(response?.[0])));
} }
private mapToShippingData(shippingDetails: any): Shipping { private mapToShippingData(shippingDetails: any): Shipping {

View File

@ -1,9 +1,93 @@
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable, map } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { Country, TravelPlan } from '../../models/carnet/travel-plan';
import { UserService } from '../common/user.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TravelPlanService { export class TravelPlanService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor() { } private http = inject(HttpClient);
private userService = inject(UserService);
getCountriesAndMessages(): Observable<Country[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCountriesAndMessages`).pipe(
map((response) =>
response.map((item) => ({
name: item.COUNTRYNAME,
value: item.COUNTRYCODE,
actionMessage: item.COUNTRYMESSAGE,
actionType: item.ADDLPARAMVALUE1,
action: item.ADDLPARAMVALUE2
}))
)
);
}
getTravelPlan(headerid: number): Observable<TravelPlan> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCountryDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
map(response => this.mapToTravelPlanData(response)));
}
private mapToTravelPlanData(travelPlanDetails: any): TravelPlan {
if (!travelPlanDetails?.P_COUNTRYTABLE && !travelPlanDetails?.NOOFUSSETS) {
return {
entries: [],
transits: []
} as TravelPlan;
}
const countryTable: any[] = travelPlanDetails.P_COUNTRYTABLE;
let travelPlanData: TravelPlan = {
usSets: travelPlanDetails.NOOFUSSETS,
entries: countryTable
.filter((item) => item.VISISTTRANSITIND === 'V')
.map(item => ({
name: item.COUNTRYNAME,
value: item.COUNTRYCODE,
visits: item.NOOFTIMESENTLEAVE
})),
transits: countryTable
.filter(item => item.VISISTTRANSITIND === 'T')
.map(item => ({
name: item.COUNTRYNAME,
value: item.COUNTRYCODE,
visits: item.NOOFTIMESENTLEAVE
}))
};
return travelPlanData;
}
saveTravelPlan(headerid: number, travelPlan: TravelPlan): Observable<TravelPlan> {
const countryTable: any[] = [
...travelPlan.entries.map(entry => ({
P_VISISTTRANSITIND: 'V' as const,
COUNTRYCODE: entry.value,
NOOFTIMESENTLEAVE: entry.visits
})),
...travelPlan.transits.map(transit => ({
P_VISISTTRANSITIND: 'T' as const,
COUNTRYCODE: transit.value,
NOOFTIMESENTLEAVE: transit.visits
}))
];
let travelPlanData = {
P_HEADERID: headerid,
P_USSETS: travelPlan.usSets,
P_COUNTRYTABLE: countryTable,
P_USERID: this.userService.getUser()
}
return this.http.post<TravelPlan>(`${this.apiUrl}/${this.apiDb}/AddCountries`, travelPlanData);
}
} }