This commit is contained in:
Suganthi 2025-06-04 23:21:18 -05:00
commit c0500c458b
131 changed files with 24326 additions and 0 deletions

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# CarnetPortalApp
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.5.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

132
angular.json Normal file
View File

@ -0,0 +1,132 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"carnet-portal-app": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/carnet-portal-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles"
]
},
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "src/server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1500kB",
"maximumError": "5MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "carnet-portal-app:build:production"
},
"development": {
"buildTarget": "carnet-portal-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"stylePreprocessorOptions": {
"includePaths": [
"src/styles"
]
},
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
},
"deploy": {
"builder": "angular-cli-ghpages:deploy"
}
},
"i18n": {
"sourceLocale": "en-US"
}
}
},
"cli": {
"analytics": "7c8b4b49-3b4a-41d5-97c5-1671d7b09dae"
}
}

15136
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "carnet-portal-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:carnet-portal-app": "node dist/carnet-portal-app/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/cdk": "^19.2.7",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/material": "^19.2.7",
"@angular/material-moment-adapter": "^19.2.7",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/platform-server": "^19.2.0",
"@angular/router": "^19.2.0",
"@angular/ssr": "^19.2.5",
"express": "^4.18.2",
"ng2-charts": "^8.0.0",
"ngx-cookie-service": "^19.1.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.2.5",
"@angular/cli": "^19.2.5",
"@angular/compiler-cli": "^19.2.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"angular-cli-ghpages": "^2.0.3",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
public/images/logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,7 @@
<app-secured-header *ngIf="isUserLoggedIn"></app-secured-header>
<main class="main-content">
<router-outlet></router-outlet>
</main>
<app-footer></app-footer>

View File

@ -0,0 +1,22 @@
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative; // Needed for footer positioning
}
.main-content {
flex: 1;
padding: 24px;
max-width: 90%;
margin: 0 auto;
width: 100%;
padding-bottom: 80px;
}
@media (max-width: 960px) {
.main-content {
padding: 16px;
padding-bottom: 80px; // Maintain bottom padding on mobile
}
}

39
src/app/app.component.ts Normal file
View File

@ -0,0 +1,39 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SecuredHeaderComponent } from './common/secured-header/secured-header.component';
import { UserService } from './core/services/user.service';
import { FooterComponent } from './common/footer/footer.component';
import { CommonModule } from '@angular/common';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-root',
imports: [RouterOutlet, SecuredHeaderComponent, FooterComponent, CommonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'carnet-portal-app';
isUserLoggedIn = false;
private userSubscription!: Subscription;
constructor(private userService: UserService
) { }
ngOnInit(): void {
this.isUserLoggedIn = this.userService.isLoggedIn();
this.userSubscription = this.userService
.watchUser()
.subscribe(userLoggedIn => {
this.isUserLoggedIn = userLoggedIn
});
}
ngOnDestroy(): void {
if (this.userSubscription) {
this.userSubscription.unsubscribe();
}
}
}

View File

@ -0,0 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

21
src/app/app.config.ts Normal file
View File

@ -0,0 +1,21 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import {
provideCharts,
withDefaultRegisterables,
} from 'ng2-charts';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideHttpClient(),
provideCharts(withDefaultRegisterables())]
};

19
src/app/app.routes.ts Normal file
View File

@ -0,0 +1,19 @@
import { Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './auth/auth.guard';
import { AddServiceProviderComponent } from './service-provider/add/add-service-provider.component';
import { EditServiceProviderComponent } from './service-provider/edit/edit-service-provider.component';
import { NotFoundComponent } from './shared/components/not-found/not-found.component';
import { UserSettingsComponent } from './user-settings/user-settings.component';
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'usersettings', component: UserSettingsComponent, canActivate: [AuthGuard] },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'service-provider/:id', component: EditServiceProviderComponent, canActivate: [AuthGuard] },
{ path: 'add-service-provider', component: AddServiceProviderComponent, canActivate: [AuthGuard] },
{ path: '404', component: NotFoundComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', redirectTo: '/404' }
];

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { UserService } from '../core/services/user.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
if (this.userService.isLoggedIn()) {
return true;
}
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}

View File

@ -0,0 +1,13 @@
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="#">Privacy Policy</a>
<a href="#">Terms of Service</a>
<a href="#">Contact Us</a>
</div>
<div class="copyright">
&copy; {{ currentYear }} USCIB Carnet Portal. All rights reserved.
</div>
</div>
</footer>

View File

@ -0,0 +1,58 @@
@import 'colors';
.footer {
background-color: $background-header;
padding: 8px;
border-top: 1px solid $divider-color;
backdrop-filter: blur(10px);
background-color: rgba($background-header, 0.9);
/* Fixed footer styles */
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
/* Ensure footer stays above content */
.footer-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.footer-links {
display: flex;
gap: 24px;
a {
color: $text-secondary;
text-decoration: none;
transition: color 0.3s ease;
&:hover {
color: $primary-color;
}
}
}
.copyright {
color: $text-secondary;
font-size: 12px;
}
}
@media (max-width: 600px) {
.footer {
padding: 16px;
.footer-links {
flex-direction: column;
gap: 8px;
align-items: center;
}
}
}

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
@Component({
selector: 'app-footer',
imports: [AngularMaterialModule],
templateUrl: './footer.component.html',
styleUrl: './footer.component.scss'
})
export class FooterComponent {
currentYear = new Date().getFullYear();
}

View File

@ -0,0 +1,35 @@
<nav class="navbar">
<div class="navbar-brand">
<img src="images/logo-white.png" alt="App Logo" class="logo">
<span class="app-title">USCIB Carnet Portal</span>
</div>
<div class="navbar-menu">
<a class="nav-link" (click)="navigateTo('/home')">Home</a>
<a class="nav-link" (click)="navigateTo('/manageusers')">Users</a>
<a class="nav-link" (click)="navigateTo('/regions')">Regions</a>
<div class="profile-container">
<button mat-icon-button (click)="toggleProfileMenu()" class="profile-button">
<mat-icon>account_circle</mat-icon>
</button>
<div class="profile-menu" *ngIf="showProfileMenu">
<div class="profile-info">
<mat-icon>email</mat-icon>
<span>{{ userEmail }}</span>
</div>
<button mat-menu-item (click)="navigateTo('/usersettings')">
<mat-icon>settings</mat-icon>
<span>User Settings</span>
</button>
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
<span>Logout</span>
</button>
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,110 @@
@import 'colors';
@import 'mixins';
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
height: 64px;
background-color: $primary-color;
color: white;
@include box-shadow(2);
position: relative;
z-index: 100; // Higher than footer's z-index
.navbar-brand {
display: flex;
align-items: center;
gap: 16px;
.logo {
height: 40px;
width: auto;
}
.app-title {
font-size: 20px;
font-weight: 500;
}
}
.navbar-menu {
display: flex;
align-items: center;
gap: 24px;
.nav-link {
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.3s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
.profile-container {
position: relative;
.profile-button {
color: white;
}
.profile-menu {
position: absolute;
right: 0;
top: 48px;
background-color: white;
border-radius: 4px;
@include box-shadow(3);
min-width: 200px;
overflow: hidden;
z-index: 101;
.profile-info {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid $divider-color;
color: $text-primary;
mat-icon {
color: $text-secondary;
}
}
button {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: $text-primary;
border: none;
cursor: pointer;
mat-icon {
color: $text-secondary;
}
}
}
}
}
@media (max-width: 768px) {
.navbar {
padding: 0 12px;
.app-title {
display: none;
}
.navbar-menu {
gap: 12px;
}
}
}

View File

@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { UserService } from '../../core/services/user.service';
import { Router } from '@angular/router';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-secured-header',
imports: [AngularMaterialModule, CommonModule],
templateUrl: './secured-header.component.html',
styleUrl: './secured-header.component.scss'
})
export class SecuredHeaderComponent implements OnInit {
userEmail: string = '';
showProfileMenu: boolean = false;
constructor(
private userService: UserService,
private router: Router
) { }
ngOnInit(): void {
this.userEmail = this.userService.getSafeUser();
}
toggleProfileMenu(): void {
this.showProfileMenu = !this.showProfileMenu;
}
logout(): void {
this.userService.clearUser();
this.router.navigate(['/login']);
this.showProfileMenu = false;
}
navigateTo(route: string): void {
this.router.navigate([route]);
this.showProfileMenu = false;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
export interface CarnetStatus {
id: string;
name: string;
value: string;
color: string;
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export interface Region {
id: number,
region: string;
regionname: string;
}

View File

@ -0,0 +1,16 @@
export interface BasicDetail {
cargoSurety: string;
cargoPolicyNo: string;
bondSurety: string;
spid: number;
companyName: string;
lookupCode?: string;
address1: string;
address2?: string;
city: string;
country: string;
state: string;
zip: string;
issuingRegion: string;
replacementRegion: string;
}

View File

@ -0,0 +1,10 @@
export interface BasicFee {
basicFeeId: number;
startCarnetValue: number;
endCarnetValue: number | null;
fees: number;
effectiveDate: Date;
spid?: number;
dateCreated?: Date | null;
createdBy?: string | null;
}

View File

@ -0,0 +1,9 @@
export interface CarnetFee {
feeCommissionId: number;
feeType: string;
commissionRate: number;
effectiveDate: Date;
spid: number;
dateCreated?: Date | null;
createdBy?: string | null;
}

View File

@ -0,0 +1,8 @@
export interface CarnetSequence {
spid: number;
carnetType: string;
region: number;
startNumber: number;
endNumber: number;
lastNumber: number;
}

View File

@ -0,0 +1,19 @@
export interface Contact {
spContactId: number;
serviceProviderId: number;
defaultContact: boolean;
firstName: string;
lastName: string;
middleInitial: string;
title: string;
phone: string;
mobile: string;
fax: string;
email: string;
dateCreated?: Date | null;
createdBy?: string | null;
lastUpdatedBy?: string | null;
lastUpdatedDate?: Date | null;
isInactive?: boolean | null;
inactivatedDate?: Date | null;
}

View File

@ -0,0 +1,10 @@
export interface ContinuationSheetFee {
id: number;
spid: number;
rate: number;
effectiveDate: Date;
customerType: string;
carnetType: string;
dateCreated?: Date | null;
createdBy?: string | null;
}

View File

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

View File

@ -0,0 +1,12 @@
export interface ExpeditedFee {
expeditedFeeId: number;
customerType: string;
deliveryType: string;
startTime: number;
endTime: number;
timeZone: string;
fee: number;
effectiveDate: Date;
dateCreated?: Date | null;
createdBy?: string | null;
}

View File

@ -0,0 +1,12 @@
export interface SecurityDeposit {
securityDepositId: number;
holderType: string;
uscibMember: boolean;
specialCommodity: string;
specialCountry: string;
rate: number;
effectiveDate: Date;
spid: number;
dateCreated?: Date | null;
createdBy?: string | null;
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
export interface UserPreferences {
pageSize?: number;
}
// Default preferences
export const DEFAULT_USER_PREFERENCES: UserPreferences = {
pageSize: 5
};

View File

@ -0,0 +1,63 @@
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ApiErrorHandlerService {
private genericErrorMessage = 'An unexpected error occurred. Please try again later.';
/**
* Extracts user-friendly error messages from API error responses
* @param error The error response object
* @returns Array of error messages to display
*/
handleApiError(error: HttpErrorResponse | any, customErrorMessage: string): string {
if (!error) {
return (customErrorMessage) ? customErrorMessage : this.genericErrorMessage;
}
// Handle 400 Bad Request with pipe-delimited messages
if (error.status === 400 && error.error?.message) {
return this.parsePipeDelimitedMessages(error.error.message);
}
// Handle other status codes
return this.getAppropriateGenericMessage(error.status, customErrorMessage);
}
/**
* Splits pipe-delimited messages and cleans them up
* @param messageString The raw message string from API
* @returns Array of cleaned error messages
*/
private parsePipeDelimitedMessages(messageString: string): string {
if (!messageString || typeof messageString !== 'string') {
return this.genericErrorMessage;
}
return messageString.slice(0, -1);
// .split('|')
// .map(msg => msg.trim())
// .filter(msg => msg.length > 0);
}
/**
* Returns appropriate generic message based on status code
* @param status HTTP status code
* @returns Generic error message
*/
private getAppropriateGenericMessage(status: number, customErrorMessage: string): string {
switch (status) {
case 401:
return 'Unauthorized access. Please login again.';
case 403:
return 'You do not have permission to perform this action.';
case 404:
return 'The requested resource was not found.';
case 500:
default:
return (customErrorMessage) ? customErrorMessage : this.genericErrorMessage;;
}
}
}

View File

@ -0,0 +1,17 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = environment.apiUrl;
constructor(private http: HttpClient) { }
login(username: string, password: string): Observable<any> {
return this.http.post(`${this.apiUrl}/login`, { p_emailaddr: username, p_password: password });
}
}

View File

@ -0,0 +1,86 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { BasicDetail } from '../models/service-provider/basic-detail';
import { filter, map, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class BasicDetailService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService) { }
getBasicDetailsById(id: number): BasicDetail | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetSelectedServiceprovider?p_spid=${id}`).pipe(
filter(response => response.length > 0),
map(response => this.mapToBasicDetail(response?.[0])));
}
private mapToBasicDetail(basicDetails: any): BasicDetail {
return {
spid: basicDetails.SPID,
companyName: basicDetails.NAMEOF,
lookupCode: basicDetails.LOOKUPCODE,
address1: basicDetails.ADDRESS1,
address2: basicDetails.ADDRESS2,
city: basicDetails.CITY,
state: basicDetails.STATE,
country: basicDetails.COUNTRY,
issuingRegion: basicDetails.ISSUINGREGION,
replacementRegion: basicDetails.REPLACEMENTREGION,
zip: basicDetails.ZIP,
cargoSurety: basicDetails.CARGOSURETY,
cargoPolicyNo: basicDetails.CARGOPOLICYNO,
bondSurety: basicDetails.BONDSURETY
};
}
createBasicDetails(data: BasicDetail): Observable<any> {
const basicDetails = {
p_name: data.companyName,
p_lookupcode: data.lookupCode,
p_address1: data.address1,
p_address2: data.address2,
p_city: data.city,
p_state: data.state,
p_zip: data.zip,
p_country: data.country,
p_issuingregion: data.issuingRegion,
p_replacementregion: data.replacementRegion,
p_bondsurety: data.bondSurety,
p_cargopolicyno: data.cargoPolicyNo,
p_cargosurety: data.cargoSurety,
p_user_id: this.userService.getUser(),
}
return this.http.post(`${this.apiUrl}/${this.apiDb}/InsertNewServiceProvider`, basicDetails);
}
updateBasicDetails(id: number, data: BasicDetail): Observable<any> {
const basicDetails = {
p_spid: id,
p_name: data.companyName,
p_lookupcode: data.lookupCode,
p_address1: data.address1,
p_address2: data.address2,
p_city: data.city,
p_state: data.state,
p_zip: data.zip,
p_country: data.country,
p_issuingregion: data.issuingRegion,
p_replacementregion: data.replacementRegion,
p_bondsurety: data.bondSurety,
p_cargopolicyno: data.cargoPolicyNo,
p_cargosurety: data.cargoSurety,
p_user_id: this.userService.getUser(),
}
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateServiceProvider`, basicDetails);
}
}

View File

@ -0,0 +1,67 @@
import { Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { UserService } from './user.service';
import { BasicFee } from '../models/service-provider/basic-fee';
import { map, Observable } from 'rxjs';
import { CommonService } from './common.service';
@Injectable({
providedIn: 'root'
})
export class BasicFeeService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
getBasicFees(spid: number): Observable<BasicFee[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetBasicFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
map(response => this.mapToBasicFees(response)));
}
private mapToBasicFees(data: any[]): BasicFee[] {
return data.map(item => ({
basicFeeId: item.BASICFEESETUPID,
startCarnetValue: item.STARTCARNETVALUE,
endCarnetValue: item.ENDCARNETVALUE,
fees: item.FEES,
effectiveDate: item.EFFDATE,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null
}));
}
createBasicFee(spid: number, fee: BasicFee): Observable<any> {
const payload = {
P_SPID: spid,
P_STARTCARNETVALUE: fee.startCarnetValue,
P_ENDCARNETVALUE: fee.endCarnetValue,
P_FEES: fee.fees,
P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate),
P_USERID: this.userService.getUser()
};
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateBasicFee`, payload);
}
updateBasicFee(feeId: number, fee: BasicFee): Observable<any> {
const payload = {
P_BASICFEESETUPID: feeId,
P_STARTCARNETVALUE: fee.startCarnetValue,
P_ENDCARNETVALUE: fee.endCarnetValue,
P_FEES: fee.fees,
P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate),
P_USERID: this.userService.getUser()
};
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateBasicFee`, payload);
}
// deleteBasicFee(feeId: number): Observable<any> {
// return this.http.delete<any>(`${this.apiUrl}/${this.apiDb}/DeleteBasicFee/${feeId}`, {
// params: {
// p_userid: this.userService.getUser()
// }
// });
// }
}

View File

@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { CarnetFee } from '../models/service-provider/carnet-fee';
import { CommonService } from './common.service';
@Injectable({
providedIn: 'root'
})
export class CarnetFeeService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
getFeeCommissions(spid: number): Observable<CarnetFee[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetFeeComm?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
map(response => this.mapToFeeCommissions(response)));
}
private mapToFeeCommissions(data: any[]): CarnetFee[] {
return data.map(item => ({
feeCommissionId: item.FEECOMMID,
feeType: item.FEETYPEID,
description: item.DESCRIPTION,
commissionRate: item.COMMRATE,
effectiveDate: item.EFFDATE,
spid: item.SPID,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null
}));
}
createFeeCommission(spid: number, data: CarnetFee): Observable<any> {
const feeCommission = {
P_SPID: spid,
P_PARAMID: data.feeType,
P_COMMRATE: data.commissionRate,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_USERID: this.userService.getUser()
};
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateFeeComm`, feeCommission);
}
updateFeeCommission(id: number, data: CarnetFee): Observable<any> {
const feeCommission = {
P_FEECOMMID: id,
P_RATE: data.commissionRate,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_USERID: this.userService.getUser()
};
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateFeeComm`, feeCommission);
}
// deleteFeeCommission(id: number): Observable<void> {
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/DeleteFeeCommission/${id}`);
// }
}

View File

@ -0,0 +1,45 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';
import { UserService } from './user.service';
import { map, Observable } from 'rxjs';
import { CarnetSequence } from '../models/service-provider/carnet-sequence';
@Injectable({
providedIn: 'root'
})
export class CarnetSequenceService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService) { }
getCarnetSequenceById(id: number): Observable<CarnetSequence[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCarnetSequence?p_spid=${id}`).pipe(
map(response => this.mapToCarnetSequence(response)));
}
private mapToCarnetSequence(data: any[]): CarnetSequence[] {
return data.map(item => ({
spid: item.SPID,
region: item.REGIONID,
carnetType: item.CARNETTYPE,
startNumber: item.STARTNUMBER,
endNumber: item.ENDNUMBER,
lastNumber: item.LASTNUMBER
}));
}
createCarnetSequence(data: CarnetSequence): Observable<any> {
const carnetSequence = {
p_spid: data.spid,
p_regionid: data.region,
p_startnumber: data.startNumber,
p_endnumber: data.endNumber,
p_carnettype: data.carnetType
}
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateCarnetSequence`, carnetSequence);
}
}

View File

@ -0,0 +1,159 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs';
import { Region } from '../models/region';
import { Country } from '../models/country';
import { State } from '../models/state';
import { environment } from '../../../environments/environment';
import { DeliveryType } from '../models/delivery-type';
import { FeeType } from '../models/fee-type';
import { TimeZone } from '../models/timezone';
import { BondSurety } from '../models/bond-surety';
import { CargoPolicy } from '../models/cargo-policy';
import { CargoSurety } from '../models/cargo-surety';
import { CarnetStatus } from '../models/carnet-status';
@Injectable({
providedIn: 'root'
})
export class CommonService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient) { }
getCountries(spid: number = 0): Observable<Country[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=002&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getStates(country: string, spid: number = 0): Observable<State[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=001&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE,
countryValue: item.ADDLPARAMVALUE1,
})).filter((state) => state.countryValue === country) // Filter by country value
),
catchError((error) => {
console.error('Error fetching states:', error);
return of([]);
})
);
}
getRegions(): Observable<Region[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions`).pipe(
map((response) =>
response.map((item) => ({
id: item.REGIONID,
region: item.REGION,
regionname: item.REGIONNAME
}))
)
);
}
getDeliveryTypes(spid: number = 0): Observable<DeliveryType[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=006&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getTimezones(spid: number = 0): Observable<TimeZone[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=010&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getFeeTypes(spid: number = 0): Observable<FeeType[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=009&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getBondSuretys(spid: number = 0): Observable<BondSurety[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=003&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getCargoPolicies(spid: number = 0): Observable<CargoPolicy[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=004&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getCargoSuretys(spid: number = 0): Observable<CargoSurety[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=005&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
}))
)
);
}
getCarnetStatuses(spid: number = 0): Observable<CarnetStatus[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=011&P_SPID=0`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE,
color: item.ADDLPARAMVALUE1,
}))
)
);
}
formatUSDate(datetime: Date): string {
const date = new Date(datetime);
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const year = date.getUTCFullYear();
return `${month}/${day}/${year}`;
}
}

View File

@ -0,0 +1,85 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { Contact } from '../models/service-provider/contact';
import { map, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ContactService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService) { }
getContactsById(id: number): Observable<Contact[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetSPAllContacts?p_SPid=${id}`).pipe(
map(response => this.mapToContacts(response)));
}
private mapToContacts(data: any[]): Contact[] {
return data.map(contact => ({
spContactId: contact.SPCONTACTID,
serviceProviderId: contact.SPID,
defaultContact: contact.DEFCONTACTFLAG === 'Y',
firstName: contact.FIRSTNAME,
lastName: contact.LASTNAME,
title: contact.TITLE,
phone: contact.PHONENO,
mobile: contact.MOBILENO,
fax: contact.FAXNO || null,
email: contact.EMAILADDRESS,
middleInitial: contact.MIDDLEINITIAL || null,
createdBy: contact.CREATEDBY || null,
dateCreated: contact.DATECREATED || null,
lastUpdatedBy: contact.LASTUPDATEDBY || null,
lastUpdatedDate: contact.LASTUPDATEDDATE || null,
isInactive: contact.INACTIVEFLAG === 'Y' || false,
inactivatedDate: contact.INACTIVEDATE || null
}));
}
createContact(spid: number, data: Contact): Observable<any> {
const contact = {
p_spid: spid,
p_defcontactflag: data.defaultContact ? 'Y' : 'N',
p_firstname: data.firstName,
p_lastname: data.lastName,
P_MIDDLEINITIAL: data.middleInitial,
p_title: data.title,
p_phoneno: data.phone,
p_mobileno: data.mobile,
p_faxno: data.fax,
p_emailaddress: data.email,
p_user_id: this.userService.getUser()
}
return this.http.post(`${this.apiUrl}/${this.apiDb}/InsertSPContacts`, contact);
}
updateContact(spContactId: number, data: Contact): Observable<any> {
const contact = {
p_spcontactid: spContactId,
p_firstname: data.firstName,
p_lastname: data.lastName,
P_MIDDLEINITIAL: data.middleInitial,
p_title: data.title,
p_phoneno: data.phone,
p_mobileno: data.mobile,
p_faxno: data.fax,
p_emailaddress: data.email,
p_user_id: this.userService.getUser()
}
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateSPContacts`, contact);
}
deleteContact(spContactId: string): Observable<any> {
return this.http.post(`${this.apiUrl}/${this.apiDb}/InactivateSPContact?p_spcontactid=${spContactId}`, null);
}
}

View File

@ -0,0 +1,66 @@
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { ContinuationSheetFee } from '../models/service-provider/continuation-sheet-fee';
import { map, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { CommonService } from './common.service';
@Injectable({
providedIn: 'root'
})
export class ContinuationSheetFeeService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
getContinuationSheets(spid: number): Observable<ContinuationSheetFee[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCsFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
map(response => this.mapToContinuationSheetFee(response)));
}
private mapToContinuationSheetFee(data: any[]): ContinuationSheetFee[] {
return data.map(item => ({
id: item.CSFEESETUPID,
spid: item.SPID,
customerType: item.CUSTOMERTYPE,
carnetType: item.CARNETTYPE,
effectiveDate: item.EFFDATE,
rate: item.RATE,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null
}));
}
addContinuationSheet(spid: number, data: ContinuationSheetFee): Observable<any> {
const continuationSheet = {
P_SPID: spid,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_CUSTOMERTYPE: data.customerType,
P_CARNETTYPE: data.carnetType,
P_RATE: data.rate,
P_USERID: this.userService.getUser()
}
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateCsFee`, continuationSheet);
}
updateContinuationSheet(id: number, data: ContinuationSheetFee): Observable<any> {
const continuationSheet = {
P_CSFEESETUPID: 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}/UpdateCsFee`, continuationSheet);
}
// deleteContinuationSheet(id: string): Observable<void> {
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
// }
}

View File

@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
import { CookieService as NgxCookieService } from 'ngx-cookie-service';
@Injectable({
providedIn: 'root'
})
export class CookieHelperService {
constructor(private ngxCookieService: NgxCookieService) { }
/**
* Get a value from cookie
* @param key Cookie key
* @returns Parsed JSON value or null if not found
*/
get<T>(key: string): T | null {
try {
const value = this.ngxCookieService.get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Error parsing cookie value', error);
return null;
}
}
/**
* Set a value in cookie
* @param key Cookie key
* @param value Value to store (will be stringified)
* @param expiresDays Number of days until cookie expires (default 365)
*/
set(key: string, value: any, expiresDays: number = 365): void {
const expires = new Date();
expires.setDate(expires.getDate() + expiresDays);
this.ngxCookieService.set(
key,
JSON.stringify(value),
expires,
'/',
undefined,
false,
'Lax'
);
}
/**
* Remove a cookie
* @param key Cookie key to remove
*/
remove(key: string): void {
this.ngxCookieService.delete(key, '/');
}
/**
* Check if a cookie exists
* @param key Cookie key to check
*/
has(key: string): boolean {
return this.ngxCookieService.check(key);
}
}

View File

@ -0,0 +1,69 @@
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { HttpClient } from '@angular/common/http';
import { CounterfoilFee } from '../models/service-provider/counterfoil-fee';
import { map, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { CommonService } from './common.service';
@Injectable({
providedIn: 'root'
})
export class CounterfoilFeeService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
getCounterfoils(spid: number): Observable<CounterfoilFee[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCfFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
map(response => this.mapToCounterFoilFee(response)));
}
private mapToCounterFoilFee(data: any[]): CounterfoilFee[] {
return data.map(item => ({
id: item.CFFEESETUPID,
spid: item.SPID,
startSets: item.STARTSETS,
endSets: item.ENDSETS,
customerType: item.CUSTOMERTYPE,
carnetType: item.CARNETTYPE,
effectiveDate: item.EFFDATE,
rate: item.RATE,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null
}));
}
addCounterfoil(spid: number, data: CounterfoilFee): Observable<any> {
const counterfoilFee = {
P_SPID: spid,
P_STARTSETS: data.startSets,
P_ENDSETS: data.endSets,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_CUSTOMERTYPE: data.customerType,
P_CARNETTYPE: data.carnetType,
P_RATE: data.rate,
P_USERID: this.userService.getUser()
}
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateCfFee`, counterfoilFee);
}
updateCounterfoil(id: number, data: CounterfoilFee): Observable<any> {
const counterfoilFee = {
P_CFFEESETUPID: 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}//UpdateCfFee`, counterfoilFee);
}
// deleteCounterfoil(id: string): Observable<void> {
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
// }
}

View File

@ -0,0 +1,72 @@
import { Injectable } from '@angular/core';
import { ExpeditedFee } from '../models/service-provider/expedited-fee';
import { map, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { UserService } from './user.service';
import { environment } from '../../../environments/environment';
import { CommonService } from './common.service';
@Injectable({
providedIn: 'root'
})
export class ExpeditedFeeService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
getExpeditedFees(spid: number): Observable<ExpeditedFee[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetEfFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
map(response => this.mapToExpeditedFee(response)));
}
private mapToExpeditedFee(data: any[]): ExpeditedFee[] {
return data.map(item => ({
expeditedFeeId: item.EXPFEESETUPID,
customerType: item.CUSTOMERTYPE,
deliveryType: item.DELIVERYTYPE,
startTime: item.STARTTIME,
endTime: item.ENDTIME,
timeZone: item.TIMEZONE,
fee: item.FEES,
effectiveDate: item.EFFDATE,
spid: item.SPID,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null
}));
}
createExpeditedFee(spid: number, data: ExpeditedFee): Observable<any> {
const expeditedFee = {
P_SPID: spid,
P_CUSTOMERTYPE: data.customerType,
P_DELIVERYTYPE: data.deliveryType,
P_TIMEZONE: data.timeZone,
P_STARTTIME: +data.startTime,
P_ENDTIME: +data.endTime,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_FEES: data.fee,
P_USERID: this.userService.getUser()
}
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateEfFee`, expeditedFee);
}
updateExpeditedFee(id: number, data: ExpeditedFee): Observable<any> {
const expeditedFee = {
P_EFFEESETUPID: id,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_FEES: data.fee,
P_USERID: this.userService.getUser()
}
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}//UpdateEfFee`, expeditedFee);
}
// deleteExpeditedFee(id: string): Observable<void> {
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
// }
}

View File

@ -0,0 +1,42 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { map, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class HomeService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService) { }
getCarnetSummaryData(): Observable<any> {
const userid = this.userService.getUser();
return this.http.get(`${this.apiUrl}/${this.apiDb}/GetCarnetSummaryData/${userid}`);
}
getCarnetDataByStatus(spid: number, carnetStatus: string): Observable<any> {
const userid = this.userService.getUser();
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCarnetDetailsbyCarnetStatus/${spid}/${userid}/${carnetStatus}`).pipe(
map(response => this.mapToCarnetData(response)));
}
private mapToCarnetData(data: any[]): any[] {
return data.map(item => ({
applicationName: item.APPLICATIONNAME,
holderName: item.HOLDERNAME,
carnetNumber: item.CARNETNO,
usSets: item.USSETS,
foreignSets: item.FOREIGNSETS,
transitSets: item.TRANSITSETS,
carnetValue: item.CARNETVALUE,
issueDate: item.ISSUEDATE || null,
expiryDate: item.EXPDATE || null,
orderType: item.ORDERTYPE,
carnetStatus: item.CARNETSTATUS
}));
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private snackBar: MatSnackBar) { }
showSuccess(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 3000,
panelClass: ['success-snackbar']
});
}
showError(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 3000,
panelClass: ['error-snackbar']
});
}
showWarning(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 3000,
panelClass: ['error-snackbar']
});
}
}

View File

@ -0,0 +1,69 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { SecurityDeposit } from '../models/service-provider/security-deposit';
import { map, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { CommonService } from './common.service';
@Injectable({
providedIn: 'root'
})
export class SecurityDepositService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
getSecurityDeposits(spid: number): Observable<SecurityDeposit[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetBondRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
map(response => this.mapToSecurityDeposit(response)));
}
private mapToSecurityDeposit(data: any[]): SecurityDeposit[] {
return data.map(item => ({
securityDepositId: item.BONDRATESETUPID,
holderType: item.HOLDERTYPE,
uscibMember: item.USCIBMEMBERFLAG,
specialCommodity: item.SPCLCOMMODITY,
specialCountry: item.SPCLCOUNTRY,
rate: item.RATE,
effectiveDate: item.EFFDATE,
spid: item.SPID,
createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null
}));
}
createSecurityDeposit(spid: number, data: SecurityDeposit): Observable<any> {
const securityDeposit = {
P_SPID: spid,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_HOLDERTYPE: data.holderType,
P_USCIBMEMBERFLAG: data.uscibMember ? 'Y' : 'N',
P_SPCLCOMMODITY: data.specialCommodity,
P_SPCLCOUNTRY: data.specialCountry,
P_RATE: data.rate,
P_USERID: this.userService.getUser()
}
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateBondRate`, securityDeposit);
}
updateSecurityDeposit(id: number, data: SecurityDeposit): Observable<any> {
const securityDeposit = {
P_BONDRATESETUPID: 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}/UpdateBondRate`, securityDeposit);
}
// deleteSecurityDeposit(id: string): Observable<void> {
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
// }
}

View File

@ -0,0 +1,32 @@
import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private readonly platformId = inject(PLATFORM_ID);
constructor() { }
setItem(key: string, value: string): void {
sessionStorage.setItem(key, value);
}
getItem(key: string): string | null {
if (isPlatformBrowser(this.platformId)) {
return sessionStorage.getItem(key);
}
return null;
}
removeItem(key: string): void {
sessionStorage.removeItem(key);
}
clear(): void {
sessionStorage.clear();
}
}

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TimeFormatService {
/**
* Formats a time range with timezone
* @param startTime 24-hour format (e.g., "13:00")
* @param endTime 24-hour format (e.g., "17:00") - optional
* @param timezone Timezone abbreviation (e.g., "EST")
* @returns Formatted time range string
*/
formatTimeRange(startTime: string, timezone: string, endTime?: string): string {
if (!startTime) return '';
const start = this.formatSingleTime(startTime);
if (!endTime) {
return start.endsWith('m') ? `${start} above ${timezone}` : `${start}am above ${timezone}`;
}
const end = this.formatSingleTime(endTime);
const formattedTz = timezone ? ` ${timezone}` : '';
// If both times have the same meridian (am/pm), only show it at the end
if (start.slice(-2) === end.slice(-2)) {
return `${start.replace(/[ap]m$/, '')}-${end}${formattedTz}`;
}
return `${start}-${end}${formattedTz}`;
}
private formatSingleTime(time24: string): string {
if (!time24) return time24;
const hours = +time24;
const period = hours >= 12 ? 'pm' : 'am';
let displayHours = hours % 12;
displayHours = displayHours === 0 ? 12 : displayHours; // Convert 0 to 12
return `${displayHours}${period}`;
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { CookieHelperService } from './cookie.service';
import { DEFAULT_USER_PREFERENCES, UserPreferences } from '../models/user-preference';
@Injectable({
providedIn: 'root'
})
export class UserPreferencesService {
private readonly COOKIE_KEY = 'user_preferences';
constructor(private cookieService: CookieHelperService) { }
getPreferences(): UserPreferences {
const preferences = this.cookieService.get<UserPreferences>(this.COOKIE_KEY);
return preferences || { ...DEFAULT_USER_PREFERENCES };
}
savePreferences(preferences: UserPreferences): void {
this.cookieService.set(this.COOKIE_KEY, preferences);
}
getUserPrefenceByKey(keyName: string): any {
const preferences = this.getPreferences();
return preferences[keyName as keyof typeof preferences];
}
resetToDefaults(): void {
this.cookieService.remove(this.COOKIE_KEY);
}
}

View File

@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { StorageService } from './storage.service';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly USER_EMAIL_KEY = 'CurrentUserEmail';
private userLoggedInSubject = new BehaviorSubject<boolean>(true);
constructor(private storageService: StorageService) { }
watchUser(): Observable<boolean> {
return this.userLoggedInSubject.asObservable();
}
setUser(email: string): void {
if (!email) {
console.error('Cannot set empty user email');
return;
}
this.storageService.setItem(this.USER_EMAIL_KEY, email);
this.userLoggedInSubject.next(true);
}
getUser(): string | null {
const user = this.storageService.getItem(this.USER_EMAIL_KEY);
if (!user) {
this.userLoggedInSubject.next(false);
}
return user;
}
clearUser(): void {
this.storageService.removeItem(this.USER_EMAIL_KEY);
this.userLoggedInSubject.next(false);
}
isLoggedIn(): boolean {
return !!this.getUser();
}
getSafeUser(): string {
return this.getUser() || '';
}
}

View File

@ -0,0 +1,24 @@
<div class="dashboard-chart-container">
<div *ngIf="!chartData || chartData.length === 0" class="no-data">
<mat-icon>pie_chart</mat-icon>
<p>No carnet data available</p>
</div>
<div class="charts-grid">
<div *ngFor="let config of chartConfigs; let i = index" class="chart-card">
<div class="chart-header">
<h4 class="chart-title">{{ config.title }}</h4>
<button mat-icon-button class="edit-icon" (click)="navigateToManageProvider(config.spid)"
matTooltip="Edit Service Provider">
<mat-icon>edit</mat-icon>
</button>
</div>
<div class="chart-wrapper">
<canvas baseChart [data]="config.data" [options]="config.options" [type]="chartType"
(chartClick)="chartClicked($event, i)" [plugins]="chartPlugins">
</canvas>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,91 @@
@import 'colors';
@import 'mixins';
.dashboard-chart-container {
padding: 24px 0px;
margin-bottom: 24px;
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 24px;
}
.chart-card {
background: #fff;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
.chart-wrapper {
position: relative;
height: 250px;
min-height: 250px;
}
.chart-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
padding: 0 8px;
.chart-title {
margin: 0;
font-size: 16px;
font-weight: 400;
color: $primary-color;
}
.edit-icon {
color: rgba(0, 0, 0, 0.54);
transition: color 0.2s ease;
&:hover {
color: $primary-color;
}
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
}
.no-data {
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.54);
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 16px;
color: rgba(0, 0, 0, 0.24);
}
p {
margin: 0;
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.dashboard-chart-container {
padding: 16px;
.charts-grid {
grid-template-columns: 1fr;
}
}
}

View File

@ -0,0 +1,134 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { Router } from '@angular/router';
import { CarnetStatus } from '../../core/models/carnet-status';
@Component({
selector: 'app-chart',
imports: [CommonModule, AngularMaterialModule, BaseChartDirective],
templateUrl: './chart.component.html',
styleUrl: './chart.component.scss'
})
export class ChartComponent {
@Input() chartData: any[] = [];
@Input() carnetStatuses: CarnetStatus[] = [];
@Output() carnetStatusData = new EventEmitter<any>();
constructor(private router: Router) { }
navigateToManageProvider(spid: number): void {
this.router.navigate(['/service-provider', spid]);
}
public chartClicked(event: any, chartIndex: number): void {
const active = event.active;
if (active?.length) {
const dataIndex = active[0].index;
const chart = this.chartConfigs[chartIndex];
const spid = chart.spid as number;
const carnetStatus = this.carnetStatuses.find(t => t.name === chart.data?.labels?.[dataIndex]);
if (carnetStatus !== undefined) {
this.carnetStatusData.emit({
spid: spid,
carnetStatus: carnetStatus.value
});
}
}
}
public chartOptions: ChartConfiguration['options'] = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
color: '#333',
font: {
size: 14
},
padding: 20
}
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.dataset.label || '';
const value = context.raw as number || 0;
const total = (context.dataset.data as number[]).reduce((a: number, b: number) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
},
}
};
public chartType: ChartType = 'pie';
public chartPlugins = [];
public chartConfigs: {
title: string,
spid: number,
data: ChartData<'pie'>;
colors: string[];
options: ChartConfiguration['options'];
}[] = [];
ngOnChanges() {
this.processChartData();
}
private processChartData(): void {
this.chartConfigs = this.chartData.map(provider => {
const statusColors = provider.CARNETSTATUS.map((status: string) => {
const carnetStatus = this.carnetStatuses.find(t => t.value === status);
return carnetStatus ? carnetStatus.color : '#9E9E9E';
});
const labels = provider.CARNETSTATUS.map((status: string) => {
const carnetStatus = this.carnetStatuses.find(t => t.value === status);
return carnetStatus!.name;
});
return {
title: provider.Service_Provider_Name,
spid: provider.SPID,
data: {
labels: labels,
datasets: [{
data: provider.Carnet_Count,
backgroundColor: statusColors,
hoverBackgroundColor: statusColors.map((c: any) => this.adjustBrightness(c, -20)),
borderWidth: 1,
borderColor: '#fff'
}]
},
colors: statusColors,
options: this.chartOptions
};
});
}
private adjustBrightness(color: string, percent: number): string {
// Helper function to adjust color brightness
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (
0x1000000 +
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255)
).toString(16).slice(1);
}
}

View File

@ -0,0 +1,95 @@
<div class="dashboard-container">
<div class="content">
<div class="quick-actions">
<button mat-raised-button color="accent" routerLink="/add-service-provider">
<mat-icon>add</mat-icon> Add New Service Provider
</button>
</div>
<!-- Chart Section -->
<div class="chart-section">
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses" (carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
</div>
<!-- Carnet Details Section -->
<div class="carnet-details-section">
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<div class="table-container mat-elevation-z8" *ngIf="showTable">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="applicationName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Application Name</th>
<td mat-cell *matCellDef="let item">{{ item.applicationName }}</td>
</ng-container>
<ng-container matColumnDef="holderName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Name</th>
<td mat-cell *matCellDef="let item">{{ item.holderName }}</td>
</ng-container>
<ng-container matColumnDef="carnetNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Number</th>
<td mat-cell *matCellDef="let item">{{ item.carnetNumber }}</td>
</ng-container>
<ng-container matColumnDef="usSets">
<th mat-header-cell *matHeaderCellDef mat-sort-header>US Sets</th>
<td mat-cell *matCellDef="let item">{{ item.usSets }}</td>
</ng-container>
<ng-container matColumnDef="foreignSets">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Foregin Sets</th>
<td mat-cell *matCellDef="let item">{{ item.foreignSets }}</td>
</ng-container>
<ng-container matColumnDef="transitSets">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Transit Sets</th>
<td mat-cell *matCellDef="let item">{{ item.transitSets }}</td>
</ng-container>
<ng-container matColumnDef="carnetValue">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Value</th>
<td mat-cell *matCellDef="let item">{{ item.carnetValue }}</td>
</ng-container>
<ng-container matColumnDef="issueDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Issue Date</th>
<td mat-cell *matCellDef="let item">{{ item.issueDate |
date:'mediumDate':'UTC' }}</td>
</ng-container>
<ng-container matColumnDef="expiryDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expiry Date</th>
<td mat-cell *matCellDef="let item">{{ item.expiryDate |
date:'mediumDate':'UTC' }}</td>
</ng-container>
<ng-container matColumnDef="orderType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Order Type</th>
<td mat-cell *matCellDef="let item">{{ item.orderType }}</td>
</ng-container>
<ng-container matColumnDef="carnetStatus">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Status</th>
<td mat-cell *matCellDef="let item">{{ getCarnetStatusLabel(item.carnetStatus) }}</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 data available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,63 @@
.dashboard-container {
padding: 24px;
.quick-actions {
display: flex;
justify-content: end;
button {
margin-bottom: 16px;
}
}
.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);
}
}
}
.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;
}
}
}

View File

@ -0,0 +1,113 @@
import { Component, ViewChild } from '@angular/core';
import { HomeService } from '../core/services/home.service';
import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { ChartComponent } from './chart/chart.component';
import { RouterLink } from '@angular/router';
import { NotificationService } from '../core/services/notification.service';
import { ApiErrorHandlerService } from '../core/services/api-error-handler.service';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { UserPreferences } from '../core/models/user-preference';
import { UserPreferencesService } from '../core/services/user-preference.service';
import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../shared/custom-paginator';
import { CommonService } from '../core/services/common.service';
import { CarnetStatus } from '../core/models/carnet-status';
@Component({
selector: 'app-home',
imports: [AngularMaterialModule, ChartComponent, RouterLink, CommonModule],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class HomeComponent {
carnetData: any[] = [];
isLoading = false;
showTable = false;
userPreferences: UserPreferences;
carnetStatuses: CarnetStatus[] = [];
dataSource = new MatTableDataSource<any>();
@ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort, { static: false })
set sort(value: MatSort) {
this.dataSource.sort = value;
}
displayedColumns: string[] = ['applicationName', 'holderName', 'carnetNumber', 'usSets', 'foreignSets', 'transitSets', 'carnetValue', 'issueDate', 'expiryDate', 'orderType', 'carnetStatus'];
constructor(
private homeService: HomeService,
private errorHandler: ApiErrorHandlerService,
private notificationService: NotificationService,
userPrefenceService: UserPreferencesService,
private commonService: CommonService) {
this.userPreferences = userPrefenceService.getPreferences();
}
ngOnInit(): void {
this.loadCarnetStatuses();
this.loadCarnetData();
}
loadCarnetStatuses(): void {
this.commonService.getCarnetStatuses().subscribe({
next: (carnetStatuses) => {
this.carnetStatuses = carnetStatuses;
},
error: (error) => {
console.error('Error loading carnet status:', error);
}
});
}
loadCarnetData(): void {
this.isLoading = true;
this.homeService.getCarnetSummaryData().subscribe({
next: (data) => {
this.carnetData = data;
this.isLoading = false;
},
error: (error) => {
console.error('Error loading carnet data:', error);
this.isLoading = false;
}
});
}
onCarnetStatusClick(event: any): void {
this.isLoading = true;
this.showTable = false;
this.homeService.getCarnetDataByStatus(event.spid, event.carnetStatus).subscribe({
next: (carnetDetails) => {
this.isLoading = false;
this.showTable = true;
this.dataSource.data = carnetDetails;
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load carnet data for the selected status');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading carnet data for the selected status:', error);
}
});
}
exportData() {
}
getCarnetStatusLabel(value: string): string {
const carnetStatus = this.carnetStatuses.find(t => t.value === value);
return carnetStatus ? carnetStatus.name : value;
}
}

View File

@ -0,0 +1,57 @@
<div class="login-container">
<div class="login-card">
<div class="logo-container">
<img src="images/logo.jpeg" alt="USCIB Logo" class="logo">
</div>
<h2 class="welcome-title">Welcome to USCIB Carnet Portal!</h2>
<h3 class="subtitle">ATA Carnet: Your Passport for Duty-Free Global Trade</h3>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="login-form">
<mat-form-field appearance="outline">
<mat-label>Username</mat-label>
<input matInput formControlName="username" required>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="loginForm.get('username')?.errors?.['required']">
Username is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" required>
<mat-icon matSuffix>lock</mat-icon>
<mat-error *ngIf="loginForm.get('password')?.errors?.['required']">
Password is required
</mat-error>
</mat-form-field>
<div class="forgot-password">
<a href="#" class="forgot-link">Forgot your password?</a>
</div>
<button mat-raised-button color="primary" type="submit" [disabled]="!loginForm.valid">
Sign In
</button>
<mat-error class="error-message" *ngIf="errorMessage !== ''">
{{errorMessage}}
</mat-error>
</form>
</div>
<div class="info-section">
<h3>ATA Carnet</h3>
<p>
Also known as the "Merchandise Passport," is an international customs document that simplifies temporary
exports to over 79 countries and territories.
It allows businesses to explore new markets, showcase products at trade shows, and attend global conferences
without paying duties or taxes.
</p>
<p>
It simplifies customs procedures for the temporary movement of goods and allows goods to
enter Customs territories of the ATA Carnet system free of customs duties and taxes for up to
one year.
</p>
</div>
</div>

View File

@ -0,0 +1,111 @@
@import 'colors';
.login-container {
display: flex;
min-height: 85vh;
background-color: #f5f5f5;
.login-card {
flex: 1;
max-width: 500px;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.logo-container {
text-align: center;
.logo {
max-width: 120px;
height: auto;
}
}
.welcome-title {
text-align: center;
color: $primary-color;
margin-bottom: 0.5rem;
}
.subtitle {
text-align: center;
color: #666;
font-size: 1rem;
margin-bottom: 2rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
mat-form-field {
width: 100%;
}
.forgot-password {
text-align: right;
margin-top: -1rem;
.forgot-link {
color: #666;
text-decoration: none;
font-size: 0.875rem;
&:hover {
text-decoration: underline;
}
}
}
button {
margin-top: 1rem;
padding: 0.5rem;
font-size: 1rem;
}
.error-message {
text-align: center;
}
}
}
.info-section {
flex: 1;
padding: 4rem;
background-color: $primary-color;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
h3 {
font-size: 2rem;
margin-bottom: 1.5rem;
}
p {
line-height: 1.6;
font-size: 1rem;
}
}
}
@media (max-width: 768px) {
.login-container {
flex-direction: column;
.login-card {
max-width: 100%;
padding: 1.5rem;
}
.info-section {
padding: 2rem;
display: none;
}
}
}

View File

@ -0,0 +1,63 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../core/services/auth.service';
import { CommonModule } from '@angular/common';
import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { UserService } from '../core/services/user.service';
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {
loginForm: FormGroup;
isLoading = false;
errorMessage = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private userService: UserService,
private router: Router
) {
this.loginForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
ngOnInit(): void {
if (this.userService.isLoggedIn()) {
this.router.navigate(['/home']);
}
}
onSubmit(): void {
if (this.loginForm.valid) {
this.isLoading = true;
this.errorMessage = '';
const { username, password } = this.loginForm.value;
this.authService.login(username, password).subscribe({
next: (response) => {
this.isLoading = false;
if (response?.msg) {
this.userService.setUser(username);
this.router.navigate(['/home']);
} else {
this.errorMessage = response?.error ?? response?.msg;
}
},
error: (error) => {
this.isLoading = false;
this.errorMessage = 'Invalid username or password';
console.error('Login error:', error);
}
});
}
}
}

View File

@ -0,0 +1,87 @@
<div class="service-provider-container">
<mat-stepper orientation="vertical" [linear]="true" (selectionChange)="onStepChange($event)"
[selectedIndex]="currentStep">
<!-- Basic Details Step -->
<mat-step [completed]="basicDetailsCompleted">
<ng-template matStepLabel>Basic Details</ng-template>
<app-basic-details [isEditMode]="isEditMode" (spidCreated)="onBasicDetailsSaved($event)">
</app-basic-details>
</mat-step>
<!-- Contacts Step -->
<mat-step [completed]="contactsCompleted" [editable]="!!serviceProviderId && basicDetailsCompleted">
<ng-template matStepLabel>Contacts</ng-template>
<app-contacts *ngIf="serviceProviderId" [spid]="serviceProviderId" (hasContacts)="onContactsSaved($event)"
[userPreferences]="userPreferences">
</app-contacts>
</mat-step>
<!-- Carnet Sequence Step -->
<mat-step [editable]="!!serviceProviderId && contactsCompleted" [completed]="carnetSequenceCompleted">
<ng-template matStepLabel>Carnet Sequence</ng-template>
<app-carnet-sequence *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasCarnetSequence)="onCarnetSequenceSaved($event)"
[userPreferences]="userPreferences"></app-carnet-sequence>
</mat-step>
<!-- Fees & Commission Step -->
<mat-step [editable]="!!serviceProviderId && carnetSequenceCompleted"
[completed]="feeCommissionCompleted && carnetSequenceCompleted">
<ng-template matStepLabel>Carnet Fee & Commission Setup</ng-template>
<app-carnet-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasFeeCommissions)="onFeeCommissionSaved($event)" [userPreferences]="userPreferences"></app-carnet-fee>
</mat-step>
<!-- Basic Fee Step -->
<mat-step [editable]="!!serviceProviderId && feeCommissionCompleted"
[completed]="basicFeeCompleted && feeCommissionCompleted">
<ng-template matStepLabel>Basic Fee Setup</ng-template>
<app-basic-fee *ngIf="serviceProviderId" [spid]="serviceProviderId" (hasBasicFees)="onBasicFeeSaved($event)"
[userPreferences]="userPreferences"></app-basic-fee>
</mat-step>
<!-- Counterfoil Fee Step -->
<mat-step [editable]="!!serviceProviderId && basicFeeCompleted" [completed]="counterfoilFeeCompleted">
<ng-template matStepLabel>Counterfoil Setup</ng-template>
<app-counterfoil-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasCounterFoilFee)="onCounterfoilFeeSaved($event)"
[userPreferences]="userPreferences"></app-counterfoil-fee>
</mat-step>
<!-- Continuation Sheet Fee Step -->
<mat-step [editable]="!!serviceProviderId && counterfoilFeeCompleted"
[completed]="continuationSheetFeeCompleted">
<ng-template matStepLabel>Continuation Sheet Fee Setup</ng-template>
<app-continuation-sheet-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasContinuationSheetFee)="onContinuationSheetFeeSaved($event)"
[userPreferences]="userPreferences"></app-continuation-sheet-fee>
</mat-step>
<!-- Expedited Fee Step -->
<mat-step [editable]="!!serviceProviderId && continuationSheetFeeCompleted" [completed]="expeditedFeeCompleted">
<ng-template matStepLabel>Expedited Fee Setup</ng-template>
<app-expedited-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasExpeditedFees)="onExpeditedFeeSaved($event)"
[userPreferences]="userPreferences"></app-expedited-fee>
</mat-step>
<!-- Security Deposit Step -->
<mat-step [editable]="!!serviceProviderId && expeditedFeeCompleted" [completed]="securityDepositCompleted">
<ng-template matStepLabel>Security Deposit</ng-template>
<app-security-deposit *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasSecurityDeposits)="onSecurityDepositSaved($event)"
[userPreferences]="userPreferences"></app-security-deposit>
</mat-step>
</mat-stepper>
</div>

View File

@ -0,0 +1,25 @@
@import 'colors';
.service-provider-container {
max-width: 1200px;
margin: 24px auto;
padding: 16px;
mat-stepper {
background-color: transparent;
}
.step-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 24px;
}
}
@media (max-width: 768px) {
.service-provider-container {
padding: 8px;
margin: 16px auto;
}
}

View File

@ -0,0 +1,85 @@
import { Component } from '@angular/core';
import { BasicDetailsComponent } from '../basic-details/basic-details.component';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { ContactsComponent } from '../contacts/contacts.component';
import { CarnetSequenceComponent } from '../carnet-sequence/carnet-sequence.component';
import { CarnetFeeComponent } from '../carnet-fee/carnet-fee.component';
import { BasicFeeComponent } from '../basic-fee/basic-fee.component';
import { CounterfoilFeeComponent } from '../counterfoil-fee/counterfoil-fee.component';
import { ExpeditedFeeComponent } from '../expedited-fee/expedited-fee.component';
import { SecurityDepositComponent } from '../security-deposit/security-deposit.component';
import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component";
import { UserPreferencesService } from '../../core/services/user-preference.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-add-service-provider',
imports: [BasicDetailsComponent, AngularMaterialModule, CommonModule, ContactsComponent, CarnetSequenceComponent,
CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, ContinuationSheetFeeComponent],
templateUrl: './add-service-provider.component.html',
styleUrl: './add-service-provider.component.scss'
})
export class AddServiceProviderComponent {
isEditMode = false;
serviceProviderId: number | null = null;
currentStep: number = 0;
isLoading: boolean = false;
userPreferences: UserPreferences;
basicDetailsCompleted: boolean = false;
contactsCompleted: boolean = false;
carnetSequenceCompleted: boolean = false;
feeCommissionCompleted: boolean = false;
basicFeeCompleted: boolean = false;
counterfoilFeeCompleted: boolean = false;
continuationSheetFeeCompleted: boolean = false;
expeditedFeeCompleted: boolean = false;
securityDepositCompleted: boolean = false;
constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
}
onBasicDetailsSaved(event: string): void {
this.serviceProviderId = +event;
this.basicDetailsCompleted = true;
}
onContactsSaved(event: boolean): void {
this.contactsCompleted = event;
}
onCarnetSequenceSaved(event: boolean): void {
this.carnetSequenceCompleted = event;
}
onFeeCommissionSaved(event: boolean): void {
this.feeCommissionCompleted = event;
}
onBasicFeeSaved(event: boolean): void {
this.basicFeeCompleted = event;
}
onCounterfoilFeeSaved(event: boolean): void {
this.counterfoilFeeCompleted = event;
}
onContinuationSheetFeeSaved(event: boolean): void {
this.continuationSheetFeeCompleted = event;
}
onExpeditedFeeSaved(event: boolean): void {
this.expeditedFeeCompleted = event;
}
onSecurityDepositSaved(event: boolean): void {
this.securityDepositCompleted = event;
}
onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex;
}
}

View File

@ -0,0 +1,186 @@
<div class="basic-details-container">
<mat-card class="details-card mat-elevation-z4">
<mat-card-content>
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<form [formGroup]="basicDetailsForm" class="details-form" *ngIf="!isLoading"
(ngSubmit)="saveBasicDetails()">
<!-- Company Information Row -->
<div class="form-row">
<mat-form-field appearance="outline" class="company-name">
<mat-label>Company Name</mat-label>
<input matInput formControlName="companyName" required>
<mat-icon matSuffix>business</mat-icon>
<mat-error *ngIf="f['companyName'].errors?.['required']">
Company name is required
</mat-error>
<mat-error *ngIf="f['companyName'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="lookup-code">
<mat-label>Lookup Code</mat-label>
<input matInput formControlName="lookupCode">
<mat-error *ngIf="f['lookupCode'].errors?.['maxlength']">
Maximum 20 characters allowed
</mat-error>
</mat-form-field>
</div>
<!-- Address Information Row -->
<div class="form-row">
<mat-form-field appearance="outline" class="address-line-1">
<mat-label>Address Line 1</mat-label>
<input matInput formControlName="address1" required>
<mat-error *ngIf="f['address1'].errors?.['required']">
Address is required
</mat-error>
<mat-error *ngIf="f['address1'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="address-line-2">
<mat-label>Address Line 2 (Optional)</mat-label>
<input matInput formControlName="address2">
<mat-error *ngIf="f['address2'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<!-- Location Information Row -->
<div class="form-row">
<mat-form-field appearance="outline" class="city">
<mat-label>City</mat-label>
<input matInput formControlName="city" required>
<mat-error *ngIf="f['city'].errors?.['required']">
City is required
</mat-error>
<mat-error *ngIf="f['city'].errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="country">
<mat-label>Country</mat-label>
<mat-select formControlName="country" required
(selectionChange)="onCountryChange($event.value)">
<mat-option *ngFor="let country of countries" [value]="country.value">
{{ country.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['country'].errors?.['required']">
Country is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="state">
<mat-label>State/Province</mat-label>
<mat-select formControlName="state" required>
<mat-option *ngFor="let state of states" [value]="state.value">
{{ state.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['state'].errors?.['required']">
State is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="zip">
<mat-label>ZIP/Postal Code</mat-label>
<input matInput formControlName="zip" required>
<mat-error *ngIf="f['zip'].errors?.['required']">
ZIP/Postal code is required
</mat-error>
<mat-error
*ngIf="f['country']?.value === 'US' && f['zip']?.touched && f['zip']?.errors?.['invalidUSZip']">
Please enter a valid 5-digit US ZIP code
</mat-error>
<mat-error
*ngIf="f['country']?.value === 'CA' && f['zip']?.touched && f['zip']?.errors?.['invalidCanadaPostal']">
Please enter a valid postal code (e.g., A1B2C3)
</mat-error>
</mat-form-field>
</div>
<!-- Region Information Row -->
<div class="form-row">
<mat-form-field appearance="outline" class="issuing-region">
<mat-label>Issuing Region</mat-label>
<mat-select formControlName="issuingRegion" required>
<mat-option *ngFor="let region of regions" [value]="region.region">
{{ region.regionname }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['issuingRegion'].errors?.['required']">
Issuing region is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="replacement-region">
<mat-label>Replacement Region</mat-label>
<mat-select formControlName="replacementRegion" required>
<mat-option *ngFor="let region of regions" [value]="region.region">
{{ region.regionname }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['replacementRegion'].errors?.['required']">
Replacement region is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="cargo-surety">
<mat-label>Cargo Surety</mat-label>
<mat-select formControlName="cargoSurety" required>
<mat-option *ngFor="let cargoSurety of cargoSuretys" [value]="cargoSurety.value">
{{ cargoSurety.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['cargoSurety'].errors?.['required']">
Cargo Surety is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="cargo-policy">
<mat-label>Cargo Policy</mat-label>
<mat-select formControlName="cargoPolicyNo" required>
<mat-option *ngFor="let cargoPolicy of cargoPolicies" [value]="cargoPolicy.value">
{{ cargoPolicy.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['cargoPolicyNo'].errors?.['required']">
Cargo Policy is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="bond-surety">
<mat-label>Bond Surety</mat-label>
<mat-select formControlName="bondSurety" required>
<mat-option *ngFor="let bondSurety of bondSuretys" [value]="bondSurety.value">
{{ bondSurety.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['bondSurety'].errors?.['required']">
Bond Surety is required
</mat-error>
</mat-form-field>
</div>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit" [disabled]="basicDetailsForm.invalid">
Save
</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,125 @@
@import 'colors';
@import 'mixins';
.basic-details-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
.details-card {
overflow: hidden;
transition: all 0.3s ease;
position: relative;
background: none;
box-shadow: none;
.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;
}
.details-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
align-items: start;
// Specific field sizing
.company-name {
grid-column: span 2;
}
.lookup-code {
grid-column: span 1;
}
.address-line-1,
.address-line-2 {
grid-column: span 3;
}
.city {
grid-column: span 1;
}
.country {
grid-column: span 1;
}
.state {
grid-column: span 1;
}
.zip {
grid-column: span 1;
}
.issuing-region,
.replacement-region {
grid-column: span 1;
}
.cargo-surety,
.cargo-policy,
.bond-surety {
grid-column: span 1;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
}
mat-form-field {
width: 100%;
mat-icon[matSuffix] {
color: $icon-color;
}
}
}
}
// Responsive adjustments
@media (max-width: 960px) {
.basic-details-container {
.details-card {
.form-row {
grid-template-columns: 1fr;
.company-name,
.address-line-1,
.address-line-2,
.lookup-code,
.city,
.country,
.state,
.zip,
.issuing-region,
.replacement-region,
.cargo-surety,
.cargo-policy,
.bond-surety {
grid-column: span 1;
}
}
}
}
}

View File

@ -0,0 +1,288 @@
import { Component, Input, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BasicDetail } from '../../core/models/service-provider/basic-detail';
import { Country } from '../../core/models/country';
import { Region } from '../../core/models/region';
import { State } from '../../core/models/state';
import { CommonService } from '../../core/services/common.service';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { NotificationService } from '../../core/services/notification.service';
import { ZipCodeValidator } from '../../shared/validators/zipcode-validator';
import { BasicDetailService } from '../../core/services/basic-detail.service';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { BondSurety } from '../../core/models/bond-surety';
import { CargoSurety } from '../../core/models/cargo-surety';
import { CargoPolicy } from '../../core/models/cargo-policy';
@Component({
selector: 'app-basic-details',
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
templateUrl: './basic-details.component.html',
styleUrls: ['./basic-details.component.scss']
})
export class BasicDetailsComponent implements OnInit, OnDestroy {
@Input() isEditMode = false;
@Input() spid: number = 0;
@Output() spidCreated = new EventEmitter<string>();
@Output() serviceProviderName = new EventEmitter<string>();
basicDetailsForm: FormGroup;
countries: Country[] = [];
regions: Region[] = [];
states: State[] = [];
bondSuretys: BondSurety[] = [];
cargoSuretys: CargoSurety[] = [];
cargoPolicies: CargoPolicy[] = [];
isLoading = true;
countriesHasStates = ['US', 'CA', 'MX'];
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
private commonService: CommonService,
private basicDetailService: BasicDetailService,
private notificationService: NotificationService,
private errorHandler: ApiErrorHandlerService
) {
this.basicDetailsForm = this.createForm();
}
ngOnInit(): void {
this.loadLookupData();
// this.spidCreated.emit(this.spid?.toString());
// Patch edit form data
if (this.spid > 0) {
this.basicDetailService.getBasicDetailsById(this.spid).subscribe({
next: (basicDetail: BasicDetail) => {
if (basicDetail?.spid > 0) {
this.patchFormData(basicDetail);
this.serviceProviderName.emit(basicDetail.companyName);
}
this.isLoading = false;
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic details');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading basic details:', error);
}
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
createForm(): FormGroup {
return this.fb.group({
companyName: ['', [Validators.required, Validators.maxLength(100)]],
lookupCode: ['', [Validators.maxLength(20)]],
address1: ['', [Validators.required, Validators.maxLength(100)]],
address2: ['', [Validators.maxLength(100)]],
city: ['', [Validators.required, Validators.maxLength(50)]],
country: ['', Validators.required],
state: ['', Validators.required],
zip: ['', [Validators.required, ZipCodeValidator('country')]],
issuingRegion: ['', Validators.required],
replacementRegion: ['', Validators.required],
cargoSurety: ['', Validators.required],
cargoPolicyNo: ['', Validators.required],
bondSurety: ['', Validators.required]
});
}
loadLookupData(): void {
this.commonService.getCountries(this.spid)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (countries) => {
this.countries = countries;
},
error: (error) => {
console.error('Failed to load countries', error);
this.isLoading = false;
}
});
this.loadRegions();
this.loadCargoSuretys();
this.loadCargoPolicies();
this.loadBondSuretys();
}
loadRegions(): void {
this.commonService.getRegions()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (regions) => {
this.regions = regions;
this.isLoading = false;
},
error: (error) => {
console.error('Failed to load regions', error);
this.isLoading = false;
}
});
}
loadStates(country: string): void {
this.isLoading = true;
country = this.countriesHasStates.includes(country) ? country : 'FN';
this.commonService.getStates(country, this.spid)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (states) => {
this.states = states;
const stateControl = this.basicDetailsForm.get('state');
if (this.countriesHasStates.includes(country)) {
stateControl?.enable();
} else {
stateControl?.disable();
stateControl?.setValue('FN');
}
this.isLoading = false;
},
error: (error) => {
console.error('Failed to load states', error);
this.isLoading = false;
}
});
}
loadBondSuretys(): void {
this.commonService.getBondSuretys()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (bondSuretys) => {
this.bondSuretys = bondSuretys;
this.isLoading = false;
},
error: (error) => {
console.error('Failed to load bond suretys', error);
this.isLoading = false;
}
});
}
loadCargoSuretys(): void {
this.commonService.getCargoSuretys()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (cargoSuretys) => {
this.cargoSuretys = cargoSuretys;
this.isLoading = false;
},
error: (error) => {
console.error('Failed to load cargo suretys', error);
this.isLoading = false;
}
});
}
loadCargoPolicies(): void {
this.commonService.getCargoPolicies()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (cargoPolicies) => {
this.cargoPolicies = cargoPolicies;
this.isLoading = false;
},
error: (error) => {
console.error('Failed to load cargo policies', error);
this.isLoading = false;
}
});
}
patchFormData(data: BasicDetail): void {
this.basicDetailsForm.patchValue({
companyName: data.companyName,
lookupCode: data.lookupCode,
address1: data.address1,
address2: data.address2,
city: data.city,
country: data.country,
state: data.state,
zip: data.zip,
issuingRegion: data.issuingRegion,
replacementRegion: data.replacementRegion,
cargoSurety: data.cargoSurety,
cargoPolicyNo: data.cargoPolicyNo,
bondSurety: data.bondSurety
});
if (data.country) {
this.loadStates(data.country);
}
if (this.isEditMode) {
this.basicDetailsForm.get('issuingRegion')?.disable();
this.basicDetailsForm.get('replacementRegion')?.disable();
this.basicDetailsForm.get('companyName')?.disable();
this.basicDetailsForm.get('cargoSurety')?.disable();
this.basicDetailsForm.get('bondSurety')?.disable();
}
}
onCountryChange(country: string): void {
this.basicDetailsForm.get('state')?.reset();
if (country) {
this.loadStates(country);
}
this.basicDetailsForm.get('zip')?.updateValueAndValidity();
}
// Convenience getter for easy access to form fields
get f() {
return this.basicDetailsForm.controls;
}
saveBasicDetails(): void {
if (this.basicDetailsForm.invalid) {
this.basicDetailsForm.markAllAsTouched();
return;
}
const basicDetailData: BasicDetail = this.basicDetailsForm.value;
// states
basicDetailData.state = this.basicDetailsForm.get('state')?.value;
// non editable fields values
if (this.isEditMode) {
basicDetailData.issuingRegion = this.basicDetailsForm.get('issuingRegion')?.value;
basicDetailData.replacementRegion = this.basicDetailsForm.get('replacementRegion')?.value;
basicDetailData.companyName = this.basicDetailsForm.get('companyName')?.value;
basicDetailData.cargoSurety = this.basicDetailsForm.get('cargoSurety')?.value;
basicDetailData.bondSurety = this.basicDetailsForm.get('bondSurety')?.value;
}
const saveObservable = this.isEditMode && this.spid > 0
? this.basicDetailService.updateBasicDetails(this.spid, basicDetailData)
: this.basicDetailService.createBasicDetails(basicDetailData);
saveObservable.subscribe({
next: (basicData: any) => {
this.notificationService.showSuccess(`Basic details ${this.isEditMode ? 'updated' : 'added'} successfully`);
if (!this.isEditMode) {
this.spidCreated.emit(basicData.SPID);
}
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditMode ? 'update' : 'add'} basic details`);
this.notificationService.showError(errorMessage);
console.error('Error saving contact:', error);
}
});
}
}

View File

@ -0,0 +1,146 @@
<div class="basic-fee-container">
<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>
<!-- Start Carnet Value Column -->
<ng-container matColumnDef="startCarnetValue">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Start Carnet Value</th>
<td mat-cell *matCellDef="let fee">{{ fee.startCarnetValue | number }}</td>
</ng-container>
<!-- End Carnet Value Column -->
<ng-container matColumnDef="endCarnetValue">
<th mat-header-cell *matHeaderCellDef mat-sort-header>End Carnet Value</th>
<td mat-cell *matCellDef="let fee">
{{ fee.endCarnetValue ? (fee.endCarnetValue | number) : 'Above' }}
</td>
</ng-container>
<!-- Fees Column -->
<ng-container matColumnDef="fees">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Fees</th>
<td mat-cell *matCellDef="let fee">{{ fee.fees | 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 fee">{{ fee.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 fee">
<button mat-icon-button color="primary" (click)="editFee(fee)" matTooltip="Edit">
<mat-icon>edit</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 basic fees available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Fee Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="feeForm" (ngSubmit)="saveFee()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Basic Fee' : 'Add New Basic Fee' }}</h3>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Start Carnet Value</mat-label>
<input matInput type="number" formControlName="startCarnetValue" required min="0" step="1">
<mat-error *ngIf="feeForm.get('startCarnetValue')?.errors?.['required']">
Start value is required
</mat-error>
<mat-error *ngIf="feeForm.get('startCarnetValue')?.errors?.['min']">
Value must be positive
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End Carnet Value (leave blank for no upper limit)</mat-label>
<input matInput type="number" formControlName="endCarnetValue" min="0" step="1">
<mat-error *ngIf="feeForm.get('endCarnetValue')?.errors?.['min']">
Value must be positive
</mat-error>
<mat-error *ngIf="feeForm.get('endCarnetValue')?.errors?.['invalidRange']">
Must be greater than start value
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Fees</mat-label>
<span class="dollar-prefix" matPrefix>$&nbsp;</span>
<input matInput type="number" formControlName="fees" required min="0" step="1">
<mat-error *ngIf="feeForm.get('fees')?.errors?.['required']">
Fees are required
</mat-error>
<mat-error *ngIf="feeForm.get('fees')?.errors?.['min']">
Fees must be positive
</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="feeForm.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-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="feeForm.invalid">
{{ isEditing ? 'Update' : 'Save' }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,156 @@
@import 'colors';
@import 'mixins';
.basic-fee-container {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
.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);
}
}
}
.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: $primary-color;
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
mat-form-field {
flex: 1;
}
}
.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;
}
.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.9375rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.basic-fee-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,228 @@
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { BasicFeeService } from '../../core/services/basic-fee.service';
import { NotificationService } from '../../core/services/notification.service';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { BasicFee } from '../../core/models/service-provider/basic-fee';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { CustomPaginator } from '../../shared/custom-paginator';
import { forkJoin } from 'rxjs';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-basic-fee',
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule, FormsModule],
templateUrl: './basic-fee.component.html',
styleUrl: './basic-fee.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class BasicFeeComponent {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['startCarnetValue', 'endCarnetValue', 'fees', 'effectiveDate', 'actions'];
dataSource = new MatTableDataSource<BasicFee>();
feeForm: FormGroup;
isEditing = false;
currentFeeId: number | null = null;
isLoading = false;
showForm = false;
readOnlyFields: any = {
lastChangedDate: null,
lastChangedBy: null
};
@Input() isEditMode = false;
@Input() userPreferences!: UserPreferences;
@Input() spid: number = 0;
@Output() hasBasicFees = new EventEmitter<boolean>();
constructor(
private fb: FormBuilder,
private basicFeeService: BasicFeeService,
private notificationService: NotificationService,
private dialog: MatDialog,
private errorHandler: ApiErrorHandlerService
) {
this.feeForm = this.fb.group({
startCarnetValue: [0, [Validators.required, Validators.min(0)]],
endCarnetValue: [null, [Validators.min(0)]],
fees: [0, [Validators.required, Validators.min(0)]],
effectiveDate: ['', Validators.required]
}, { validators: this.validateRange });
}
ngOnInit(): void {
this.loadBasicFees();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadBasicFees(): void {
this.isLoading = true;
this.basicFeeService.getBasicFees(this.spid).subscribe({
next: (fees) => {
this.dataSource.data = fees;
this.hasBasicFees.emit(fees.length > 0);
this.isLoading = false;
if (this.dataSource.data.length == 0) {
this.initializeDefaultFees();
}
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic fees');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading basic fees:', error);
}
});
}
initializeDefaultFees(): void {
this.isLoading = true;
const defaultFees: BasicFee[] = [
{ basicFeeId: 0, startCarnetValue: 1, endCarnetValue: 9999, fees: 255, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 10000, endCarnetValue: 49999, fees: 300, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 50000, endCarnetValue: 149999, fees: 365, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 150000, endCarnetValue: 399999, fees: 425, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 400000, endCarnetValue: 999999, fees: 480, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 1000000, endCarnetValue: null, fees: 545, effectiveDate: new Date() }
];
// Create an array of observables for each fee creation
const creationObservables = defaultFees.map(fee =>
this.basicFeeService.createBasicFee(this.spid, fee)
);
// Execute all creations in parallel and wait for all to complete
forkJoin(creationObservables).subscribe({
next: () => {
this.loadBasicFees(); // Refresh the list after all creations are done
this.isLoading = false;
},
error: (error) => {
this.isLoading = false;
console.error('Error initializing default fees:', error);
// Even if some failed, try to load what was created
this.loadBasicFees();
}
});
}
validateRange(formGroup: FormGroup): { [key: string]: any } | null {
const start = formGroup.get('startCarnetValue')?.value;
const end = formGroup.get('endCarnetValue')?.value;
if (end !== null && start !== null && end <= start) {
return { invalidRange: true };
}
return null;
}
// addNewFee(): void {
// this.showForm = true;
// this.isEditing = false;
// this.currentFeeId = null;
// this.feeForm.reset({
// startCarnetValue: 0,
// endCarnetValue: null,
// fees: 0
// });
// }
editFee(fee: BasicFee): void {
this.showForm = true;
this.isEditing = true;
this.currentFeeId = fee.basicFeeId;
this.feeForm.patchValue({
startCarnetValue: fee.startCarnetValue,
endCarnetValue: fee.endCarnetValue,
fees: fee.fees,
effectiveDate: fee.effectiveDate
});
this.readOnlyFields.lastChangedDate = fee.dateCreated;
this.readOnlyFields.lastChangedBy = fee.createdBy;
this.feeForm.get('startCarnetValue')?.disable();
this.feeForm.get('endCarnetValue')?.disable();
}
saveFee(): void {
if (this.feeForm.invalid) {
this.feeForm.markAllAsTouched();
return;
}
const feeData: BasicFee = {
basicFeeId: this.currentFeeId || 0,
startCarnetValue: this.feeForm.value.startCarnetValue,
endCarnetValue: this.feeForm.value.endCarnetValue,
fees: this.feeForm.value.fees,
effectiveDate: this.feeForm.value.effectiveDate,
spid: this.spid
};
const saveObservable = this.isEditing && this.currentFeeId
? this.basicFeeService.updateBasicFee(this.currentFeeId, feeData)
: this.basicFeeService.createBasicFee(this.spid, feeData);
saveObservable.subscribe({
next: () => {
this.notificationService.showSuccess(`Basic fee ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadBasicFees();
this.cancelEdit();
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} basic fee`);
this.notificationService.showError(errorMessage);
console.error('Error saving basic fee:', error);
}
});
}
// deleteFee(feeId: number): void {
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// width: '350px',
// data: {
// title: 'Confirm Delete',
// message: 'Are you sure you want to delete this basic fee?',
// confirmText: 'Delete',
// cancelText: 'Cancel'
// }
// });
// dialogRef.afterClosed().subscribe(result => {
// if (result) {
// this.basicFeeService.deleteBasicFee(feeId).subscribe({
// next: () => {
// this.notificationService.showSuccess('Basic fee deleted successfully');
// this.loadBasicFees();
// },
// error: (error) => {
// this.notificationService.showError('Failed to delete basic fee');
// console.error('Error deleting basic fee:', error);
// }
// });
// }
// });
// }
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentFeeId = null;
this.feeForm.reset();
}
}

View File

@ -0,0 +1,133 @@
<div class="fee-commission-container">
<div class="actions-bar">
<button mat-raised-button color="primary" *ngIf="!isEditMode" (click)="addNewFeeCommission()">
<mat-icon>add</mat-icon> Add New Fee & Commission
</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>
<!-- Fee Type Column -->
<ng-container matColumnDef="feeType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Fee Type</th>
<td mat-cell *matCellDef="let item">{{ getFeeTypeLabel(item.feeType) }}</td>
</ng-container>
<!-- Commission Rate Column -->
<ng-container matColumnDef="commissionRate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Commission Rate</th>
<td mat-cell *matCellDef="let item">{{ item.commissionRate }}</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)="editFeeCommission(item)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<!-- <button mat-icon-button color="warn" (click)="deleteFeeCommission(item.feeCommissionId)"
matTooltip="Delete" *ngIf="!isEditMode">
<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 fee & commission records available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Fee & Commission Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="feeCommissionForm" (ngSubmit)="saveFeeCommission()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Fee & Commission' : 'Add New Fee & Commission' }}</h3>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Fee Type</mat-label>
<mat-select formControlName="feeType" required>
<mat-option *ngFor="let type of feeTypes" [value]="type.id">
{{ type.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="feeCommissionForm.get('feeType')?.errors?.['required']">
Fee type is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Commission Rate</mat-label>
<input matInput type="number" formControlName="commissionRate">
<mat-error *ngIf="feeCommissionForm.get('commissionRate')?.errors?.['required']">
Commission rate is required
</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="feeCommissionForm.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-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="feeCommissionForm.invalid">
{{ isEditing ? 'Update' : 'Save' }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,166 @@
@import 'colors';
@import 'mixins';
.fee-commission-container {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
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: $primary-color;
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
mat-form-field {
flex: 1;
}
}
.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;
}
.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.9375rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.fee-commission-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,209 @@
import { Component, EventEmitter, 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 { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { NotificationService } from '../../core/services/notification.service';
import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator';
import { CommonService } from '../../core/services/common.service';
import { CarnetFee } from '../../core/models/service-provider/carnet-fee';
import { FeeType } from '../../core/models/fee-type';
import { CarnetFeeService } from '../../core/services/carnet-fee.service';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-carnet-fee',
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
templateUrl: './carnet-fee.component.html',
styleUrl: './carnet-fee.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class CarnetFeeComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['feeType', 'commissionRate', 'effectiveDate', 'actions'];
dataSource = new MatTableDataSource<CarnetFee>();
feeCommissionForm: FormGroup;
isEditing = false;
currentFeeCommissionId: number | null = null;
isLoading = false;
showForm = false;
feeTypes: FeeType[] = [];
readOnlyFields: any = {
lastChangedDate: null,
lastChangedBy: null
};
@Input() isEditMode = false;
@Input() userPreferences!: UserPreferences;
@Input() spid: number = 0;
@Output() hasFeeCommissions = new EventEmitter<boolean>();
constructor(
private fb: FormBuilder,
private feeCommissionService: CarnetFeeService,
private commonService: CommonService,
private notificationService: NotificationService,
private dialog: MatDialog,
private errorHandler: ApiErrorHandlerService
) {
this.feeCommissionForm = this.fb.group({
feeType: ['', Validators.required],
commissionRate: [0, [Validators.required]],
effectiveDate: ['', Validators.required]
});
}
ngOnInit(): void {
this.loadFeeCommissions();
this.loadFeeTypes();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadFeeCommissions(): void {
this.isLoading = true;
this.feeCommissionService.getFeeCommissions(this.spid).subscribe({
next: (fees) => {
this.dataSource.data = fees;
this.hasFeeCommissions.emit(fees.length > 0);
this.isLoading = false;
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load fee & commission data');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading fee & commission data:', error);
}
});
}
loadFeeTypes(): void {
this.commonService.getFeeTypes().subscribe({
next: (types) => {
this.feeTypes = types;
},
error: (error) => {
console.error('Error loading fee types:', error);
}
});
}
// 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();
// }
// }
addNewFeeCommission(): void {
this.showForm = true;
this.isEditing = false;
this.currentFeeCommissionId = null;
this.feeCommissionForm.reset();
this.feeCommissionForm.get('feeType')?.enable();
}
editFeeCommission(feeCommission: CarnetFee): void {
this.showForm = true;
this.isEditing = true;
this.currentFeeCommissionId = feeCommission.feeCommissionId;
this.feeCommissionForm.patchValue({
feeType: feeCommission.feeType,
commissionRate: feeCommission.commissionRate,
effectiveDate: feeCommission.effectiveDate
});
this.readOnlyFields.lastChangedDate = feeCommission.dateCreated;
this.readOnlyFields.lastChangedBy = feeCommission.createdBy;
this.feeCommissionForm.get('feeType')?.disable();
}
saveFeeCommission(): void {
if (this.feeCommissionForm.invalid) {
this.feeCommissionForm.markAllAsTouched();
return;
}
const formData = this.feeCommissionForm.value;
const feeCommissionData: CarnetFee = {
feeCommissionId: this.currentFeeCommissionId || 0,
feeType: formData.feeType,
commissionRate: formData.commissionRate,
effectiveDate: formData.effectiveDate,
spid: this.spid
};
const saveObservable = this.isEditing && this.currentFeeCommissionId
? this.feeCommissionService.updateFeeCommission(this.currentFeeCommissionId, feeCommissionData)
: this.feeCommissionService.createFeeCommission(this.spid, feeCommissionData);
saveObservable.subscribe({
next: () => {
this.notificationService.showSuccess(`Fee & commission ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadFeeCommissions();
this.cancelEdit();
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} fee & commission`);
this.notificationService.showError(errorMessage);
console.error('Error saving fee & commission:', error);
}
});
}
// deleteFeeCommission(feeCommissionId: number): void {
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// width: '350px',
// data: {
// title: 'Confirm Delete',
// message: 'Are you sure you want to delete this fee & commission record?',
// confirmText: 'Delete',
// cancelText: 'Cancel'
// }
// });
// dialogRef.afterClosed().subscribe(result => {
// if (result) {
// this.feeCommissionService.deleteFeeCommission(feeCommissionId).subscribe({
// next: () => {
// this.notificationService.showSuccess('Fee & commission record deleted successfully');
// this.loadFeeCommissions();
// },
// error: (error) => {
// this.notificationService.showError('Failed to delete fee & commission record');
// console.error('Error deleting fee & commission record:', error);
// }
// });
// }
// });
// }
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentFeeCommissionId = null;
this.feeCommissionForm.reset();
}
getFeeTypeLabel(value: string): string {
const type = this.feeTypes.find(t => t.id === value);
return type ? type.name : value;
}
}

View File

@ -0,0 +1,148 @@
<div class="carnet-sequence-container">
<div class="actions-bar">
<button mat-raised-button color="primary" *ngIf="!isEditMode" (click)="addNewSequence()">
<mat-icon>add</mat-icon> Add New Sequence
</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 sequence">{{ getCarnetTypeLabel(sequence.carnetType) }}</td>
</ng-container>
<!-- Region Column -->
<ng-container matColumnDef="region">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Region</th>
<td mat-cell *matCellDef="let sequence">{{ getRegionLabel(sequence.region) }}</td>
</ng-container>
<!-- Start Number Column -->
<ng-container matColumnDef="startNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Start Number</th>
<td mat-cell *matCellDef="let sequence">{{ sequence.startNumber | number }}</td>
</ng-container>
<!-- End Number Column -->
<ng-container matColumnDef="endNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>End Number</th>
<td mat-cell *matCellDef="let sequence">{{ sequence.endNumber | number }}</td>
</ng-container>
<!-- Last Number Column -->
<ng-container matColumnDef="lastNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Number</th>
<td mat-cell *matCellDef="let sequence">{{ sequence.lastNumber | number }}</td>
</ng-container>
<!-- Actions Column -->
<!-- <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let sequence">
<button mat-icon-button color="primary" (click)="editSequence(sequence)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteSequence(sequence.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 sequences available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Sequence Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="sequenceForm" (ngSubmit)="saveSequence()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Sequence' : 'Add New Sequence' }}</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="sequenceForm.get('carnetType')?.errors?.['required']">
Carnet type is required
</mat-error>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Region</mat-label>
<mat-select formControlName="region" required>
<mat-option *ngFor="let region of regions" [value]="region.id">
{{ region.regionname }}
</mat-option>
</mat-select>
<mat-error *ngIf="sequenceForm.get('region')?.errors?.['required']">
Region is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Start Number</mat-label>
<input matInput formControlName="startNumber" type="number" required>
<mat-error *ngIf="sequenceForm.get('startNumber')?.errors?.['required']">
Start number is required
</mat-error>
<mat-error *ngIf="sequenceForm.get('startNumber')?.errors?.['pattern']">
Must be a valid number
</mat-error>
<mat-error *ngIf="sequenceForm.get('startNumber')?.errors?.['min']">
Must be greater than 0
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End Number</mat-label>
<input matInput formControlName="endNumber" type="number" required>
<mat-error *ngIf="sequenceForm.get('endNumber')?.errors?.['required']">
End number is required
</mat-error>
<mat-error *ngIf="sequenceForm.get('endNumber')?.errors?.['pattern']">
Must be a valid number
</mat-error>
<mat-error *ngIf="sequenceForm.get('endNumber')?.errors?.['min']">
Must be greater than 0
</mat-error>
</mat-form-field>
</div>
<mat-error *ngIf="sequenceForm?.errors?.['invalidRange']" class="form-error">
End number must be greater than start number
</mat-error>
<div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="sequenceForm.invalid">
{{ isEditing ? 'Update' : 'Save' }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,159 @@
@import 'colors';
@import 'mixins';
.carnet-sequence-container {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
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;
}
.mat-column-startNumber,
.mat-column-endNumber,
.mat-column-lastNumber {
text-align: right;
padding-right: 24px;
}
}
.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: 1rem;
border-radius: 8px;
.form-header {
margin-bottom: 8px;
h3 {
margin: 0;
color: $primary-color;
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
mat-form-field {
flex: 1;
}
}
.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;
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.carnet-sequence-container {
padding: 16px;
.header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,245 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { CarnetSequence } from '../../core/models/service-provider/carnet-sequence';
import { Subject, takeUntil } from 'rxjs';
import { NotificationService } from '../../core/services/notification.service';
import { CommonService } from '../../core/services/common.service';
import { Region } from '../../core/models/region';
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 { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { CustomPaginator } from '../../shared/custom-paginator';
import { CarnetSequenceService } from '../../core/services/carnet-sequence.service';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-carnet-sequence',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './carnet-sequence.component.html',
styleUrl: './carnet-sequence.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class CarnetSequenceComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['carnetType', 'region', 'startNumber', 'endNumber', 'lastNumber'];
dataSource = new MatTableDataSource<CarnetSequence>();
sequenceForm: FormGroup;
isEditing = false;
currentSequenceId: string | null = null;
isLoading = false;
showForm = false;
carnetTypes = [
{ label: 'Original', value: 'ORIGINAL' },
{ label: 'Replacement', value: 'REPLACE' }
];
@Input() spid: number = 0;
@Input() userPreferences!: UserPreferences;
@Input() isEditMode = false;
@Output() hasCarnetSequence = new EventEmitter<boolean>();
private destroy$ = new Subject<void>();
sequences: CarnetSequence[] = [];
regions: Region[] = [];
constructor(
private fb: FormBuilder,
private carnetSequenceService: CarnetSequenceService,
private notificationService: NotificationService,
private commonService: CommonService,
private dialog: MatDialog,
private errorHandler: ApiErrorHandlerService
) {
this.sequenceForm = this.fb.group({
carnetType: ['ORIGINAL', Validators.required],
region: ['', Validators.required],
startNumber: ['', [
Validators.required,
Validators.pattern('^[0-9]*$'),
Validators.min(1)
]],
endNumber: ['', [
Validators.required,
Validators.pattern('^[0-9]*$'),
Validators.min(1)
]]
}, { validator: this.validateNumberRange });
}
ngOnInit(): void {
this.loadRegions();
this.loadSequences();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
private validateNumberRange(group: FormGroup): { [key: string]: any } | null {
const start = +group.get('startNumber')?.value;
const end = +group.get('endNumber')?.value;
return start && end && start >= end ? { invalidRange: true } : null;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
loadRegions(): void {
this.commonService.getRegions()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (regions) => {
this.regions = regions;
this.isLoading = false;
},
error: (error) => {
console.error('Failed to load regions', error);
this.isLoading = false;
}
});
}
private loadSequences(): void {
if (!this.spid) return;
this.isLoading = true;
this.carnetSequenceService.getCarnetSequenceById(this.spid).subscribe({
next: (carnetSequences: CarnetSequence[]) => {
// this.sequences = carnetSequences;
this.isLoading = false;
this.dataSource.data = carnetSequences;
this.isLoading = false;
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load sequences');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading sequences:', error);
}
});
}
addSequence(): void {
if (this.sequenceForm.invalid) {
this.sequenceForm.markAllAsTouched();
return;
}
const sequenceData = {
...this.sequenceForm.value,
spid: this.spid,
lastNumber: this.sequenceForm.value.startNumber
};
this.carnetSequenceService.createCarnetSequence(sequenceData).subscribe({
next: () => {
this.notificationService.showSuccess('Sequence added successfully');
this.loadSequences();
this.hasCarnetSequence.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to add sequence');
this.notificationService.showError(errorMessage);
console.error('Error adding sequence:', error);
}
});
}
// 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();
// }
// }
addNewSequence(): void {
this.showForm = true;
this.isEditing = false;
this.currentSequenceId = null;
this.sequenceForm.reset({ carnetType: 'ORIGINAL' });
}
saveSequence(): void {
if (this.sequenceForm.invalid) {
this.sequenceForm.markAllAsTouched();
return;
}
const sequenceData: CarnetSequence = {
...this.sequenceForm.value,
spid: this.spid
};
this.carnetSequenceService.createCarnetSequence(sequenceData)
.subscribe({
next: () => {
this.notificationService.showSuccess('Sequence added successfully');
this.loadSequences();
this.cancelEdit();
this.hasCarnetSequence.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to add sequence`);
this.notificationService.showError(errorMessage);
console.error('Error adding sequence:', error);
}
});
}
// deleteSequence(sequenceId: string): void {
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// width: '350px',
// data: {
// title: 'Confirm Delete',
// message: 'Are you sure you want to delete this sequence?',
// confirmText: 'Delete',
// cancelText: 'Cancel'
// }
// });
// dialogRef.afterClosed().subscribe(result => {
// if (result) {
// this.carnetSequenceService.deleteSequence(sequenceId).subscribe({
// next: () => {
// this.notificationService.showSuccess('Sequence deleted successfully');
// this.loadSequences();
// this.saved.emit(true);
// },
// error: (error) => {
// this.notificationService.showError('Failed to delete sequence');
// }
// });
// }
// });
// }
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentSequenceId = null;
this.sequenceForm.reset({ carnetType: 'ORIGINAL' });
}
getCarnetTypeLabel(type: string): string {
return this.carnetTypes.find(t => t.value === type)?.label || type;
}
getRegionLabel(type: number): string {
return this.regions.find(t => t.id === type)?.regionname || type.toString();
}
}

View File

@ -0,0 +1,253 @@
<div class="contacts-container">
<div class="actions-bar">
<!-- <mat-form-field appearance="outline">
<mat-icon matPrefix>search</mat-icon>
<input matInput (keyup)="applyFilter($event)" placeholder="Search contacts...">
</mat-form-field> -->
<button mat-raised-button color="primary" (click)="addNewContact()">
<mat-icon>add</mat-icon> Add New Contact
</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>
<!-- First Name Column -->
<ng-container matColumnDef="firstName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>
<td mat-cell *matCellDef="let contact">{{ contact.firstName }}</td>
</ng-container>
<!-- Last Name Column -->
<ng-container matColumnDef="lastName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>
<td mat-cell *matCellDef="let contact">{{ contact.lastName }}</td>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Title</th>
<td mat-cell *matCellDef="let contact">{{ contact.title }}</td>
</ng-container>
<!-- Phone Column -->
<ng-container matColumnDef="phone">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Phone</th>
<td mat-cell *matCellDef="let contact">{{ contact.phone | phone }}</td>
</ng-container>
<!-- Email Column -->
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
<td mat-cell *matCellDef="let contact">{{ contact.email }}</td>
</ng-container>
<!-- Default Contact Column -->
<ng-container matColumnDef="defaultContact">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Default</th>
<td mat-cell *matCellDef="let contact">
<mat-icon [color]="contact.defaultContact ? 'primary' : ''">
{{ contact.defaultContact ? 'star' : 'star_border' }}
</mat-icon>
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let contact">
<button mat-icon-button color="primary" (click)="editContact(contact)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" *ngIf="!contact.defaultContact || !contact.isInactive" (click)="
deleteContact(contact.spContactId)" [hidden]="contact.defaultContact || contact.isInactive"
matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button>
<!-- <button mat-icon-button (click)="setDefaultContact(contact.contactId)"
[color]="contact.defaultContact ? 'primary' : ''" matTooltip="Set as default">
<mat-icon>star</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 records available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Contact Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="contactForm" (ngSubmit)="saveContact()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Contact' : 'Add New Contact' }}</h3>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>First Name</mat-label>
<input matInput formControlName="firstName" required>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="contactForm.get('firstName')?.errors?.['required']">
First name is required
</mat-error>
<mat-error *ngIf="contactForm.get('firstName')?.errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="small-field">
<mat-label>Middle Initial</mat-label>
<input matInput formControlName="middleInitial" maxlength="1">
<mat-error *ngIf="contactForm.get('middleInitial')?.errors?.['maxlength']">
Only 1 character allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Last Name</mat-label>
<input matInput formControlName="lastName" required>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="contactForm.get('lastName')?.errors?.['required']">
Last name is required
</mat-error>
<mat-error *ngIf="contactForm.get('lastName')?.errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Title</mat-label>
<input matInput formControlName="title" required>
<mat-icon matSuffix>work</mat-icon>
<mat-error *ngIf="contactForm.get('title')?.errors?.['required']">
Title is required
</mat-error>
<mat-error *ngIf="contactForm.get('title')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Phone</mat-label>
<input matInput formControlName="phone" required>
<mat-icon matSuffix>phone</mat-icon>
<mat-error *ngIf="contactForm.get('phone')?.errors?.['required']">
Phone is required
</mat-error>
<mat-error *ngIf="contactForm.get('phone')?.errors?.['pattern']">
Please enter a valid phone number (10-15 digits)
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Mobile</mat-label>
<input matInput formControlName="mobile">
<mat-icon matSuffix>smartphone</mat-icon>
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['required']">
Mobile is required
</mat-error>
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['pattern']">
Please enter a valid mobile number (10-15 digits)
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Fax</mat-label>
<input matInput formControlName="fax">
<mat-icon matSuffix>fax</mat-icon>
<mat-error *ngIf="contactForm.get('fax')?.errors?.['required']">
Fax is required
</mat-error>
<mat-error *ngIf="contactForm.get('fax')?.errors?.['pattern']">
Please enter a valid fax number (10-15 digits)
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" required>
<mat-icon matSuffix>email</mat-icon>
<mat-error *ngIf="contactForm.get('email')?.errors?.['required']">
Email is required
</mat-error>
<mat-error *ngIf="contactForm.get('email')?.errors?.['email']">
Please enter a valid email address
</mat-error>
<mat-error *ngIf="contactForm.get('email')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<!--
<div class="form-row">
<mat-checkbox formControlName="defaultContact">Default Contact</mat-checkbox>
</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">
{{contactReadOnlyFields.lastChangedBy || 'N/A'}}
</div>
</div>
<!-- Inactive status -->
<div class="readonly-field">
<label>Inactive Status </label>
<div class="readonly-value">
{{contactReadOnlyFields.isInactive === true ? 'Yes' : 'No' }}
</div>
</div>
</div>
<div class="field-column">
<!-- Last Changed Date -->
<div class="readonly-field">
<label>Last Changed Date</label>
<div class="readonly-value">
{{(contactReadOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
</div>
</div>
<!-- Inactivated Date -->
<div class="readonly-field">
<label>Inactivated Date</label>
<div class="readonly-value">
{{(contactReadOnlyFields.inactivatedDate | date:'mediumDate':'UTC') || 'N/A'}}
</div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="contactForm.invalid">
{{ isEditing ? 'Update' : 'Save' }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,171 @@
@import 'colors';
@import 'mixins';
.contacts-container {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
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: 180px;
text-align: center;
}
.mat-column-defaultContact {
width: 80px;
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: $primary-color;
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
mat-form-field {
flex: 1;
}
.small-field {
max-width: 120px;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 16px;
}
.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.9375rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.contacts-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
.small-field {
max-width: 100% !important;
}
}
}
}

View File

@ -0,0 +1,209 @@
import { Component, EventEmitter, 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 { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { NotificationService } from '../../core/services/notification.service';
import { Contact } from '../../core/models/service-provider/contact';
import { PhonePipe } from '../../shared/pipes/phone.pipe';
import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator';
import { ContactService } from '../../core/services/contact.service';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-contacts',
imports: [AngularMaterialModule, ReactiveFormsModule, PhonePipe, CommonModule],
templateUrl: './contacts.component.html',
styleUrl: './contacts.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class ContactsComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'email', 'defaultContact', 'actions'];
dataSource = new MatTableDataSource<any>();
contactForm: FormGroup;
isEditing = false;
currentContactId: number | null = null;
isLoading = false;
showForm = false;
contactReadOnlyFields: any = {
lastChangedDate: null,
lastChangedBy: null,
isInactive: null,
inactivatedDate: null
};
@Input() spid: number = 0;
@Input() userPreferences: UserPreferences = {};
@Output() hasContacts = new EventEmitter<boolean>();
constructor(
private fb: FormBuilder,
private contactService: ContactService,
private notificationService: NotificationService,
private dialog: MatDialog,
private errorHandler: ApiErrorHandlerService
) {
this.contactForm = this.fb.group({
firstName: ['', [Validators.required, Validators.maxLength(50)]],
lastName: ['', [Validators.required, Validators.maxLength(50)]],
middleInitial: ['', [Validators.maxLength(1)]],
title: ['', [Validators.required, Validators.maxLength(100)]],
phone: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
fax: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]],
defaultContact: [false]
});
}
ngOnInit(): void {
this.loadContacts();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadContacts(): void {
this.isLoading = true;
this.contactService.getContactsById(this.spid).subscribe({
next: (contacts: Contact[]) => {
this.dataSource.data = contacts;
this.isLoading = false;
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading contacts:', error);
}
});
}
// 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();
// }
// }
addNewContact(): void {
this.showForm = true;
this.isEditing = false;
this.currentContactId = null;
this.contactForm.reset();
// this.contactForm.patchValue({ defaultContact: false });
}
editContact(contact: Contact): void {
this.showForm = true;
this.isEditing = true;
this.currentContactId = contact.spContactId;
this.contactForm.patchValue({
firstName: contact.firstName,
lastName: contact.lastName,
middleInitial: contact.middleInitial,
title: contact.title,
phone: contact.phone,
mobile: contact.mobile,
fax: contact.fax,
email: contact.email
// defaultContact: contact.defaultContact
});
this.contactReadOnlyFields.lastChangedDate = contact.lastUpdatedDate ?? contact.dateCreated;
this.contactReadOnlyFields.lastChangedBy = contact.lastUpdatedBy ?? contact.createdBy;
this.contactReadOnlyFields.isInactive = contact.isInactive;
this.contactReadOnlyFields.inactivatedDate = contact.inactivatedDate;
}
saveContact(): void {
if (this.contactForm.invalid) {
this.contactForm.markAllAsTouched();
return;
}
// default the first contact
const contactData: Contact = this.contactForm.value;
contactData.defaultContact = this.dataSource?.data?.length === 0;
const saveObservable = this.isEditing && (this.currentContactId! > 0)
? this.contactService.updateContact(this.currentContactId!, contactData)
: this.contactService.createContact(this.spid, contactData);
saveObservable.subscribe({
next: () => {
this.notificationService.showSuccess(`Contact ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadContacts();
this.cancelEdit();
this.hasContacts.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} contact`);
this.notificationService.showError(errorMessage);
console.error('Error saving contact:', error);
}
});
}
deleteContact(contactId: string): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '350px',
data: {
title: 'Confirm Delete',
message: 'Are you sure you want to delete this contact?',
confirmText: 'Delete',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.contactService.deleteContact(contactId).subscribe({
next: () => {
this.notificationService.showSuccess('Contact deleted successfully');
this.loadContacts();
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete contact');
this.notificationService.showError(errorMessage);
console.error('Error deleting contact:', error);
}
});
}
});
}
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentContactId = null;
this.contactForm.reset();
}
// setDefaultContact(contactId: string): void {
// this.contactService.setDefaultServiceProviderContact(this.spid, contactId).subscribe({
// next: () => {
// this.notificationService.showSuccess('Default contact updated successfully');
// this.loadContacts();
// },
// error: (error) => {
// this.notificationService.showError('Failed to set default contact');
// console.error('Error setting default contact:', error);
// }
// });
// }
}

View File

@ -0,0 +1,161 @@
<div class="continuation-sheet-container">
<div class="actions-bar">
<button mat-raised-button color="primary" (click)="addNewContinuationSheet()">
<mat-icon>add</mat-icon> Add New Continuation Sheet
</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>
<!-- Customer Type Column -->
<ng-container matColumnDef="customerType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Customer Type</th>
<td mat-cell *matCellDef="let item">
{{ getOptionLabel(customerTypes, item.customerType) }}
</td>
</ng-container>
<!-- 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>
<!-- 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)="editContinuationSheet(item)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<!-- <button mat-icon-button color="warn" (click)="deleteContinuationSheet(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 continuation sheet setups available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Continuation Sheet Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="continuationSheetForm" (ngSubmit)="saveContinuationSheet()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Continuation Sheet' : 'Add New Continuation Sheet' }}</h3>
</div>
<div class="form-row">
<mat-label>Customer Type</mat-label>
<mat-radio-group formControlName="customerType" required class="horizontal-group">
<mat-radio-button *ngFor="let type of customerTypes" [value]="type.value" class="radio-button">
{{ type.label }}
</mat-radio-button>
</mat-radio-group>
<mat-error *ngIf="continuationSheetForm.get('customerType')?.errors?.['required']">
Customer type is required
</mat-error>
</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">
{{ type.label }}
</mat-radio-button>
</mat-radio-group>
<mat-error *ngIf="continuationSheetForm.get('carnetType')?.errors?.['required']" class="radio-button">
Carnet type is required
</mat-error>
</div>
<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="continuationSheetForm.get('rate')?.errors?.['required']">
Rate is required
</mat-error>
<mat-error *ngIf="continuationSheetForm.get('rate')?.errors?.['pattern']">
Must be a valid dollar amount
</mat-error>
<mat-error *ngIf="continuationSheetForm.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="continuationSheetForm.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-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="continuationSheetForm.invalid">
{{ isEditing ? 'Update' : 'Save' }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,184 @@
@import 'colors';
@import 'mixins';
.continuation-sheet-container {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
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: $primary-color;
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
mat-form-field {
flex: 1;
}
}
.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.9375rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.continuation-sheet-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,213 @@
import { Component, EventEmitter, 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 { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { NotificationService } from '../../core/services/notification.service';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { of } from 'rxjs';
import { CustomPaginator } from '../../shared/custom-paginator';
import { ContinuationSheetFeeService } from '../../core/services/continuation-sheet-fee.service';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-continuation-sheet-fee',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './continuation-sheet-fee.component.html',
styleUrl: './continuation-sheet-fee.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class ContinuationSheetFeeComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['customerType', 'carnetType', 'rate', 'effectiveDate', 'actions'];
dataSource = new MatTableDataSource<any>();
continuationSheetForm: FormGroup;
isEditing = false;
currentContinuationSheetId: number | null = null;
isLoading = false;
showForm = false;
readOnlyFields: any = {
lastChangedDate: null,
lastChangedBy: null
};
// Dropdown options
customerTypes = [
{ label: 'Preparer', value: 'PREPARER' },
{ label: 'Self Issuer', value: 'SELFISSUER' }
];
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() hasContinuationSheetFee = new EventEmitter<boolean>();
constructor(
private fb: FormBuilder,
private continuationSheetFeeService: ContinuationSheetFeeService,
private notificationService: NotificationService,
private dialog: MatDialog,
private errorHandler: ApiErrorHandlerService
) {
this.continuationSheetForm = this.fb.group({
customerType: ['PREPARER', Validators.required],
carnetType: ['ORIGINAL', Validators.required],
rate: ['', [
Validators.required,
Validators.pattern(/^\d+\.?\d{0,2}$/),
Validators.min(0)
]],
effectiveDate: ['', Validators.required]
});
}
ngOnInit(): void {
this.loadContinuationSheets();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadContinuationSheets(): void {
if (!this.spid) return;
this.isLoading = true;
this.continuationSheetFeeService.getContinuationSheets(this.spid).subscribe({
next: (continuationSheets) => {
this.dataSource.data = continuationSheets;
this.isLoading = false;
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load continuation sheets');
this.notificationService.showError(errorMessage);
this.isLoading = false;
return of([]);
}
});
}
// 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();
// }
// }
addNewContinuationSheet(): void {
this.showForm = true;
this.isEditing = false;
this.currentContinuationSheetId = null;
this.continuationSheetForm.reset({
customerType: 'PREPARER',
carnetType: 'ORIGINAL'
});
this.continuationSheetForm.get('customerType')?.enable();
this.continuationSheetForm.get('carnetType')?.enable();
}
editContinuationSheet(continuationSheet: any): void {
this.showForm = true;
this.isEditing = true;
this.currentContinuationSheetId = continuationSheet.id;
this.continuationSheetForm.patchValue({
customerType: continuationSheet.customerType,
carnetType: continuationSheet.carnetType,
rate: continuationSheet.rate,
effectiveDate: new Date(continuationSheet.effectiveDate)
});
this.readOnlyFields.lastChangedDate = continuationSheet.dateCreated;
this.readOnlyFields.lastChangedBy = continuationSheet.createdBy;
this.continuationSheetForm.get('customerType')?.disable();
this.continuationSheetForm.get('carnetType')?.disable();
}
saveContinuationSheet(): void {
if (this.continuationSheetForm.invalid) {
this.continuationSheetForm.markAllAsTouched();
return;
}
const continuationSheetData = {
...this.continuationSheetForm.value,
spid: this.spid
};
const saveObservable = this.isEditing && this.currentContinuationSheetId
? this.continuationSheetFeeService.updateContinuationSheet(this.currentContinuationSheetId, continuationSheetData)
: this.continuationSheetFeeService.addContinuationSheet(this.spid, continuationSheetData);
saveObservable.subscribe({
next: () => {
this.notificationService.showSuccess(`Continuation Sheet ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadContinuationSheets();
this.cancelEdit();
this.hasContinuationSheetFee.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} continuation sheet`);
this.notificationService.showError(errorMessage);
return of(null);
}
});
}
// deleteContinuationSheet(continuationSheetId: string): void {
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// width: '350px',
// data: {
// title: 'Confirm Delete',
// message: 'Are you sure you want to delete this continuation sheet setup?',
// confirmText: 'Delete',
// cancelText: 'Cancel'
// }
// });
// dialogRef.afterClosed().subscribe(result => {
// if (result) {
// this.continuationSheetFeeService.deleteContinuationSheet(continuationSheetId).subscribe({
// next: () => {
// this.notificationService.showSuccess('Continuation sheet deleted successfully');
// this.loadContinuationSheets();
// },
// error: (error) => {
// this.notificationService.showError('Failed to delete continuation sheet');
// }
// });
// }
// });
// }
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentContinuationSheetId = null;
this.continuationSheetForm.reset({
customerType: 'PREPARER',
carnetType: 'ORIGINAL'
});
}
getOptionLabel(options: any[], value: string): string {
return options.find(opt => opt.value === value)?.label || value;
}
}

View File

@ -0,0 +1,207 @@
<div class="counterfoil-container">
<div class="actions-bar">
<button mat-raised-button color="primary" (click)="addNewCounterfoil()">
<mat-icon>add</mat-icon> Add New Counterfoil
</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>
<!-- Customer Type Column -->
<ng-container matColumnDef="customerType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Customer Type</th>
<td mat-cell *matCellDef="let item">
{{ getOptionLabel(customerTypes, item.customerType) }}
</td>
</ng-container>
<!-- 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)="editCounterfoil(item)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<!-- <button mat-icon-button color="warn" (click)="deleteCounterfoil(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 counterfoil setups available</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Counterfoil Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="counterfoilForm" (ngSubmit)="saveCounterfoil()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Counterfoil Setup' : 'Add New Counterfoil Setup' }}</h3>
</div>
<div class="form-row">
<mat-label>Customer Type</mat-label>
<mat-radio-group formControlName="customerType" required class="horizontal-group">
<mat-radio-button *ngFor="let type of customerTypes" [value]="type.value" class="radio-button">
{{ type.label }}
</mat-radio-button>
</mat-radio-group>
<mat-error *ngIf="counterfoilForm.get('customerType')?.errors?.['required']">
Customer type is required
</mat-error>
</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="counterfoilForm.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="counterfoilForm.get('startSets')?.errors?.['required']">
Start sets is required
</mat-error>
<mat-error *ngIf="counterfoilForm.get('startSets')?.errors?.['pattern']">
Must be a valid number
</mat-error>
<mat-error *ngIf="counterfoilForm.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="counterfoilForm.get('endSets')?.errors?.['required']">
End sets is required
</mat-error>
<mat-error *ngIf="counterfoilForm.get('endSets')?.errors?.['pattern']">
Must be a valid number
</mat-error>
<mat-error *ngIf="counterfoilForm.get('endSets')?.errors?.['min']">
Must be greater than 0
</mat-error>
</mat-form-field>
</div>
<mat-error *ngIf="counterfoilForm?.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="counterfoilForm.get('rate')?.errors?.['required']">
Rate is required
</mat-error>
<mat-error *ngIf="counterfoilForm.get('rate')?.errors?.['pattern']">
Must be a valid dollar amount
</mat-error>
<mat-error *ngIf="counterfoilForm.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="counterfoilForm.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-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="counterfoilForm.invalid">
{{ isEditing ? 'Update' : 'Save' }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,184 @@
@import 'colors';
@import 'mixins';
.counterfoil-container {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
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: $primary-color;
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
mat-form-field {
flex: 1;
}
}
.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.9375rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.counterfoil-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,237 @@
import { Component, EventEmitter, 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 { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { NotificationService } from '../../core/services/notification.service';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { CounterfoilFee } from '../../core/models/service-provider/counterfoil-fee';
import { CustomPaginator } from '../../shared/custom-paginator';
import { CounterfoilFeeService } from '../../core/services/counterfoil-fee.service';
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
import { UserPreferences } from '../../core/models/user-preference';
@Component({
selector: 'app-counterfoil-fee',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './counterfoil-fee.component.html',
styleUrl: './counterfoil-fee.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class CounterfoilFeeComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['customerType', 'carnetType', 'startSets', 'endSets', 'rate', 'effectiveDate', 'actions'];
dataSource = new MatTableDataSource<any>();
counterfoilForm: FormGroup;
isEditing = false;
currentCounterfoilId: number | null = null;
isLoading = false;
showForm = false;
readOnlyFields: any = {
lastChangedDate: null,
lastChangedBy: null
};
// Dropdown options
customerTypes = [
{ label: 'Preparer', value: 'PREPARER' },
{ label: 'Self Issuer', value: 'SELFISSUER' }
];
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() hasCounterFoilFee = new EventEmitter<boolean>();
constructor(
private fb: FormBuilder,
private counterfoilFeeService: CounterfoilFeeService,
private notificationService: NotificationService,
private dialog: MatDialog,
private errorHandler: ApiErrorHandlerService
) {
this.counterfoilForm = this.fb.group({
customerType: ['PREPARER', Validators.required],
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.loadCounterfoils();
}
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;
}
loadCounterfoils(): void {
if (!this.spid) return;
this.isLoading = true;
this.counterfoilFeeService.getCounterfoils(this.spid)
.subscribe({
next: (
counterfoils: CounterfoilFee[]) => {
this.dataSource.data = counterfoils;
this.isLoading = false;
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load counterfoils');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading counterfoils:', error);
}
});
}
// 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();
// }
// }
addNewCounterfoil(): void {
this.showForm = true;
this.isEditing = false;
this.currentCounterfoilId = null;
this.counterfoilForm.reset({
customerType: 'PREPARER',
carnetType: 'ORIGINAL'
});
this.counterfoilForm.get('customerType')?.enable();
this.counterfoilForm.get('carnetType')?.enable();
this.counterfoilForm.get('startSets')?.enable();
this.counterfoilForm.get('endSets')?.enable();
}
editCounterfoil(counterfoil: any): void {
this.showForm = true;
this.isEditing = true;
this.currentCounterfoilId = counterfoil.id;
this.counterfoilForm.patchValue({
customerType: counterfoil.customerType,
carnetType: counterfoil.carnetType,
startSets: counterfoil.startSets,
endSets: counterfoil.endSets,
rate: counterfoil.rate,
effectiveDate: new Date(counterfoil.effectiveDate)
});
this.readOnlyFields.lastChangedDate = counterfoil.dateCreated;
this.readOnlyFields.lastChangedBy = counterfoil.createdBy;
this.counterfoilForm.get('customerType')?.disable();
this.counterfoilForm.get('carnetType')?.disable();
this.counterfoilForm.get('startSets')?.disable();
this.counterfoilForm.get('endSets')?.disable();
}
saveCounterfoil(): void {
if (this.counterfoilForm.invalid) {
this.counterfoilForm.markAllAsTouched();
return;
}
const counterfoilData = {
...this.counterfoilForm.value,
spid: this.spid
};
const saveObservable = this.isEditing && this.currentCounterfoilId
? this.counterfoilFeeService.updateCounterfoil(this.currentCounterfoilId, counterfoilData)
: this.counterfoilFeeService.addCounterfoil(this.spid, counterfoilData);
saveObservable.subscribe({
next: () => {
this.notificationService.showSuccess(`Counterfoil ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadCounterfoils();
this.cancelEdit();
this.hasCounterFoilFee.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} counterfoil`);
this.notificationService.showError(errorMessage);
console.error('Error saving counterfoil:', error);
}
});
}
// deleteCounterfoil(counterfoilId: string): void {
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// width: '350px',
// data: {
// title: 'Confirm Delete',
// message: 'Are you sure you want to delete this counterfoil setup?',
// confirmText: 'Delete',
// cancelText: 'Cancel'
// }
// });
// dialogRef.afterClosed().subscribe(result => {
// if (result) {
// this.counterfoilFeeService.deleteCounterfoil(counterfoilId).subscribe({
// next: () => {
// this.notificationService.showSuccess('Counterfoil deleted successfully');
// this.loadCounterfoils();
// },
// error: (error) => {
// this.notificationService.showError('Failed to delete counterfoil');
// }
// });
// }
// });
// }
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentCounterfoilId = null;
this.counterfoilForm.reset({
customerType: 'PREPARER',
carnetType: 'ORIGINAL'
});
}
getOptionLabel(options: any[], value: string): string {
return options.find(opt => opt.value === value)?.label || value;
}
}

View File

@ -0,0 +1,76 @@
<h2 *ngIf="this.serviceProviderName" class="page-header">Manage {{this.serviceProviderName}}</h2>
<div class="service-provider-action-buttons">
<button mat-button (click)="accordion().openAll()">Expand All</button>
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
</div>
<mat-accordion class="service-provider-headers-align" multi>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Basic Details </mat-panel-title>
</mat-expansion-panel-header>
<app-basic-details [spid]="spid" [isEditMode]="isEditMode"
(serviceProviderName)="onServiceProviderNameUpdate($event)"></app-basic-details>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Contacts </mat-panel-title>
</mat-expansion-panel-header>
<app-contacts [spid]="spid" [userPreferences]="userPreferences"></app-contacts>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Carnet Sequence </mat-panel-title>
</mat-expansion-panel-header>
<app-carnet-sequence [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-carnet-sequence>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Carnet Fee & Commission Setup </mat-panel-title>
</mat-expansion-panel-header>
<app-carnet-fee [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences"></app-carnet-fee>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Basic Fee Setup</mat-panel-title>
</mat-expansion-panel-header>
<app-basic-fee [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences"></app-basic-fee>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Counterfoil Setup</mat-panel-title>
</mat-expansion-panel-header>
<app-counterfoil-fee [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-counterfoil-fee>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Continuation Sheet Fee Setup </mat-panel-title>
</mat-expansion-panel-header>
<app-continuation-sheet-fee [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-continuation-sheet-fee>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Expedited Fee Setup</mat-panel-title>
</mat-expansion-panel-header>
<app-expedited-fee [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-expedited-fee>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Security Deposit </mat-panel-title>
</mat-expansion-panel-header>
<app-security-deposit [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-security-deposit>
</mat-expansion-panel>
</mat-accordion>

View File

@ -0,0 +1,20 @@
@import 'colors';
.page-header {
margin: 0.5rem 0px;
color: $primary-color;
font-weight: 500;
}
.service-provider-action-buttons {
padding-bottom: 20px;
}
.service-provider-headers-align .mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.service-provider-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
margin-left: 8px;
}

View File

@ -0,0 +1,47 @@
import { afterNextRender, Component, viewChild } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { MatAccordion } from '@angular/material/expansion';
import { BasicDetailsComponent } from '../basic-details/basic-details.component';
import { ContactsComponent } from '../contacts/contacts.component';
import { ActivatedRoute } from '@angular/router';
import { CarnetSequenceComponent } from '../carnet-sequence/carnet-sequence.component';
import { CarnetFeeComponent } from "../carnet-fee/carnet-fee.component";
import { BasicFeeComponent } from "../basic-fee/basic-fee.component";
import { CounterfoilFeeComponent } from "../counterfoil-fee/counterfoil-fee.component";
import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component";
import { ExpeditedFeeComponent } from "../expedited-fee/expedited-fee.component";
import { SecurityDepositComponent } from "../security-deposit/security-deposit.component";
import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-edit-service-provider',
imports: [AngularMaterialModule, BasicDetailsComponent, ContactsComponent, CarnetSequenceComponent, CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ContinuationSheetFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, CommonModule],
templateUrl: './edit-service-provider.component.html',
styleUrl: './edit-service-provider.component.scss'
})
export class EditServiceProviderComponent {
accordion = viewChild.required(MatAccordion);
isEditMode = true;
spid = 0;
serviceProviderName: string | null = null;
userPreferences: UserPreferences;
constructor(private route: ActivatedRoute, private userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
afterNextRender(() => {
// Open all panels
this.accordion().openAll();
});
}
ngOnInit(): void {
const idParam = this.route.snapshot.paramMap.get('id');
this.spid = idParam ? parseInt(idParam, 10) : 0;
}
onServiceProviderNameUpdate(event: string): void {
this.serviceProviderName = event;
}
}

Some files were not shown because too many files have changed in this diff Show More