feedback updates

This commit is contained in:
Cyril Joseph 2025-09-07 21:55:23 -03:00
parent 0c863458d3
commit 23b6d55261
22 changed files with 852 additions and 39 deletions

View File

@ -9,6 +9,7 @@ import { NotificationService } from '../../core/services/common/notification.ser
import { StorageService } from '../../core/services/common/storage.service'; import { StorageService } from '../../core/services/common/storage.service';
import { finalize } from 'rxjs'; import { finalize } from 'rxjs';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { PaymentDetail } from '../../core/models/carnet/payment-details';
// This is necessary to tell TypeScript that a global 'paypal' object exists. // This is necessary to tell TypeScript that a global 'paypal' object exists.
declare var paypal: any; declare var paypal: any;
@ -30,10 +31,7 @@ export class CheckoutComponent {
@ViewChild('paypalcontrol', { static: true }) paypalElement!: ElementRef; @ViewChild('paypalcontrol', { static: true }) paypalElement!: ElementRef;
orderDetails = { orderDetails: PaymentDetail = {};
price: '',
description: '',
};
orderTotal = 0.00; orderTotal = 0.00;
currency = 'USD'; currency = 'USD';
@ -47,6 +45,8 @@ export class CheckoutComponent {
constructor() { constructor() {
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication') this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication')
this.orderDetails.headerid = this.currentApplicationDetails?.headerid;
this.orderDetails.applicationName = this.currentApplicationDetails?.applicationName;
this.orderDetails.description = ` 'Carnet Application - ${this.currentApplicationDetails?.headerid} - ${this.currentApplicationDetails?.applicationName}` this.orderDetails.description = ` 'Carnet Application - ${this.currentApplicationDetails?.headerid} - ${this.currentApplicationDetails?.applicationName}`
} }
@ -99,7 +99,7 @@ export class CheckoutComponent {
try { try {
// Call the createOrder method from your service // Call the createOrder method from your service
const orderId = await this.paymentService.createOrder(this.orderDetails); const orderId = await this.paymentService.createOrder(this.orderDetails);
// console.log('Order ID created by server:', orderId); this.orderDetails.orderId = orderId;
return orderId; return orderId;
} catch (error) { } catch (error) {
this.notificationService.showError('Failed to initiate the payment'); this.notificationService.showError('Failed to initiate the payment');
@ -112,7 +112,7 @@ export class CheckoutComponent {
onApprove: async (data: any, actions: any) => { onApprove: async (data: any, actions: any) => {
try { try {
// data.orderID is the ID of the transaction from PayPal // data.orderID is the ID of the transaction from PayPal
const captureDetails = await this.paymentService.completePayment(data.orderID); await this.paymentService.completePayment(this.orderDetails);
// You can now redirect the user or update the UI // You can now redirect the user or update the UI
this.changeInProgress = true; this.changeInProgress = true;
@ -139,11 +139,20 @@ export class CheckoutComponent {
// 3. Handle errors // 3. Handle errors
onError: (error: any) => { onError: (error: any) => {
this.notificationService.showError('Payment transaction failed'); this.notificationService.showError('Payment transaction failed');
this.orderDetails.paymentStatus = 'Failed';
this.orderDetails.paymentErrorDetails = JSON.stringify(error);
this.paymentService.logTransactionDetails(this.orderDetails);
console.error('An error occurred during the transaction:', error); console.error('An error occurred during the transaction:', error);
}, },
// 4. Handle cancellation // 4. Handle cancellation
onCancel: (data: any) => { onCancel: (data: any) => {
this.orderDetails.paymentStatus = 'Cancelled';
if (this.orderDetails.orderId) {
this.paymentService.logTransactionDetails(this.orderDetails);
}
this.onCancel(); this.onCancel();
} }
}).render(this.paypalElement.nativeElement) }).render(this.paypalElement.nativeElement)

View File

@ -0,0 +1,9 @@
export interface PaymentDetail {
headerid?: number;
applicationName?: string;
price?: string;
description?: string;
orderId?: string;
paymentStatus?: string;
paymentErrorDetails?: string;
}

View File

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

View File

@ -0,0 +1,12 @@
export interface CargoRate {
id: number,
spid: number,
startSets: number,
endSets: number,
carnetType: string,
effectiveDate: Date,
rate: number,
dateCreated?: Date | null;
createdBy?: string | null;
expired?: boolean;
}

View File

@ -5,6 +5,7 @@ export interface SecurityDeposit {
specialCommodity: string; specialCommodity: string;
specialCountry: string; specialCountry: string;
rate: number; rate: number;
rateInPercentage: number;
effectiveDate: Date; effectiveDate: Date;
spid: number; spid: number;
dateCreated?: Date | null; dateCreated?: Date | null;

View File

@ -1,9 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class InsuranceService {
constructor() { }
}

View File

@ -2,6 +2,8 @@ import { inject, Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { PaymentDetail } from '../../models/carnet/payment-details';
import { UserService } from '../common/user.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -11,11 +13,14 @@ export class PaymentService {
private apiDb = environment.apiDb; private apiDb = environment.apiDb;
private http = inject(HttpClient); private http = inject(HttpClient);
private userService = inject(UserService);
createOrder(orderDetails: any): Promise<string> { createOrder(paymentDetails: PaymentDetail): Promise<string> {
const data = { const data = {
P_PRICE: orderDetails.price, P_APPLICATIONNAME: paymentDetails.applicationName,
P_DESCRIPTION: orderDetails.description P_PRICE: paymentDetails.price,
P_DESCRIPTION: paymentDetails.description,
P_USERID: this.userService.getUser()
} }
return firstValueFrom( return firstValueFrom(
@ -23,12 +28,29 @@ export class PaymentService {
).then(order => order.id); ).then(order => order.id);
} }
completePayment(orderId: string): Promise<any> { completePayment(paymentDetails: PaymentDetail): Promise<any> {
const data = { const data = {
P_ORDERID: orderId P_ORDERID: paymentDetails.orderId,
P_APPLICATIONNAME: paymentDetails.applicationName,
P_PRICE: paymentDetails.price,
P_USERID: this.userService.getUser()
} }
return firstValueFrom( return firstValueFrom(
this.http.post(`${this.apiUrl}/oracle/CompletePayment`, data)); this.http.post(`${this.apiUrl}/${this.apiDb}/CompletePayment`, data));
}
logTransactionDetails(paymentDetails: PaymentDetail): Promise<any> {
const data = {
P_APPLICATIONNAME: paymentDetails.applicationName,
P_ORDERID: paymentDetails.orderId,
P_PAYMENTAMOUNT: paymentDetails.price,
P_STATUS: paymentDetails.paymentStatus,
P_USERID: this.userService.getUser(),
P_PAYMENTERROR: paymentDetails.paymentErrorDetails
}
return firstValueFrom(
this.http.post(`${this.apiUrl}/${this.apiDb}/SaveHistory`, data));
} }
} }

View File

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

View File

@ -34,7 +34,7 @@ export class BasicDetailService {
carnetIssuingRegion: basicDetails.ISSUINGREGION, carnetIssuingRegion: basicDetails.ISSUINGREGION,
revenueLocation: basicDetails.REVENUELOCATION, revenueLocation: basicDetails.REVENUELOCATION,
zip: basicDetails.ZIP, zip: basicDetails.ZIP,
industryType: ""// basicDetails.INDUSTRYTYPE industryType: basicDetails.INDUSTRYTYPE
}; };
} }

View File

@ -0,0 +1,70 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { CommonService } from '../common/common.service';
import { UserService } from '../common/user.service';
import { CargoRate } from '../../models/service-provider/cargo-rate';
@Injectable({
providedIn: 'root'
})
export class CargoRateService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
private http = inject(HttpClient);
private userService = inject(UserService);
private commonService = inject(CommonService);
getCargoRates(spid: number): Observable<CargoRate[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCargoRates/${spid}/ACTIVE`).pipe(
map(response => this.mapToCargoRates(response)));
}
private mapToCargoRates(data: any[]): CargoRate[] {
return data.map(item => ({
id: item.CARGORATESETUPID,
spid: item.SPID,
startSets: item.STARTSETS,
endSets: item.ENDSETS,
carnetType: item.CARNETTYPE,
effectiveDate: item.EFFDATE,
rate: item.RATE,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
}));
}
addCargoRate(spid: number, data: CargoRate): Observable<any> {
const cargoRate = {
P_SPID: spid,
P_STARTSETS: data.startSets,
P_ENDSETS: data.endSets,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_CARNETTYPE: data.carnetType,
P_RATE: data.rate,
P_USERID: this.userService.getUser()
}
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateCargoRate`, cargoRate);
}
updateCargoRate(id: number, data: CargoRate): Observable<any> {
const cargoRate = {
P_CARGORATESETUPID: id,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_RATE: data.rate,
P_USERID: this.userService.getUser()
}
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}//UpdateCargoRate`, cargoRate);
}
// deleteCargo(id: string): Observable<void> {
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
// }
}

View File

@ -30,6 +30,7 @@ export class SecurityDepositService {
specialCommodity: item.SPCLCOMMODITY, specialCommodity: item.SPCLCOMMODITY,
specialCountry: item.SPCLCOUNTRY, specialCountry: item.SPCLCOUNTRY,
rate: item.RATE, rate: item.RATE,
rateInPercentage: item.RATE * 100,
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
spid: item.SPID, spid: item.SPID,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
@ -48,6 +49,7 @@ export class SecurityDepositService {
P_SPCLCOMMODITY: data.specialCommodity, P_SPCLCOMMODITY: data.specialCommodity,
P_SPCLCOUNTRY: data.specialCountry, P_SPCLCOUNTRY: data.specialCountry,
P_RATE: data.rate, P_RATE: data.rate,
P_PCT_VALUE: data.rateInPercentage.toString(),
P_USERID: this.userService.getUser() P_USERID: this.userService.getUser()
} }
@ -60,6 +62,7 @@ export class SecurityDepositService {
P_BONDRATESETUPID: id, P_BONDRATESETUPID: id,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate), P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_RATE: data.rate, P_RATE: data.rate,
P_PCT_VALUE: data.rateInPercentage.toString(),
P_USERID: this.userService.getUser() P_USERID: this.userService.getUser()
} }

View File

@ -86,18 +86,18 @@
</button> </button>
<button mat-icon-button color="primary" *ngIf="isParamRecord <button mat-icon-button color="primary" *ngIf="isParamRecord
&& allowEdit" && allowEdit" (click)="onEditParam(client)" [matTooltip]="'edit'">
(click)="onEditParam(client)" [matTooltip]="'edit'">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button color="warn" <button mat-icon-button color="warn"
*ngIf="isParamRecord && (client.inactiveCodeFlag === 'N' || !client.inactiveCodeFlag)" *ngIf="isParamRecord && (client.inactiveCodeFlag === 'N' || !client.inactiveCodeFlag) && allowEdit"
matTooltip="Inactivate" (click)="inActivateParamRecord(client.paramId)"> matTooltip="Inactivate" (click)="inActivateParamRecord(client.paramId)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<button mat-icon-button color="warn" *ngIf="(isParamRecord) && client.inactiveCodeFlag === 'Y'" <button mat-icon-button color="warn"
*ngIf="(isParamRecord) && client.inactiveCodeFlag === 'Y' && allowEdit"
matTooltip="Reactivate" (click)="reActivateParamRecord(client.paramId)"> matTooltip="Reactivate" (click)="reActivateParamRecord(client.paramId)">
<mat-icon>delete_outline</mat-icon> <mat-icon>delete_outline</mat-icon>
</button> </button>

View File

@ -83,5 +83,14 @@
[userPreferences]="userPreferences"></app-security-deposit> [userPreferences]="userPreferences"></app-security-deposit>
</mat-step> </mat-step>
<!-- Cargo Rate Step -->
<mat-step [editable]="!!serviceProviderId && securityDepositCompleted" [completed]="cargoRateCompleted">
<ng-template matStepLabel>Cargo Rate Setup</ng-template>
<app-cargo-rate *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasCargoRates)="onCargoRateSaved($event)" [userPreferences]="userPreferences">
</app-cargo-rate>
</mat-step>
</mat-stepper> </mat-stepper>
</div> </div>

View File

@ -13,11 +13,12 @@ import { SecurityDepositComponent } from '../security-deposit/security-deposit.c
import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component"; import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component";
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service'; import { UserPreferencesService } from '../../core/services/user-preference.service';
import { CargoRateComponent } from '../cargo-rate/cargo-rate.component';
@Component({ @Component({
selector: 'app-add-service-provider', selector: 'app-add-service-provider',
imports: [BasicDetailsComponent, AngularMaterialModule, CommonModule, ContactsComponent, CarnetSequenceComponent, imports: [BasicDetailsComponent, AngularMaterialModule, CommonModule, ContactsComponent, CarnetSequenceComponent,
CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, ContinuationSheetFeeComponent], CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, ContinuationSheetFeeComponent, CargoRateComponent],
templateUrl: './add-service-provider.component.html', templateUrl: './add-service-provider.component.html',
styleUrl: './add-service-provider.component.scss' styleUrl: './add-service-provider.component.scss'
}) })
@ -37,6 +38,7 @@ export class AddServiceProviderComponent {
continuationSheetFeeCompleted: boolean = false; continuationSheetFeeCompleted: boolean = false;
expeditedFeeCompleted: boolean = false; expeditedFeeCompleted: boolean = false;
securityDepositCompleted: boolean = false; securityDepositCompleted: boolean = false;
cargoRateCompleted: boolean = false;
constructor(userPrefenceService: UserPreferencesService) { constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences(); this.userPreferences = userPrefenceService.getPreferences();
@ -79,6 +81,10 @@ export class AddServiceProviderComponent {
this.securityDepositCompleted = event; this.securityDepositCompleted = event;
} }
onCargoRateSaved(event: boolean): void {
this.cargoRateCompleted = event;
}
onStepChange(event: StepperSelectionEvent): void { onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex; this.currentStep = event.selectedIndex;
} }

View File

@ -0,0 +1,192 @@
<div class="cargorate-container">
<div class="actions-bar">
<mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewCargoRate()">
<mat-icon>add</mat-icon> Add New Cargo Rate
</button>
</div>
<div class="table-container mat-elevation-z8">
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<table mat-table [dataSource]="dataSource" matSort>
<!-- Carnet Type Column -->
<ng-container matColumnDef="carnetType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Type</th>
<td mat-cell *matCellDef="let item">
{{ getOptionLabel(carnetTypes, item.carnetType) }}
</td>
</ng-container>
<!-- Start Sets Column -->
<ng-container matColumnDef="startSets">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Start Sets</th>
<td mat-cell *matCellDef="let item">{{ item.startSets }}</td>
</ng-container>
<!-- End Sets Column -->
<ng-container matColumnDef="endSets">
<th mat-header-cell *matHeaderCellDef mat-sort-header>End Sets</th>
<td mat-cell *matCellDef="let item">{{ item.endSets }}</td>
</ng-container>
<!-- Rate Column -->
<ng-container matColumnDef="rate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Rate</th>
<td mat-cell *matCellDef="let item">{{ item.rate | currency }}</td>
</ng-container>
<!-- Effective Date Column -->
<ng-container matColumnDef="effectiveDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th>
<td mat-cell *matCellDef="let item">
{{ item.effectiveDate | date:'mediumDate':'UTC' }}
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let item">
<button mat-icon-button color="primary" (click)="editCargoRate(item)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<!-- <button mat-icon-button color="warn" (click)="deleteCargoRate(item.id)" matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button> -->
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr matNoDataRow *matNoDataRow>
<td [colSpan]="displayedColumns.length" class="no-data-message">
<mat-icon>info</mat-icon>
<span>No cargo rate setups available</span>
</td>
</tr>
</table>
<mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- CargoRate Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="cargoRateForm" (ngSubmit)="saveCargoRate()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Cargo Rate Setup' : 'Add New Cargo Rate Setup' }}</h3>
</div>
<div class="form-row">
<mat-label>Carnet Type</mat-label>
<mat-radio-group formControlName="carnetType" required class="horizontal-group">
<mat-radio-button *ngFor="let type of carnetTypes" [value]="type.value" class="radio-button">
{{ type.label }}
</mat-radio-button>
</mat-radio-group>
<mat-error *ngIf="cargoRateForm.get('carnetType')?.errors?.['required']">
Carnet type is required
</mat-error>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Start Sets</mat-label>
<input matInput formControlName="startSets" type="number" required>
<mat-error *ngIf="cargoRateForm.get('startSets')?.errors?.['required']">
Start sets is required
</mat-error>
<mat-error *ngIf="cargoRateForm.get('startSets')?.errors?.['pattern']">
Must be a valid number
</mat-error>
<mat-error *ngIf="cargoRateForm.get('startSets')?.errors?.['min']">
Must be greater than 0
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End Sets</mat-label>
<input matInput formControlName="endSets" type="number" required>
<mat-error *ngIf="cargoRateForm.get('endSets')?.errors?.['required']">
End sets is required
</mat-error>
<mat-error *ngIf="cargoRateForm.get('endSets')?.errors?.['pattern']">
Must be a valid number
</mat-error>
<mat-error *ngIf="cargoRateForm.get('endSets')?.errors?.['min']">
Must be greater than 0
</mat-error>
</mat-form-field>
</div>
<mat-error *ngIf="cargoRateForm?.errors?.['invalidRange']" class="form-error">
End sets must be greater than start sets
</mat-error>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Rate</mat-label>
<input matInput formControlName="rate" type="number" step="1" required>
<span class="dollar-prefix" matPrefix>$&nbsp;</span>
<mat-error *ngIf="cargoRateForm.get('rate')?.errors?.['required']">
Rate is required
</mat-error>
<mat-error *ngIf="cargoRateForm.get('rate')?.errors?.['pattern']">
Must be a valid dollar amount
</mat-error>
<mat-error *ngIf="cargoRateForm.get('rate')?.errors?.['min']">
Must be 0 or greater
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Effective Date</mat-label>
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
<mat-error *ngIf="cargoRateForm.get('effectiveDate')?.errors?.['required']">
Effective date is required
</mat-error>
</mat-form-field>
</div>
<div *ngIf="isEditing" class="readonly-section">
<div class="readonly-fields">
<div class="field-column">
<!-- Last Changed By -->
<div class="readonly-field">
<label>Last Changed By</label>
<div class="readonly-value">
{{readOnlyFields.lastChangedBy || 'N/A'}}
</div>
</div>
</div>
<div class="field-column">
<!-- Last Changed Date -->
<div class="readonly-field">
<label>Last Changed Date</label>
<div class="readonly-value">
{{(readOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
</div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit"
[disabled]="cargoRateForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }}
</button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,191 @@
.cargorate-container {
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button {
float: right;
}
}
.table-container {
position: relative;
overflow: auto;
border-radius: 8px;
.loading-shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.7);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
mat-table {
width: 100%;
mat-icon {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
.mat-column-actions {
width: 120px;
text-align: center;
}
}
.no-data-message {
text-align: center;
padding: 0.9rem;
color: rgba(0, 0, 0, 0.54);
mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
margin-bottom: -3px;
}
}
mat-paginator {
border-top: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 0 0 8px 8px;
padding-top: 4px;
}
}
.form-container {
background-color: white;
padding: 24px;
border-radius: 8px;
margin-top: 16px;
.form-header {
// margin-bottom: 24px;
h3 {
margin: 0;
color: var(--mat-sys-primary);
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
align-items: start;
mat-form-field {
flex: 1;
}
mat-label {
font-size: 0.875rem;
}
}
.form-error {
margin-top: -16px;
margin-bottom: 8px;
display: block;
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
font-size: 12px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 16px;
}
.horizontal-group {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
::ng-deep .mat-radio-button.mat-accent .mat-radio-outer-circle {
border-color: #3f51b5;
}
::ng-deep .mat-radio-button.mat-accent .mat-radio-inner-circle {
background-color: #3f51b5;
}
.dollar-prefix {
padding-left: 4px;
}
.readonly-section {
padding-top: 0.5rem;
border-top: 1px solid #eee;
.readonly-fields {
display: flex;
gap: 2rem;
.field-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
}
.readonly-field {
label {
display: block;
font-size: 0.875rem;
color: #666;
margin-bottom: 0.25rem;
}
.readonly-value {
padding: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.cargorate-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,244 @@
import { Component, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator';
import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service';
import { finalize } from 'rxjs';
import { CargoRate } from '../../core/models/service-provider/cargo-rate';
import { CargoRateService } from '../../core/services/service-provider/cargo-rate.service';
@Component({
selector: 'app-cargo-rate',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './cargo-rate.component.html',
styleUrl: './cargo-rate.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class CargoRateComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['carnetType', 'startSets', 'endSets', 'rate', 'effectiveDate', 'actions'];
dataSource = new MatTableDataSource<any>();
cargoRateForm: FormGroup;
isEditing = false;
currentCargoRateId: number | null = null;
isLoading = false;
changeInProgress = false;
showForm = false;
showExpiredRecords = false;
cargoRates: CargoRate[] = [];
readOnlyFields: any = {
lastChangedDate: null,
lastChangedBy: null
};
carnetTypes = [
{ label: 'Original', value: 'ORIGINAL' },
{ label: 'Re-order', value: 'REORDER' },
{ label: 'Replacement', value: 'REPLACE' }
];
@Input() isEditMode = false;
@Input() spid: number = 0;
@Input() userPreferences!: UserPreferences;
@Output() hasCargoRates = new EventEmitter<boolean>();
private fb = inject(FormBuilder);
private cargoRateService = inject(CargoRateService);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
constructor() {
this.cargoRateForm = this.fb.group({
carnetType: ['ORIGINAL', Validators.required],
startSets: ['', [
Validators.required,
Validators.pattern('^[0-9]*$'),
Validators.min(1)
]],
endSets: ['', [
Validators.required,
Validators.pattern('^[0-9]*$'),
Validators.min(1)
]],
rate: ['', [
Validators.required,
Validators.pattern(/^\d+\.?\d{0,2}$/),
Validators.min(0)
]],
effectiveDate: ['', Validators.required]
}, { validator: this.validateSetsRange });
}
ngOnInit(): void {
this.loadCargoRates();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
private validateSetsRange(group: FormGroup): { [key: string]: any } | null {
const start = +group.get('startSets')?.value;
const end = +group.get('endSets')?.value;
return start && end && start >= end ? { invalidRange: true } : null;
}
loadCargoRates(): void {
if (!this.spid) return;
this.isLoading = true;
this.cargoRateService.getCargoRates(this.spid).pipe(finalize(() => {
this.isLoading = false;
}))
.subscribe({
next: (
cargoRates: CargoRate[]) => {
this.cargoRates = cargoRates;
this.dataSource.data = this.cargoRates;
this.renderRecods();
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load cargo rates');
this.notificationService.showError(errorMessage);
console.error('Error loading cargo rates:', error);
}
});
}
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.cargoRates.filter(record => record.expired);
} else {
this.dataSource.data = this.cargoRates.filter(record => !record.expired);
}
}
// applyFilter(event: Event): void {
// const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase();
// if (this.dataSource.paginator) {
// this.dataSource.paginator.firstPage();
// }
// }
addNewCargoRate(): void {
this.showForm = true;
this.isEditing = false;
this.currentCargoRateId = null;
this.cargoRateForm.reset({
carnetType: 'ORIGINAL'
});
this.cargoRateForm.get('carnetType')?.enable();
this.cargoRateForm.get('startSets')?.enable();
this.cargoRateForm.get('endSets')?.enable();
}
editCargoRate(cargoRate: any): void {
this.showForm = true;
this.isEditing = true;
this.currentCargoRateId = cargoRate.id;
this.cargoRateForm.patchValue({
carnetType: cargoRate.carnetType,
startSets: cargoRate.startSets,
endSets: cargoRate.endSets,
rate: cargoRate.rate,
effectiveDate: new Date(cargoRate.effectiveDate)
});
this.readOnlyFields.lastChangedDate = cargoRate.dateCreated;
this.readOnlyFields.lastChangedBy = cargoRate.createdBy;
this.cargoRateForm.get('carnetType')?.disable();
this.cargoRateForm.get('startSets')?.disable();
this.cargoRateForm.get('endSets')?.disable();
}
saveCargoRate(): void {
if (this.cargoRateForm.invalid) {
this.cargoRateForm.markAllAsTouched();
return;
}
const cargoRateData = {
...this.cargoRateForm.value,
spid: this.spid
};
const saveObservable = this.isEditing && this.currentCargoRateId
? this.cargoRateService.updateCargoRate(this.currentCargoRateId, cargoRateData)
: this.cargoRateService.addCargoRate(this.spid, cargoRateData);
this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => {
this.notificationService.showSuccess(`Cargo Rate ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadCargoRates();
this.cancelEdit();
this.hasCargoRates.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} cargo rate`);
this.notificationService.showError(errorMessage);
console.error('Error saving cargo rate:', error);
}
});
}
// deleteCargoRate(cargoRateId: string): void {
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// width: '350px',
// data: {
// title: 'Confirm Delete',
// message: 'Are you sure you want to delete this cargoRate setup?',
// confirmText: 'Delete',
// cancelText: 'Cancel'
// }
// });
// dialogRef.afterClosed().subscribe(result => {
// if (result) {
// this.cargoRateService.deleteCargoRate(cargoRateId).subscribe({
// next: () => {
// this.notificationService.showSuccess('CargoRate deleted successfully');
// this.loadCargoRates();
// },
// error: (error) => {
// this.notificationService.showError('Failed to delete cargoRate');
// }
// });
// }
// });
// }
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentCargoRateId = null;
this.cargoRateForm.reset({
carnetType: 'ORIGINAL'
});
}
getOptionLabel(options: any[], value: string): string {
return options.find(opt => opt.value === value)?.label || value;
}
}

View File

@ -73,4 +73,12 @@
<app-security-deposit [spid]="spid" [isEditMode]="isEditMode" <app-security-deposit [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-security-deposit> [userPreferences]="userPreferences"></app-security-deposit>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Cargo Rate Setup </mat-panel-title>
</mat-expansion-panel-header>
<app-cargo-rate [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences">
</app-cargo-rate>
</mat-expansion-panel>
</mat-accordion> </mat-accordion>

View File

@ -14,10 +14,11 @@ import { SecurityDepositComponent } from "../security-deposit/security-deposit.c
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { UserPreferencesService } from '../../core/services/user-preference.service'; import { UserPreferencesService } from '../../core/services/user-preference.service';
import { CargoRateComponent } from '../cargo-rate/cargo-rate.component';
@Component({ @Component({
selector: 'app-edit-service-provider', selector: 'app-edit-service-provider',
imports: [AngularMaterialModule, BasicDetailsComponent, ContactsComponent, CarnetSequenceComponent, CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ContinuationSheetFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, CommonModule], imports: [AngularMaterialModule, BasicDetailsComponent, ContactsComponent, CarnetSequenceComponent, CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ContinuationSheetFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, CommonModule, CargoRateComponent],
templateUrl: './edit-service-provider.component.html', templateUrl: './edit-service-provider.component.html',
styleUrl: './edit-service-provider.component.scss' styleUrl: './edit-service-provider.component.scss'
}) })

View File

@ -48,6 +48,12 @@
<td mat-cell *matCellDef="let deposit">{{ deposit.rate | currency }}</td> <td mat-cell *matCellDef="let deposit">{{ deposit.rate | currency }}</td>
</ng-container> </ng-container>
<!-- Rate In Percentage Column -->
<ng-container matColumnDef="rateInPercentage">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Rate In Percentage</th>
<td mat-cell *matCellDef="let deposit">{{ deposit.rateInPercentage | percent }}</td>
</ng-container>
<!-- Effective Date Column --> <!-- Effective Date Column -->
<ng-container matColumnDef="effectiveDate"> <ng-container matColumnDef="effectiveDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th>
@ -97,7 +103,7 @@
<mat-label>Holder Type</mat-label> <mat-label>Holder Type</mat-label>
<mat-radio-group formControlName="holderType" required class="horizontal-group"> <mat-radio-group formControlName="holderType" required class="horizontal-group">
<mat-radio-button *ngFor="let type of holderTypes" [value]="type.value" class="radio-button"> <mat-radio-button *ngFor="let type of holderTypes" [value]="type.value" class="radio-button">
{{ type.label }} {{ type.name }}
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
<mat-error *ngIf="depositForm.get('holderType')?.errors?.['required']"> <mat-error *ngIf="depositForm.get('holderType')?.errors?.['required']">
@ -147,6 +153,18 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Rate %</mat-label>
<input matInput type="number" formControlName="rateInPercentage" required min="0" step="1">
<span class="percent-suffix" matSuffix>%&nbsp;</span>
<mat-error *ngIf="depositForm.get('rateInPercentage')?.errors?.['required']">
Rate % is required
</mat-error>
<mat-error *ngIf="depositForm.get('rateInPercentage')?.errors?.['min']">
Rate % must be positive
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Effective Date</mat-label> <mat-label>Effective Date</mat-label>
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required> <input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>

View File

@ -14,6 +14,7 @@ import { CommonService } from '../../core/services/common/common.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { SecurityDepositService } from '../../core/services/service-provider/security-deposit.service'; import { SecurityDepositService } from '../../core/services/service-provider/security-deposit.service';
import { finalize } from 'rxjs'; import { finalize } from 'rxjs';
import { HolderType } from '../../core/models/holder-type';
@Component({ @Component({
selector: 'app-security-deposit', selector: 'app-security-deposit',
@ -26,7 +27,7 @@ export class SecurityDepositComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['holderType', 'uscibMember', 'specialCommodity', 'specialCountry', 'rate', 'effectiveDate', 'actions']; displayedColumns: string[] = ['holderType', 'uscibMember', 'specialCommodity', 'specialCountry', 'rate', 'rateInPercentage', 'effectiveDate', 'actions'];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
depositForm: FormGroup; depositForm: FormGroup;
isEditing = false; isEditing = false;
@ -43,18 +44,13 @@ export class SecurityDepositComponent implements OnInit {
}; };
countries: Country[] = []; countries: Country[] = [];
holderTypes: HolderType[] = [];
@Input() isEditMode = false; @Input() isEditMode = false;
@Input() spid: number = 0; @Input() spid: number = 0;
@Input() userPreferences!: UserPreferences; @Input() userPreferences!: UserPreferences;
@Output() hasSecurityDeposits = new EventEmitter<boolean>(); @Output() hasSecurityDeposits = new EventEmitter<boolean>();
holderTypes = [
{ value: 'CORP', label: 'Corporation' },
{ value: 'INDIVIDUAL', label: 'Individual' },
{ value: 'GOVERNMENT', label: 'Government Agency' }
];
yesNoOptions = [ yesNoOptions = [
{ value: 'Y', label: 'Yes' }, { value: 'Y', label: 'Yes' },
{ value: 'N', label: 'No' } { value: 'N', label: 'No' }
@ -73,6 +69,7 @@ export class SecurityDepositComponent implements OnInit {
specialCommodity: [''], specialCommodity: [''],
specialCountry: [''], specialCountry: [''],
rate: [0, [Validators.required, Validators.min(0)]], rate: [0, [Validators.required, Validators.min(0)]],
rateInPercentage: [0, [Validators.required, Validators.min(0)]],
effectiveDate: ['', Validators.required] effectiveDate: ['', Validators.required]
}); });
} }
@ -80,6 +77,7 @@ export class SecurityDepositComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.loadSecurityDeposits(); this.loadSecurityDeposits();
this.loadCountries(); this.loadCountries();
this.loadHolderTypes();
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -129,6 +127,16 @@ export class SecurityDepositComponent implements OnInit {
}); });
} }
loadHolderTypes(): void {
this.commonService.getHolderTypes().subscribe({
next: (holderTypes) => {
this.holderTypes = holderTypes;
},
error: (error) => {
console.error('Error loading holder types:', error);
}
});
}
// applyFilter(event: Event): void { // applyFilter(event: Event): void {
// const filterValue = (event.target as HTMLInputElement).value; // const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase(); // this.dataSource.filter = filterValue.trim().toLowerCase();
@ -147,7 +155,7 @@ export class SecurityDepositComponent implements OnInit {
uscibMember: 'N', uscibMember: 'N',
specialCountry: '', specialCountry: '',
}); });
this.depositForm.patchValue({ rate: 0 }); this.depositForm.patchValue({ rate: 0, rateInPercentage: 0 });
this.depositForm.get('holderType')?.enable(); this.depositForm.get('holderType')?.enable();
this.depositForm.get('uscibMember')?.enable(); this.depositForm.get('uscibMember')?.enable();
@ -165,6 +173,7 @@ export class SecurityDepositComponent implements OnInit {
specialCommodity: deposit.specialCommodity, specialCommodity: deposit.specialCommodity,
specialCountry: deposit.specialCountry, specialCountry: deposit.specialCountry,
rate: deposit.rate, rate: deposit.rate,
rateInPercentage: deposit.rateInPercentage,
effectiveDate: deposit.effectiveDate effectiveDate: deposit.effectiveDate
}); });
@ -251,7 +260,7 @@ export class SecurityDepositComponent implements OnInit {
getHolderTypeLabel(value: string): string { getHolderTypeLabel(value: string): string {
const type = this.holderTypes.find(t => t.value === value); const type = this.holderTypes.find(t => t.value === value);
return type ? type.label : value; return type ? type.name : value;
} }
getCountryName(code: string): string { getCountryName(code: string): string {