Compare commits

...

44 Commits

Author SHA1 Message Date
c5df0683a0 feedback updates 2026-01-17 17:07:28 -04:00
602793d2a4 param table default sorting 2026-01-12 22:20:29 -04:00
0da6daaa85 home page table filtering 2026-01-12 21:55:42 -04:00
2402382297 feedback updates 2026-01-10 22:39:46 -04:00
32ba75b4d2 feedback updates 2025-12-05 07:45:49 -04:00
62b78cd01a feedback updates 2025-11-13 23:38:54 -04:00
76d78c5869 feedback updates 2025-11-11 23:32:42 -04:00
251b9f8644 manage fee and region updates 2025-11-08 10:46:09 -04:00
48be10771a Update src/app/common/secured-header/secured-header.component.html 2025-11-07 16:25:41 +00:00
b26639e634 updated last line to test docker restart 2025-11-07 12:37:40 +00:00
534d19573e updated last line to test auto deployment 2025-11-07 12:23:25 +00:00
bc44c8be39 updated last line to test auto deploy 2025-11-07 12:19:44 +00:00
e83312ed0d updated last line for auto deploy test 2025-11-07 12:16:21 +00:00
8a78310c44 Updated last line for testing auto deployment 2025-11-07 12:10:18 +00:00
110d63e0f6 added one line at bottom for testing auto update 2025-11-07 12:08:19 +00:00
a0f698c31f manage fee and region updates 2025-11-06 22:40:36 -04:00
164a3ba87a added dev-2 target to serve 2025-11-03 13:16:37 +00:00
84de8fb8e8 added dev-1 to serve target 2025-11-03 12:58:24 +00:00
fa1841cdf2 added dev-1 and dev-2 configurations 2025-11-03 12:45:27 +00:00
e7ef98a238 enable add option for carnet fee and sequence in edit mode 2025-10-30 19:18:54 -03:00
aa3255e1de region and dynamic pagination & sorting updates 2025-10-29 13:41:04 -03:00
bd619d306f region updates 2025-10-28 22:21:55 -03:00
3e7ae6b70b Reverted back to 005 for cargo suretys 2025-10-28 10:02:23 +00:00
21652e1ffe Tweaked getCargoSuretys to use 003 instead of 005 2025-10-28 02:53:43 +00:00
5dfb93702c feedback updates 2025-10-27 20:52:14 -03:00
ec0a5d01a1 feedback updates 2025-10-27 20:30:39 -03:00
4833406406 feedback updates 2025-10-27 19:28:31 -03:00
a5eb513d8a made fax optional 2025-10-27 17:22:16 +00:00
96b1285124 changed cargoSurety cargoPolicyNo bondSurety to optional, getCargoSuretys shows same entries as Bond 2025-10-27 17:21:07 +00:00
ffe8404bfa Fix sorting 2025-10-27 17:18:04 +00:00
04340a5572 Fix issue with sorting 2025-10-27 17:17:02 +00:00
a7a7d6581a url updates 2025-09-19 08:28:22 -03:00
c237738d27 db switcher updates 2025-09-18 13:40:45 -03:00
d6d1a2d508 db switcher updates 2025-09-17 21:52:00 -03:00
8c60d91be7 feedback updates 2025-09-11 22:45:22 -03:00
4fee83707a feedback updates 2025-09-07 21:56:17 -03:00
7f79cc6d57 feedback updates 2025-08-28 08:20:37 -03:00
b2b921e040 param updates 2025-08-19 10:34:20 -03:00
4bbbb8f1bc param and service provider updates 2025-08-15 22:34:20 -03:00
ac3e7165e5 ui and feedback updates 2025-08-02 22:35:52 -03:00
005c192ef2 Merge branch 'main' of https://git.alphaomegainfosys.com/cyril/carnet-portal-app
latest commits
2025-07-29 07:38:37 -03:00
9bcaadde03 param nav updates 2025-07-29 07:38:12 -03:00
Kallesh B S
bebd12a6f8 param service modified 2025-07-28 21:13:01 +05:30
fd3bf6c940 Merge pull request 'param-table flow' (#1) from param-table into main
merged param table
2025-07-25 14:02:10 +00:00
116 changed files with 3963 additions and 611 deletions

View File

@ -57,3 +57,5 @@ Angular CLI does not come with an end-to-end testing framework by default. You c
## Additional Resources ## 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. 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.
Test 6.

View File

@ -72,6 +72,28 @@
"with": "src/environments/environment.development.ts" "with": "src/environments/environment.development.ts"
} }
] ]
},
"dev-1": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment-dev-1.ts"
}
]
},
"dev-2": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment-dev-2.ts"
}
]
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@ -81,7 +103,9 @@
"options": { "options": {
"allowedHosts": [ "allowedHosts": [
"localhost", "localhost",
"policy.alphaomegainfosys.com" "policy.alphaomegainfosys.com",
"policy-1.alphaomegainfosys.com",
"policy-2.alphaomegainfosys.com"
] ]
}, },
"configurations": { "configurations": {
@ -91,6 +115,12 @@
"development": { "development": {
"buildTarget": "carnet-portal-app:build:development", "buildTarget": "carnet-portal-app:build:development",
"port": 5173 "port": 5173
},
"dev-1": {
"buildTarget": "carnet-portal-app:build:dev-1"
},
"dev-2": {
"buildTarget": "carnet-portal-app:build:dev-2"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

252
public/themes/default.css Normal file
View File

@ -0,0 +1,252 @@
/* Note: Color palettes are generated from primary: #607c7c, secondary: #9cb4b8 */
html {
/* COLOR SYSTEM VARIABLES */
color-scheme: light;
/* Primary palette variables */
--mat-sys-primary: light-dark(#607c7c, #afcccc);
--mat-sys-on-primary: light-dark(#ffffff, #193535);
--mat-sys-primary-container: light-dark(#ffffff, #304b4c);
--mat-sys-on-primary-container: light-dark(#607c7c, #cae8e8);
--mat-sys-inverse-primary: light-dark(#afcccc, #486363);
--mat-sys-primary-fixed: light-dark(#cae8e8, #cae8e8);
--mat-sys-primary-fixed-dim: light-dark(#afcccc, #afcccc);
--mat-sys-on-primary-fixed: light-dark(#022020, #022020);
--mat-sys-on-primary-fixed-variant: light-dark(#304b4c, #304b4c);
/* Secondary palette variables */
--mat-sys-secondary: light-dark(#607c7c, #b3cbcf);
--mat-sys-on-secondary: light-dark(#ffffff, #1d3437);
--mat-sys-secondary-container: light-dark(#607c7c, #344a4e);
--mat-sys-on-secondary-container: light-dark(#ffffff, #cee7eb);
--mat-sys-secondary-fixed: light-dark(#cee7eb, #cee7eb);
--mat-sys-secondary-fixed-dim: light-dark(#b3cbcf, #b3cbcf);
--mat-sys-on-secondary-fixed: light-dark(#071f22, #071f22);
--mat-sys-on-secondary-fixed-variant: light-dark(#344a4e, #344a4e);
/* Tertiary palette variables */
--mat-sys-tertiary: light-dark(#635b72, #cdc2dd);
--mat-sys-on-tertiary: light-dark(#ffffff, #342d42);
--mat-sys-tertiary-container: light-dark(#e9def9, #4b4359);
--mat-sys-on-tertiary-container: light-dark(#1f182c, #e9def9);
--mat-sys-tertiary-fixed: light-dark(#e9def9, #e9def9);
--mat-sys-tertiary-fixed-dim: light-dark(#cdc2dd, #cdc2dd);
--mat-sys-on-tertiary-fixed: light-dark(#1f182c, #1f182c);
--mat-sys-on-tertiary-fixed-variant: light-dark(#4b4359, #4b4359);
/* Neutral palette variables */
--mat-sys-background: light-dark(#faf9f8, #121414);
--mat-sys-on-background: light-dark(#1a1c1c, #e3e2e2);
--mat-sys-surface: light-dark(#faf9f8, #121414);
--mat-sys-surface-dim: light-dark(#dadad9, #121414);
--mat-sys-surface-bright: light-dark(#faf9f8, #383939);
--mat-sys-surface-container-low: light-dark(#f4f3f3, #1a1c1c);
--mat-sys-surface-container-lowest: light-dark(#ffffff, #0d0e0e);
--mat-sys-surface-container: light-dark(#eeeeed, #1e2020);
--mat-sys-surface-container-high: light-dark(#e9e8e7, #292a2a);
--mat-sys-surface-container-highest: light-dark(#e3e2e2, #343535);
--mat-sys-on-surface: light-dark(#1a1c1c, #e3e2e2);
--mat-sys-shadow: light-dark(#000000, #000000);
--mat-sys-scrim: light-dark(#000000, #000000);
--mat-sys-surface-tint: light-dark(#486363, #afcccc);
--mat-sys-inverse-surface: light-dark(#2f3130, #e3e2e2);
--mat-sys-inverse-on-surface: light-dark(#f1f0f0, #2f3130);
--mat-sys-outline: light-dark(#727878, #8b9292);
--mat-sys-outline-variant: light-dark(#c1c8c7, #414848);
--mat-sys-neutral10: light-dark(#1a1c1c, #1a1c1c);
/* Variable used for the form field native select option text color */
/* Error palette variables */
--mat-sys-error: light-dark(#ba1a1a, #ffb4ab);
--mat-sys-on-error: light-dark(#ffffff, #690005);
--mat-sys-error-container: light-dark(#ffdad6, #93000a);
--mat-sys-on-error-container: light-dark(#410002, #ffdad6);
/* Neutral variant palette variables */
--mat-sys-surface-variant: light-dark(#dde4e3, #414848);
--mat-sys-on-surface-variant: light-dark(#414848, #c1c8c7);
--mat-sys-neutral-variant20: light-dark(#2b3232, #2b3232);
/* Variable used for the sidenav scrim (container background shadow when opened) */
/* TYPOGRAPHY SYSTEM VARIABLES */
/* Typography variables. Only used in the different typescale system variables. */
--mat-sys-brand-font-family: Roboto;
/* The font-family to use for brand text. */
--mat-sys-plain-font-family: Roboto;
/* The font-family to use for plain text. */
--mat-sys-bold-font-weight: 700;
/* The font-weight to use for bold text. */
--mat-sys-medium-font-weight: 500;
/* The font-weight to use for medium text. */
--mat-sys-regular-font-weight: 400;
/* The font-weight to use for regular text. */
/* Typescale variables. */
/* Warning: Risk of reduced fidelity from using the composite typography tokens (ex. --mat-sys-body-large) since
tracking cannot be represented in the "font" property shorthand. Consider using the discrete properties instead. */
--mat-sys-body-large: var(--mat-sys-body-large-weight) var(--mat-sys-body-large-size) / var(--mat-sys-body-large-line-height) var(--mat-sys-body-large-font);
--mat-sys-body-large-font: var(--mat-sys-plain-font-family);
--mat-sys-body-large-line-height: 1.5rem;
--mat-sys-body-large-size: 1rem;
--mat-sys-body-large-tracking: 0.031rem;
--mat-sys-body-large-weight: var(--mat-sys-regular-font-weight);
/* Body medium typescale */
--mat-sys-body-medium: var(--mat-sys-body-medium-weight) var(--mat-sys-body-medium-size) / var(--mat-sys-body-medium-line-height) var(--mat-sys-body-medium-font);
--mat-sys-body-medium-font: var(--mat-sys-plain-font-family);
--mat-sys-body-medium-line-height: 1.25rem;
--mat-sys-body-medium-size: 0.875rem;
--mat-sys-body-medium-tracking: 0.016rem;
--mat-sys-body-medium-weight: var(--mat-sys-regular-font-weight);
/* Body small typescale */
--mat-sys-body-small: var(--mat-sys-body-small-weight) var(--mat-sys-body-small-size) / var(--mat-sys-body-small-line-height) var(--mat-sys-body-small-font);
--mat-sys-body-small-font: var(--mat-sys-plain-font-family);
--mat-sys-body-small-line-height: 1rem;
--mat-sys-body-small-size: 0.75rem;
--mat-sys-body-small-tracking: 0.025rem;
--mat-sys-body-small-weight: var(--mat-sys-regular-font-weight);
/* Display large typescale */
--mat-sys-display-large: var(--mat-sys-display-large-weight) var(--mat-sys-display-large-size) / var(--mat-sys-display-large-line-height) var(--mat-sys-display-large-font);
--mat-sys-display-large-font: var(--mat-sys-brand-font-family);
--mat-sys-display-large-line-height: 4rem;
--mat-sys-display-large-size: 3.562rem;
--mat-sys-display-large-tracking: -0.016rem;
--mat-sys-display-large-weight: var(--mat-sys-regular-font-weight);
/* Display medium typescale */
--mat-sys-display-medium: var(--mat-sys-display-medium-weight) var(--mat-sys-display-medium-size) / var(--mat-sys-display-medium-line-height) var(--mat-sys-display-medium-font);
--mat-sys-display-medium-font: var(--mat-sys-brand-font-family);
--mat-sys-display-medium-line-height: 3.25rem;
--mat-sys-display-medium-size: 2.812rem;
--mat-sys-display-medium-tracking: 0;
--mat-sys-display-medium-weight: var(--mat-sys-regular-font-weight);
/* Display small typescale */
--mat-sys-display-small: var(--mat-sys-display-small-weight) var(--mat-sys-display-small-size) / var(--mat-sys-display-small-line-height) var(--mat-sys-display-small-font);
--mat-sys-display-small-font: var(--mat-sys-brand-font-family);
--mat-sys-display-small-line-height: 2.75rem;
--mat-sys-display-small-size: 2.25rem;
--mat-sys-display-small-tracking: 0;
--mat-sys-display-small-weight: var(--mat-sys-regular-font-weight);
/* Headline large typescale */
--mat-sys-headline-large: var(--mat-sys-headline-large-weight) var(--mat-sys-headline-large-size) / var(--mat-sys-headline-large-line-height) var(--mat-sys-headline-large-font);
--mat-sys-headline-large-font: var(--mat-sys-brand-font-family);
--mat-sys-headline-large-line-height: 2.5rem;
--mat-sys-headline-large-size: 2rem;
--mat-sys-headline-large-tracking: 0;
--mat-sys-headline-large-weight: var(--mat-sys-regular-font-weight);
/* Headline medium typescale */
--mat-sys-headline-medium: var(--mat-sys-headline-medium-weight) var(--mat-sys-headline-medium-size) / var(--mat-sys-headline-medium-line-height) var(--mat-sys-headline-medium-font);
--mat-sys-headline-medium-font: var(--mat-sys-brand-font-family);
--mat-sys-headline-medium-line-height: 2.25rem;
--mat-sys-headline-medium-size: 1.75rem;
--mat-sys-headline-medium-tracking: 0;
--mat-sys-headline-medium-weight: var(--mat-sys-regular-font-weight);
/* Headline small typescale */
--mat-sys-headline-small: var(--mat-sys-headline-small-weight) var(--mat-sys-headline-small-size) / var(--mat-sys-headline-small-line-height) var(--mat-sys-headline-small-font);
--mat-sys-headline-small-font: var(--mat-sys-brand-font-family);
--mat-sys-headline-small-line-height: 2rem;
--mat-sys-headline-small-size: 1.5rem;
--mat-sys-headline-small-tracking: 0;
--mat-sys-headline-small-weight: var(--mat-sys-regular-font-weight);
/* Label large typescale */
--mat-sys-label-large: var(--mat-sys-label-large-weight) var(--mat-sys-label-large-size) / var(--mat-sys-label-large-line-height) var(--mat-sys-label-large-font);
--mat-sys-label-large-font: var(--mat-sys-plain-font-family);
--mat-sys-label-large-line-height: 1.25rem;
--mat-sys-label-large-size: 0.875rem;
--mat-sys-label-large-tracking: 0.006rem;
--mat-sys-label-large-weight: var(--mat-sys-medium-font-weight);
--mat-sys-label-large-weight-prominent: var(--mat-sys-bold-font-weight);
/* Label medium typescale */
--mat-sys-label-medium: var(--mat-sys-label-medium-weight) var(--mat-sys-label-medium-size) / var(--mat-sys-label-medium-line-height) var(--mat-sys-label-medium-font);
--mat-sys-label-medium-font: var(--mat-sys-plain-font-family);
--mat-sys-label-medium-line-height: 1rem;
--mat-sys-label-medium-size: 0.75rem;
--mat-sys-label-medium-tracking: 0.031rem;
--mat-sys-label-medium-weight: var(--mat-sys-medium-font-weight);
--mat-sys-label-medium-weight-prominent: var(--mat-sys-bold-font-weight);
/* Label small typescale */
--mat-sys-label-small: var(--mat-sys-label-small-weight) var(--mat-sys-label-small-size) / var(--mat-sys-label-small-line-height) var(--mat-sys-label-small-font);
--mat-sys-label-small-font: var(--mat-sys-plain-font-family);
--mat-sys-label-small-line-height: 1rem;
--mat-sys-label-small-size: 0.688rem;
--mat-sys-label-small-tracking: 0.031rem;
--mat-sys-label-small-weight: var(--mat-sys-medium-font-weight);
/* Title large typescale */
--mat-sys-title-large: var(--mat-sys-title-large-weight) var(--mat-sys-title-large-size) / var(--mat-sys-title-large-line-height) var(--mat-sys-title-large-font);
--mat-sys-title-large-font: var(--mat-sys-brand-font-family);
--mat-sys-title-large-line-height: 1.75rem;
--mat-sys-title-large-size: 1.375rem;
--mat-sys-title-large-tracking: 0;
--mat-sys-title-large-weight: var(--mat-sys-regular-font-weight);
/* Title medium typescale */
--mat-sys-title-medium: var(--mat-sys-title-medium-weight) var(--mat-sys-title-medium-size) / var(--mat-sys-title-medium-line-height) var(--mat-sys-title-medium-font);
--mat-sys-title-medium-font: var(--mat-sys-plain-font-family);
--mat-sys-title-medium-line-height: 1.5rem;
--mat-sys-title-medium-size: 1rem;
--mat-sys-title-medium-tracking: 0.009rem;
--mat-sys-title-medium-weight: var(--mat-sys-medium-font-weight);
/* Title small typescale */
--mat-sys-title-small: var(--mat-sys-title-small-weight) var(--mat-sys-title-small-size) / var(--mat-sys-title-small-line-height) var(--mat-sys-title-small-font);
--mat-sys-title-small-font: var(--mat-sys-plain-font-family);
--mat-sys-title-small-line-height: 1.25rem;
--mat-sys-title-small-size: 0.875rem;
--mat-sys-title-small-tracking: 0.006rem;
--mat-sys-title-small-weight: var(--mat-sys-medium-font-weight);
/* ELEVATION SYSTEM VARIABLES */
/* Box shadow colors. Only used in the elevation level system variables. */
--mat-sys-umbra-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 80%);
--mat-sys-penumbra-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 86%);
--mat-sys-ambient-color: color-mix(in srgb, var(--mat-sys-shadow), transparent 88%);
/* Elevation level system variables. These are used as the value for box-shadow CSS property. */
--mat-sys-level0: 0px 0px 0px 0px var(--mat-sys-umbra-color), 0px 0px 0px 0px var(--mat-sys-penumbra-color), 0px 0px 0px 0px var(--mat-sys-ambient-color);
--mat-sys-level1: 0px 2px 1px -1px var(--mat-sys-umbra-color), 0px 1px 1px 0px var(--mat-sys-penumbra-color), 0px 1px 3px 0px var(--mat-sys-ambient-color);
--mat-sys-level2: 0px 3px 3px -2px var(--mat-sys-umbra-color), 0px 3px 4px 0px var(--mat-sys-penumbra-color), 0px 1px 8px 0px var(--mat-sys-ambient-color);
--mat-sys-level3: 0px 3px 5px -1px var(--mat-sys-umbra-color), 0px 6px 10px 0px var(--mat-sys-penumbra-color), 0px 1px 18px 0px var(--mat-sys-ambient-color);
--mat-sys-level4: 0px 5px 5px -3px var(--mat-sys-umbra-color), 0px 8px 10px 1px var(--mat-sys-penumbra-color), 0px 3px 14px 2px var(--mat-sys-ambient-color);
--mat-sys-level5: 0px 7px 8px -4px var(--mat-sys-umbra-color), 0px 12px 17px 2px var(--mat-sys-penumbra-color), 0px 5px 22px 4px var(--mat-sys-ambient-color);
/* SHAPE SYSTEM VARIABLES */
--mat-sys-corner-extra-large: 28px;
--mat-sys-corner-extra-large-top: 28px 28px 0 0;
--mat-sys-corner-extra-small: 4px;
--mat-sys-corner-extra-small-top: 4px 4px 0 0;
--mat-sys-corner-full: 9999px;
--mat-sys-corner-large: 16px;
--mat-sys-corner-large-end: 0 16px 16px 0;
--mat-sys-corner-large-start: 16px 0 0 16px;
--mat-sys-corner-large-top: 16px 16px 0 0;
--mat-sys-corner-medium: 12px;
--mat-sys-corner-none: 0;
--mat-sys-corner-small: 8px;
/* STATE SYSTEM VARIABLES */
--mat-sys-dragged-state-layer-opacity: 0.16;
--mat-sys-focus-state-layer-opacity: 0.12;
--mat-sys-hover-state-layer-opacity: 0.08;
--mat-sys-pressed-state-layer-opacity: 0.12;
/* CUSTOM BUTTON COLORS */
--custom-button-color: #ffffff;
--custom-button-background-color: #627f9a;
--custom-hover-color: #384857;
/* CUSTOM NAV COLORS */
--custom-nav-color: #ffffff;
--custom-nav-background-color: #607c7c;
}

View File

@ -1,14 +1,14 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { Subscription } from 'rxjs';
import { CommonModule } from '@angular/common';
import { FooterComponent } from './common/footer/footer.component';
import { SecuredHeaderComponent } from './common/secured-header/secured-header.component'; import { SecuredHeaderComponent } from './common/secured-header/secured-header.component';
import { UserService } from './core/services/common/user.service'; import { UserService } from './core/services/common/user.service';
import { FooterComponent } from './common/footer/footer.component';
import { CommonModule } from '@angular/common';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, SecuredHeaderComponent, FooterComponent, CommonModule], imports: [RouterOutlet, FooterComponent, CommonModule, SecuredHeaderComponent],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
@ -17,8 +17,7 @@ export class AppComponent {
isUserLoggedIn = false; isUserLoggedIn = false;
private userSubscription!: Subscription; private userSubscription!: Subscription;
constructor(private userService: UserService private userService = inject(UserService);
) { }
ngOnInit(): void { ngOnInit(): void {

View File

@ -10,6 +10,7 @@ import {
withDefaultRegisterables, withDefaultRegisterables,
} from 'ng2-charts'; } from 'ng2-charts';
import { AuthInterceptor } from './core/interceptors/auth.interceptor'; import { AuthInterceptor } from './core/interceptors/auth.interceptor';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
@ -20,5 +21,7 @@ export const appConfig: ApplicationConfig = {
provideHttpClient(), provideHttpClient(),
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideCharts(withDefaultRegisterables()), provideCharts(withDefaultRegisterables()),
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },] { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { subscriptSizing: 'dynamic' } }
]
}; };

View File

@ -11,13 +11,19 @@ import { ForgotPasswordComponent } from './forgot-password/forgot-password.compo
import { ParamTableComponent } from './param/table/param-table.component'; import { ParamTableComponent } from './param/table/param-table.component';
import { ManageTableRecordComponent } from './param/manage-table-record/manage-table-record.component'; import { ManageTableRecordComponent } from './param/manage-table-record/manage-table-record.component';
import { ManageParamRecordComponent } from './param/manage-param-record/manage-param-record.component'; import { ManageParamRecordComponent } from './param/manage-param-record/manage-param-record.component';
import { RegionComponent } from './region/region.component';
import { ManageFeeComponent } from './fees/manage/manage-fee.component';
import { ManageCountryComponent } from './param/manage-country/manage-country.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent }, { path: 'register', component: RegisterComponent },
{ path: 'forgot-password', component: ForgotPasswordComponent }, { path: 'forgot-password', component: ForgotPasswordComponent },
{ path: 'table-record', component: ManageTableRecordComponent }, { path: 'table-record', component: ManageTableRecordComponent, canActivate: [AuthGuard] },
{ path: 'param-record/:id', component: ManageParamRecordComponent }, { path: 'param-record/:id', component: ManageParamRecordComponent, canActivate: [AuthGuard] },
{ path: 'country-messages', component: ManageCountryComponent, canActivate: [AuthGuard] },
{ path: 'region', component: RegionComponent, canActivate: [AuthGuard] },
{ path: 'fees', component: ManageFeeComponent, canActivate: [AuthGuard] },
{ path: 'usersettings', component: UserSettingsComponent, canActivate: [AuthGuard] }, { path: 'usersettings', component: UserSettingsComponent, canActivate: [AuthGuard] },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] }, { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'service-provider/:id', component: EditServiceProviderComponent, canActivate: [AuthGuard] }, { path: 'service-provider/:id', component: EditServiceProviderComponent, canActivate: [AuthGuard] },

View File

@ -1,4 +1,4 @@
<footer class="footer"> <footer class="footer" [ngClass]="{'general': !isUserLoggedIn}">
<div class="footer-content"> <div class="footer-content">
<div class="footer-links"> <div class="footer-links">
<a href="#">Privacy Policy</a> <a href="#">Privacy Policy</a>
@ -7,7 +7,7 @@
</div> </div>
<div class="copyright"> <div class="copyright">
&copy; {{ currentYear }} USCIB Carnet Portal. All rights reserved. &copy; {{ currentYear }} Alpha Omega Infosys. All rights reserved.
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -1,11 +1,8 @@
@use 'colors' as colors;
.footer { .footer {
background-color: colors.$background-header; color: var(--mat-sys-on-primary);
background: var(--mat-sys-primary);
padding: 8px; padding: 8px;
border-top: 1px solid colors.$divider-color; border-top: 1px solid var(--mat-sys-outline-variant);
backdrop-filter: blur(10px);
background-color: rgba(colors.$background-header, 0.9);
/* Fixed footer styles */ /* Fixed footer styles */
position: fixed; position: fixed;
@ -22,6 +19,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background: var(--mat-sys-primary);
} }
.footer-links { .footer-links {
@ -29,22 +27,46 @@
gap: 24px; gap: 24px;
a { a {
color: colors.$text-secondary; color: var(--mat-sys-on-primary);
background: var(--mat-sys-primary);
text-decoration: none; text-decoration: none;
transition: color 0.3s ease; transition: color 0.3s ease;
&:hover { &:hover {
color: colors.$primary-color; transform: scale(103%);
} }
} }
} }
.copyright { .copyright {
color: colors.$text-secondary;
font-size: 12px; font-size: 12px;
} }
} }
/* footer styles - not logged in users - not applicable for this site*/
// .footer.general {
// color: #000000;
// background: #ffffff;
// .footer-content {
// max-width: 1200px;
// margin: 0 auto;
// display: flex;
// flex-direction: column;
// align-items: center;
// gap: 8px;
// background: #ffffff;
// }
// .footer-links {
// a {
// color: #000000;
// background: #ffffff;
// }
// }
// }
@media (max-width: 600px) { @media (max-width: 600px) {
.footer { .footer {
padding: 16px; padding: 16px;

View File

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

View File

@ -5,11 +5,14 @@
</div> </div>
<div class="navbar-menu"> <div class="navbar-menu">
<a class="nav-link" (click)="navigateTo('/home')">Home</a> <button mat-button (click)="navigateTo('home')">Home</button>
<a class="nav-link" (click)="navigateTo('/manageusers')">Users</a> <button mat-button [matMenuTriggerFor]="configurations">Configurations</button>
<a class="nav-link" (click)="navigateTo('/regions')">Regions</a> <mat-menu #configurations="matMenu">
<a class="nav-link" (click)="navigateTo('/table-record')">Parameters</a> <button mat-menu-item (click)="navigateTo('table-record')">Table Records</button>
<button mat-menu-item (click)="navigateTo('country-messages')">Country Messages</button>
<button mat-menu-item (click)="navigateTo('region')">Manage Regions</button>
<button mat-menu-item (click)="navigateTo('fees')">Manage Fees</button>
</mat-menu>
<div class="profile-container"> <div class="profile-container">
<button mat-icon-button (click)="toggleProfileMenu()" class="profile-button"> <button mat-icon-button (click)="toggleProfileMenu()" class="profile-button">
<mat-icon>account_circle</mat-icon> <mat-icon>account_circle</mat-icon>
@ -23,7 +26,7 @@
<button mat-menu-item (click)="navigateTo('/usersettings')"> <button mat-menu-item (click)="navigateTo('/usersettings')">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
<span>User Settings</span> <span>User Preferences</span>
</button> </button>
<button mat-menu-item (click)="logout()"> <button mat-menu-item (click)="logout()">

View File

@ -1,14 +1,12 @@
@use 'colors' as colors;
.navbar { .navbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 24px; padding: 0 24px;
height: 64px; height: 64px;
background-color: colors.$primary-color; color: var(--custom-nav-color);
color: white; background: var(--custom-nav-background-color);
box-shadow: var(--mat-sys-level2); border-bottom: 1px solid var(--mat-sys-outline-variant);
position: relative; position: relative;
z-index: 100; // Higher than footer's z-index z-index: 100; // Higher than footer's z-index
@ -33,46 +31,70 @@
align-items: center; align-items: center;
gap: 24px; gap: 24px;
.nav-link { button {
cursor: pointer; color: var(--custom-nav-color);
padding: 8px 12px; background-color: var(--custom-nav-background-color);
border-radius: 4px;
transition: background-color 0.3s ease;
&:hover { &:hover {
background-color: rgba(255, 255, 255, 0.1); color: var(--custom-nav-background-color);
background-color: var(--custom-nav-color);
} }
} }
.mat-mdc-button:hover>.mat-mdc-button-persistent-ripple::before {
opacity: 0;
}
// .nav-link {
// cursor: pointer;
// padding: 8px 12px;
// border-radius: 4px;
// transition: background-color 0.3s ease;
// &:hover {
// background-color: var(--mat-sys-secondary-container);
// box-shadow: var(--mat-sys-level1);
// }
// }
} }
.profile-container { .profile-container {
position: relative; position: relative;
.profile-button { .profile-button {
color: white; color: var(--custom-nav-background-color);
background-color: var(--custom-nav-color);
&:hover {
color: var(--custom-nav-color);
background-color: var(--custom-nav-background-color);
}
} }
.profile-menu { .profile-menu {
position: absolute; position: absolute;
right: 0; right: 0;
top: 48px; top: 48px;
background-color: white; color: var(--mat-sys-on-primary-container);
background-color: var(--mat-sys-primary-container);
border-radius: 4px; border-radius: 4px;
box-shadow: var(--mat-sys-level3); box-shadow: var(--mat-sys-level3);
min-width: 200px; min-width: 200px;
overflow: hidden; overflow: hidden;
z-index: 101; z-index: 101;
font-size: 0.875rem;
.profile-info { .profile-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid colors.$divider-color; color: var(--mat-sys-on-primary-container);
color: colors.$text-primary; background-color: var(--mat-sys-primary-container);
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
mat-icon { mat-icon {
color: colors.$text-secondary; color: var(--mat-sys-on-primary-container);
} }
} }
@ -82,18 +104,30 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 16px; padding: 8px 16px;
color: colors.$text-primary;
border: none; border: none;
cursor: pointer; cursor: pointer;
color: var(--mat-sys-on-primary-container);
background-color: var(--mat-sys-primary-container);
mat-icon { // mat-icon {
color: colors.$text-secondary; // color: var(--mat-sys-on-primary-container);
// }
&:hover {
color: var(--mat-sys-on-primary);
background-color: var(--mat-sys-primary);
} }
} }
} }
} }
} }
.mat-mdc-menu-item:not([disabled]):hover {
color: var(--mat-sys-on-primary);
background-color: var(--mat-sys-primary);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar { .navbar {
padding: 0 12px; padding: 0 12px;

View File

@ -4,6 +4,7 @@ import { AngularMaterialModule } from '../../shared/module/angular-material.modu
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AuthService } from '../../core/services/common/auth.service'; import { AuthService } from '../../core/services/common/auth.service';
import { NavigationService } from '../../core/services/common/navigation.service'; import { NavigationService } from '../../core/services/common/navigation.service';
import { ThemeService } from '../../core/services/theme.service';
@Component({ @Component({
selector: 'app-secured-header', selector: 'app-secured-header',
@ -18,9 +19,11 @@ export class SecuredHeaderComponent implements OnInit {
private userService = inject(UserService); private userService = inject(UserService);
private authService = inject(AuthService); private authService = inject(AuthService);
private navigationService = inject(NavigationService); private navigationService = inject(NavigationService);
private themeService = inject(ThemeService);
ngOnInit(): void { ngOnInit(): void {
this.userEmail = this.userService.getSafeUser(); this.userEmail = this.userService.getSafeUser();
this.themeService.setTheme('default');
} }
toggleProfileMenu(): void { toggleProfileMenu(): void {

View File

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

View File

@ -7,4 +7,6 @@ export interface BasicFee {
spid?: number; spid?: number;
dateCreated?: Date | null; dateCreated?: Date | null;
createdBy?: string | null; createdBy?: string | null;
expiredDate?: Date | null;
expired?: boolean;
} }

View File

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

View File

@ -6,4 +6,6 @@ export interface CarnetFee {
spid: number; spid: number;
dateCreated?: Date | null; dateCreated?: Date | null;
createdBy?: string | null; createdBy?: string | null;
expiredDate?: Date | null;
expired?: boolean;
} }

View File

@ -7,4 +7,6 @@ export interface ContinuationSheetFee {
carnetType: string; carnetType: string;
dateCreated?: Date | null; dateCreated?: Date | null;
createdBy?: string | null; createdBy?: string | null;
expiredDate?: Date | null;
expired?: boolean;
} }

View File

@ -9,4 +9,6 @@ export interface CounterfoilFee {
rate: number, rate: number,
dateCreated?: Date | null; dateCreated?: Date | null;
createdBy?: string | null; createdBy?: string | null;
expiredDate?: Date | null;
expired?: boolean;
} }

View File

@ -9,4 +9,6 @@ export interface ExpeditedFee {
effectiveDate: Date; effectiveDate: Date;
dateCreated?: Date | null; dateCreated?: Date | null;
createdBy?: string | null; createdBy?: string | null;
expiredDate?: Date | null;
expired?: boolean;
} }

View File

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

View File

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

View File

@ -0,0 +1,6 @@
import { ParamProperties } from "./parameters";
export interface CountryMessage extends ParamProperties {
countryMessage: string | null;
isInactive: boolean;
}

View File

@ -5,18 +5,18 @@ export interface ParamProperties {
paramType: string; paramType: string;
paramDesc: string; paramDesc: string;
paramValue: string; paramValue: string;
addlParamValue1: string | null; addlParamValue1?: string | null;
addlParamValue2: string | null; addlParamValue2?: string | null;
addlParamValue3: string | null; addlParamValue3?: string | null;
addlParamValue4: string | null; addlParamValue4?: string | null;
addlParamValue5: string | null; addlParamValue5?: string | null;
sortSeq?: number; sortSeq?: number;
inactiveCodeFlag: 'Y' | 'N' | null; inactiveCodeFlag?: 'Y' | 'N' | null;
inactiveDate: string | null; inactiveDate?: string | null;
createdBy: string; createdBy?: string;
dateCreated: string; dateCreated?: string;
lastUpdatedBy: string | null; lastUpdatedBy?: string | null;
lastUpdatedDate: string | null; lastUpdatedDate?: string | null;
errorMesg: string | null; errorMesg?: string | null;
userId?: string; userId?: string;
} }

View File

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

View File

@ -0,0 +1,4 @@
export interface ContactLogin {
spid: number;
emailAddress: string;
}

View File

@ -16,4 +16,5 @@ export interface Contact {
lastUpdatedDate?: Date | null; lastUpdatedDate?: Date | null;
isInactive?: boolean | null; isInactive?: boolean | null;
inactivatedDate?: Date | null; inactivatedDate?: Date | null;
hasLogin?: boolean | null;
} }

View File

@ -17,4 +17,5 @@ export interface UserDetail {
urlKey: string; urlKey: string;
logoName: string; logoName: string;
themeName: string; themeName: string;
serviceProviderName: string;
} }

View File

@ -36,7 +36,12 @@ export class ApiErrorHandlerService {
return this.genericErrorMessage; return this.genericErrorMessage;
} }
// slice off the last pipe character only if the last character is a pipe
if (messageString.endsWith('|')) {
return messageString.slice(0, -1); return messageString.slice(0, -1);
}
return messageString;
// .split('|') // .split('|')
// .map(msg => msg.trim()) // .map(msg => msg.trim())
// .filter(msg => msg.length > 0); // .filter(msg => msg.length > 0);

View File

@ -13,6 +13,8 @@ import { ApiErrorHandlerService } from './api-error-handler.service';
}) })
export class AuthService { export class AuthService {
private apiUrl = environment.apiUrl; private apiUrl = environment.apiUrl;
private applicationName = environment.appType;
private apiDb = environment.apiDb;
private http = inject(HttpClient); private http = inject(HttpClient);
private userService = inject(UserService); private userService = inject(UserService);
@ -22,16 +24,16 @@ export class AuthService {
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
login(username: string, password: string): Observable<any> { login(username: string, password: string): Observable<any> {
return this.http.post(`${this.apiUrl}/login`, { p_emailaddr: username, p_password: password }); return this.http.post(`${this.apiUrl}/${this.apiDb}/login`, { p_emailaddr: username, p_password: password, P_APPLICATIONNAME: this.applicationName });
} }
refreshToken(): Observable<any> { refreshToken(): Observable<any> {
return this.http.get(`${this.apiUrl}/refresh-tokens`, {}); return this.http.get(`${this.apiUrl}/${this.apiDb}/refresh-tokens`, {});
} }
logout(): void { logout(): void {
if (this.userService.isLoggedIn()) { if (this.userService.isLoggedIn()) {
this.http.post(`${this.apiUrl}/logout`, {}).subscribe({ this.http.post(`${this.apiUrl}/${this.apiDb}/logout`, {}).subscribe({
next: (response) => { next: (response) => {
this.userService.clearUser(); this.userService.clearUser();
this.storageService.clear(); this.storageService.clear();
@ -49,7 +51,7 @@ export class AuthService {
register(email: string, password: string): Observable<any> { register(email: string, password: string): Observable<any> {
return this.http.post( return this.http.post(
`${this.apiUrl}/register`, `${this.apiUrl}/${this.apiDb}/register`,
{ P_EMAILADDR: email, P_PASSWORD: password } { P_EMAILADDR: email, P_PASSWORD: password }
); );
} }
@ -62,7 +64,7 @@ export class AuthService {
} }
return this.http.put( return this.http.put(
`${this.apiUrl}/forgot-password`, `${this.apiUrl}/${this.apiDb}/forgot-password`,
{ P_EMAILADDR: email, P_PASSWORD: password }, { P_EMAILADDR: email, P_PASSWORD: password },
{ headers } { headers }
); );
@ -70,7 +72,7 @@ export class AuthService {
sendEmail(email: string, type: string): Observable<any> { sendEmail(email: string, type: string): Observable<any> {
return this.http.post( return this.http.post(
`${this.apiUrl}/sendmail`, `${this.apiUrl}/${this.apiDb}/sendmail`,
{ P_TO: email, P_MAIL_TYPE: type } { P_TO: email, P_MAIL_TYPE: type }
); );
} }

View File

@ -12,6 +12,9 @@ import { CargoPolicy } from '../../models/cargo-policy';
import { CargoSurety } from '../../models/cargo-surety'; import { CargoSurety } from '../../models/cargo-surety';
import { CarnetStatus } from '../../models/carnet-status'; import { CarnetStatus } from '../../models/carnet-status';
import { Country } from '../../models/country'; import { Country } from '../../models/country';
import { UserService } from './user.service';
import { HolderType } from '../../models/holder-type';
import { ContactTitle } from '../../models/contact-title';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -21,9 +24,10 @@ export class CommonService {
private apiDb = environment.apiDb; private apiDb = environment.apiDb;
private http = inject(HttpClient); private http = inject(HttpClient);
private userService = inject(UserService);
getCountries(spid: number = 0): Observable<Country[]> { getCountries(): Observable<Country[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=002&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=002&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -34,8 +38,8 @@ export class CommonService {
); );
} }
getStates(country: string, spid: number = 0): Observable<State[]> { getStates(country: string): Observable<State[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=001&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=001&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -52,7 +56,7 @@ export class CommonService {
} }
getRegions(): Observable<Region[]> { getRegions(): Observable<Region[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions/${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
id: item.REGIONID, id: item.REGIONID,
@ -63,8 +67,8 @@ export class CommonService {
); );
} }
getDeliveryTypes(spid: number = 0): Observable<DeliveryType[]> { getDeliveryTypes(): Observable<DeliveryType[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=006&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=006&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -75,8 +79,8 @@ export class CommonService {
); );
} }
getTimezones(spid: number = 0): Observable<TimeZone[]> { getTimezones(): Observable<TimeZone[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=010&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=010&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -87,8 +91,8 @@ export class CommonService {
); );
} }
getFeeTypes(spid: number = 0): Observable<FeeType[]> { getFeeTypes(): Observable<FeeType[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=009&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=009&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -99,8 +103,8 @@ export class CommonService {
); );
} }
getBondSuretys(spid: number = 0): Observable<BondSurety[]> { getBondSuretys(): Observable<BondSurety[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=003&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=003&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -111,8 +115,8 @@ export class CommonService {
); );
} }
getCargoPolicies(spid: number = 0): Observable<CargoPolicy[]> { getCargoPolicies(): Observable<CargoPolicy[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=004&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=004&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -123,8 +127,8 @@ export class CommonService {
); );
} }
getCargoSuretys(spid: number = 0): Observable<CargoSurety[]> { getCargoSuretys(): Observable<CargoSurety[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=005&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=005&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -135,8 +139,8 @@ export class CommonService {
); );
} }
getCarnetStatuses(spid: number = 0): Observable<CarnetStatus[]> { getCarnetStatuses(): Observable<CarnetStatus[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=011&P_SPID=0`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=011&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) => map((response) =>
response.map((item) => ({ response.map((item) => ({
name: item.PARAMDESC, name: item.PARAMDESC,
@ -148,6 +152,30 @@ export class CommonService {
); );
} }
getHolderTypes(): Observable<HolderType[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=017&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE,
}))
)
);
}
getContactTitles(): Observable<ContactTitle[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=012&P_SPID=${this.userService.getUserSpid()}`).pipe(
map((response) =>
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE,
}))
)
);
}
formatUSDate(datetime: Date): string { formatUSDate(datetime: Date): string {
const date = new Date(datetime); const date = new Date(datetime);
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const month = String(date.getUTCMonth() + 1).padStart(2, '0');

View File

@ -136,7 +136,8 @@ export class UserService {
locationid: userDetails.LOCATIONID, locationid: userDetails.LOCATIONID,
urlKey: userDetails.ENCURLKEY, urlKey: userDetails.ENCURLKEY,
logoName: userDetails.LOGONAME, logoName: userDetails.LOGONAME,
themeName: userDetails.THEMENAME themeName: userDetails.THEMENAME,
serviceProviderName: userDetails.SPNAME
}; };
} }

View File

@ -3,7 +3,7 @@ import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
import { BasicFee } from '../../models/service-provider/basic-fee'; import { BasicFee } from '../../models/fees/basic-fee';
import { CommonService } from '../common/common.service'; import { CommonService } from '../common/common.service';
@Injectable({ @Injectable({
@ -30,15 +30,17 @@ export class BasicFeeService {
fees: item.FEES, fees: item.FEES,
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null dateCreated: item.DATECREATED || null,
expiredDate: item.EXPDATE || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
})); }));
} }
createBasicFee(spid: number, fee: BasicFee): Observable<any> { createBasicFee(spid: number, fee: BasicFee): Observable<any> {
const payload = { const payload = {
P_SPID: spid, P_SPID: spid,
P_STARTCARNETVALUE: fee.startCarnetValue, P_STARTNUMBER: fee.startCarnetValue,
P_ENDCARNETVALUE: fee.endCarnetValue, P_ENDNUMBER: fee.endCarnetValue,
P_FEES: fee.fees, P_FEES: fee.fees,
P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate), P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate),
P_USERID: this.userService.getUser() P_USERID: this.userService.getUser()
@ -49,8 +51,6 @@ export class BasicFeeService {
updateBasicFee(feeId: number, fee: BasicFee): Observable<any> { updateBasicFee(feeId: number, fee: BasicFee): Observable<any> {
const payload = { const payload = {
P_BASICFEESETUPID: feeId, P_BASICFEESETUPID: feeId,
P_STARTCARNETVALUE: fee.startCarnetValue,
P_ENDCARNETVALUE: fee.endCarnetValue,
P_FEES: fee.fees, P_FEES: fee.fees,
P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate), P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate),
P_USERID: this.userService.getUser() P_USERID: this.userService.getUser()

View File

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

View File

@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { CarnetFee } from '../../models/service-provider/carnet-fee'; import { CarnetFee } from '../../models/fees/carnet-fee';
import { CommonService } from '../common/common.service'; import { CommonService } from '../common/common.service';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
@ -31,7 +31,9 @@ export class CarnetFeeService {
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
spid: item.SPID, spid: item.SPID,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null dateCreated: item.DATECREATED || null,
expiredDate: item.EXPDATE || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
})); }));
} }

View File

@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ContinuationSheetFee } from '../../models/service-provider/continuation-sheet-fee'; import { ContinuationSheetFee } from '../../models/fees/continuation-sheet-fee';
import { CommonService } from '../common/common.service'; import { CommonService } from '../common/common.service';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
@ -31,7 +31,9 @@ export class ContinuationSheetFeeService {
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
rate: item.RATE, rate: item.RATE,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null dateCreated: item.DATECREATED || null,
expiredDate: item.EXPDATE || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
})); }));
} }

View File

@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { CounterfoilFee } from '../../models/service-provider/counterfoil-fee'; import { CounterfoilFee } from '../../models/fees/counterfoil-fee';
import { CommonService } from '../common/common.service'; import { CommonService } from '../common/common.service';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
@ -33,7 +33,9 @@ export class CounterfoilFeeService {
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
rate: item.RATE, rate: item.RATE,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null dateCreated: item.DATECREATED || null,
expiredDate: item.EXPDATE || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
})); }));
} }

View File

@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ExpeditedFee } from '../../models/service-provider/expedited-fee'; import { ExpeditedFee } from '../../models/fees/expedited-fee';
import { CommonService } from '../common/common.service'; import { CommonService } from '../common/common.service';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
@ -34,7 +34,9 @@ export class ExpeditedFeeService {
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
spid: item.SPID, spid: item.SPID,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null dateCreated: item.DATECREATED || null,
expiredDate: item.EXPDATE || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
})); }));
} }

View File

@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { SecurityDeposit } from '../../models/service-provider/security-deposit'; import { SecurityDeposit } from '../../models/fees/security-deposit';
import { CommonService } from '../common/common.service'; import { CommonService } from '../common/common.service';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
@ -30,10 +30,13 @@ export class SecurityDepositService {
specialCommodity: item.SPCLCOMMODITY, specialCommodity: item.SPCLCOMMODITY,
specialCountry: item.SPCLCOUNTRY, specialCountry: item.SPCLCOUNTRY,
rate: item.RATE, rate: item.RATE,
rateType: item.RATE_PCT,
effectiveDate: item.EFFDATE, effectiveDate: item.EFFDATE,
spid: item.SPID, spid: item.SPID,
createdBy: item.CREATEDBY || null, createdBy: item.CREATEDBY || null,
dateCreated: item.DATECREATED || null dateCreated: item.DATECREATED || null,
expiredDate: item.EXPDATE || null,
expired: item.EXPDATE ? new Date(item.EXPDATE) < new Date() : false
})); }));
} }
@ -43,10 +46,11 @@ export class SecurityDepositService {
P_SPID: spid, P_SPID: spid,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate), P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_HOLDERTYPE: data.holderType, P_HOLDERTYPE: data.holderType,
P_USCIBMEMBERFLAG: data.uscibMember ? 'Y' : 'N', P_USCIBMEMBERFLAG: data.uscibMember,
P_SPCLCOMMODITY: data.specialCommodity, P_SPCLCOMMODITY: data.specialCommodity,
P_SPCLCOUNTRY: data.specialCountry, P_SPCLCOUNTRY: data.specialCountry,
P_RATE: data.rate, P_RATE: data.rate,
P_PCT_VALUE: data.rateType,
P_USERID: this.userService.getUser() P_USERID: this.userService.getUser()
} }
@ -59,6 +63,7 @@ export class SecurityDepositService {
P_BONDRATESETUPID: id, P_BONDRATESETUPID: id,
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate), P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
P_RATE: data.rate, P_RATE: data.rate,
P_PCT_VALUE: data.rateType,
P_USERID: this.userService.getUser() P_USERID: this.userService.getUser()
} }

View File

@ -37,7 +37,10 @@ export class HomeService {
issueDate: item.ISSUEDATE || null, issueDate: item.ISSUEDATE || null,
expiryDate: item.EXPDATE || null, expiryDate: item.EXPDATE || null,
orderType: item.ORDERTYPE, orderType: item.ORDERTYPE,
carnetStatus: item.CARNETSTATUS carnetStatus: item.CARNETSTATUS,
securityType: item.SECURITYTYPE,
deliveryType: item.DELIVERYTYPE,
clientName: item.CLIENTNAMNE
})); }));
} }
} }

View File

@ -16,6 +16,12 @@ export class ParamService {
private userService = inject(UserService); private userService = inject(UserService);
getParameters(P_PARAMTYPE: string): Observable<ParamProperties[]> { getParameters(P_PARAMTYPE: string): Observable<ParamProperties[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetALLParamValues?P_SPID=${this.userService.getUserSpid()}&P_PARAMTYPE=${P_PARAMTYPE}`).pipe(
map(response => this.mapToParamValue(response))
)
}
getValidParameters(P_PARAMTYPE: string): Observable<ParamProperties[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_SPID=${this.userService.getUserSpid()}&P_PARAMTYPE=${P_PARAMTYPE}`).pipe( return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_SPID=${this.userService.getUserSpid()}&P_PARAMTYPE=${P_PARAMTYPE}`).pipe(
map(response => this.mapToParamValue(response)) map(response => this.mapToParamValue(response))
) )
@ -42,7 +48,6 @@ export class ParamService {
lastUpdatedDate: paramValue.LASTUPDATEDDATE, lastUpdatedDate: paramValue.LASTUPDATEDDATE,
errorMesg: paramValue.ERRORMESG, errorMesg: paramValue.ERRORMESG,
})); }));
} }
createTableRecord(description: string): Observable<any> { createTableRecord(description: string): Observable<any> {
@ -89,8 +94,6 @@ export class ParamService {
P_USERID: this.userService.getUser(), P_USERID: this.userService.getUser(),
}; };
console.log(paramDetails);
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateParamRecord`, paramDetails); return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateParamRecord`, paramDetails);
} }

View File

@ -0,0 +1,77 @@
import { inject, Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { map, Observable } from 'rxjs';
// import { UserService } from '../common/user.service';
// import { CommonService } from '../common/common.service';
import { Region } from '../../models/region/region';
@Injectable({
providedIn: 'root'
})
export class RegionService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
private http = inject(HttpClient);
// private userService = inject(UserService);
// private commonService = inject(CommonService);
getRegions(spid: number = 0): Observable<Region[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions/${spid}`).pipe(
map(response => this.mapToRegions(response)));
}
private mapToRegions(data: any[]): Region[] {
return data.map(item => ({
id: item.REGIONID,
value: item.REGION,
name: item.REGIONNAME,
// spid: item.SPID
}));
}
// getRegionById(regionId: number): Observable<Region> {
// return this.http.get<any>(`${this.apiUrl}/${this.apiDb}/GetRegion/${regionId}`).pipe(
// map(response => this.mapToRegion(response)));
// }
// private mapToRegion(data: any): Region {
// return {
// id: data.ID,
// Value: data.VALUE,
// Name: data.NAME,
// P_SPID: data.P_SPID
// };
// }
createRegion(region: Region, spid: number = 0): Observable<any> {
const payload = {
P_REGION: region.value,
P_NAME: region.name,
P_SPID: spid,
// P_USERID: this.userService.getUser()
};
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/InsertRegions`, payload);
}
updateRegion(regionId: number, region: Region): Observable<any> {
const payload = {
P_REGIONID: regionId,
P_NAME: region.name,
// P_USERID: this.userService.getUser()
};
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateRegion`, payload);
}
// deleteRegion(regionId: number): Observable<any> {
// const payload = {
// P_REGIONID: regionId,
// P_USERID: this.userService.getUser()
// };
// return this.http.delete<any>(`${this.apiUrl}/${this.apiDb}/DeleteRegion`, { body: payload });
// }
}

View File

@ -4,6 +4,7 @@ import { map, Observable } from 'rxjs';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { Contact } from '../../models/service-provider/contact'; import { Contact } from '../../models/service-provider/contact';
import { UserService } from '../common/user.service'; import { UserService } from '../common/user.service';
import { ContactLogin } from '../../models/service-provider/contact-login';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -38,7 +39,8 @@ export class ContactService {
lastUpdatedBy: contact.LASTUPDATEDBY || null, lastUpdatedBy: contact.LASTUPDATEDBY || null,
lastUpdatedDate: contact.LASTUPDATEDDATE || null, lastUpdatedDate: contact.LASTUPDATEDDATE || null,
isInactive: contact.INACTIVEFLAG === 'Y' || false, isInactive: contact.INACTIVEFLAG === 'Y' || false,
inactivatedDate: contact.INACTIVEDATE || null inactivatedDate: contact.INACTIVEDATE || null,
hasLogin: contact.LOGINFLAG === 'Y' || false
})); }));
} }
@ -49,7 +51,7 @@ export class ContactService {
p_defcontactflag: data.defaultContact ? 'Y' : 'N', p_defcontactflag: data.defaultContact ? 'Y' : 'N',
p_firstname: data.firstName, p_firstname: data.firstName,
p_lastname: data.lastName, p_lastname: data.lastName,
P_MIDDLEINITIAL: data.middleInitial, P_MIDDLEINITIAL: data.middleInitial ?? '',
p_title: data.title, p_title: data.title,
p_phoneno: data.phone, p_phoneno: data.phone,
p_mobileno: data.mobile, p_mobileno: data.mobile,
@ -67,7 +69,7 @@ export class ContactService {
p_spcontactid: spContactId, p_spcontactid: spContactId,
p_firstname: data.firstName, p_firstname: data.firstName,
p_lastname: data.lastName, p_lastname: data.lastName,
P_MIDDLEINITIAL: data.middleInitial, P_MIDDLEINITIAL: data.middleInitial ?? '',
p_title: data.title, p_title: data.title,
p_phoneno: data.phone, p_phoneno: data.phone,
p_mobileno: data.mobile, p_mobileno: data.mobile,
@ -80,7 +82,23 @@ export class ContactService {
} }
deleteContact(spContactId: string): Observable<any> { deleteContact(spContactId: string): Observable<any> {
return this.http.post(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${spContactId}`, null); return this.http.patch(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${spContactId}`, null);
} }
createContactLogin(data: ContactLogin): Observable<any> {
const contact = {
P_SPID: data.spid,
P_EMAILADDR: data.emailAddress,
P_ENABLEPASSWORDPOLICY: 'Y',
P_DOMAIN: 'test',
P_LOOKUPCODE: 'test',
P_USERID: this.userService.getUser()
}
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateSPLogins`, contact);
}
setDefaultContact(spContactId: string): Observable<any> {
return this.http.patch(`${this.apiUrl}/${this.apiDb}/SetSPDefaultContact/${spContactId}`, null);
}
} }

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StyleManagerService {
/**
* Set the stylesheet with the specified key.
*/
setStyle(key: string, href: string) {
getLinkElementForKey(key).setAttribute('href', href);
}
/**
* Remove the stylesheet with the specified key.
*/
removeStyle(key: string) {
const existingLinkElement = getExistingLinkElementByKey(key);
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
}
function getLinkElementForKey(key: string) {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string) {
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
}
function createLinkElementWithKey(key: string) {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
}
function getClassNameForKey(key: string) {
return `style-manager-${key}`;
}

View File

@ -0,0 +1,18 @@
import { inject, Injectable } from '@angular/core';
import { StyleManagerService } from './stylemanager.service';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private styleManager = inject(StyleManagerService);
setTheme(themeToSet: string) {
this.styleManager.setStyle(
"theme",
`themes/${themeToSet}.css`
);
}
}

View File

@ -1,4 +1,9 @@
<div class="basic-fee-container"> <div class="basic-fee-container">
<div class="actions-bar">
<mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
</div>
<div class="table-container mat-elevation-z8"> <div class="table-container mat-elevation-z8">
<div class="loading-shade" *ngIf="isLoading"> <div class="loading-shade" *ngIf="isLoading">
@ -32,6 +37,12 @@
<td mat-cell *matCellDef="let fee">{{ fee.effectiveDate | date:'mediumDate':'UTC' }}</td> <td mat-cell *matCellDef="let fee">{{ fee.effectiveDate | date:'mediumDate':'UTC' }}</td>
</ng-container> </ng-container>
<!-- Expired Date Column -->
<ng-container matColumnDef="expiredDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expired Date</th>
<td mat-cell *matCellDef="let fee">{{ fee.expiredDate | date:'mediumDate':'UTC' }}</td>
</ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
@ -53,8 +64,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Fee Form --> <!-- Fee Form -->
@ -136,10 +147,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="feeForm.invalid"> [disabled]="feeForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,9 +1,19 @@
.basic-fee-container { .basic-fee-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
.actions-bar {
clear: both;
margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
}
.table-container { .table-container {
position: relative; position: relative;
overflow: auto; overflow: auto;
@ -62,7 +72,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -79,6 +89,7 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
@ -130,7 +141,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -2,16 +2,16 @@ import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angu
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { BasicFee } from '../../core/models/service-provider/basic-fee'; import { BasicFee } from '../../core/models/fees/basic-fee';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator'; import { CustomPaginator } from '../../shared/custom-paginator';
import { forkJoin } from 'rxjs'; import { concat, finalize } from 'rxjs';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { BasicFeeService } from '../../core/services/service-provider/basic-fee.service'; import { BasicFeeService } from '../../core/services/fees/basic-fee.service';
@Component({ @Component({
selector: 'app-basic-fee', selector: 'app-basic-fee',
@ -21,16 +21,23 @@ import { BasicFeeService } from '../../core/services/service-provider/basic-fee.
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class BasicFeeComponent { export class BasicFeeComponent {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['startCarnetValue', 'endCarnetValue', 'fees', 'effectiveDate', 'actions']; displayedColumns: string[] = ['startCarnetValue', 'endCarnetValue', 'fees', 'effectiveDate', 'expiredDate', 'actions'];
dataSource = new MatTableDataSource<BasicFee>(); dataSource = new MatTableDataSource<BasicFee>();
feeForm: FormGroup; feeForm: FormGroup;
isEditing = false; isEditing = false;
currentFeeId: number | null = null; currentFeeId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
showExpiredRecords = false;
basicFees: BasicFee[] = [];
readOnlyFields: any = { readOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -67,11 +74,14 @@ export class BasicFeeComponent {
loadBasicFees(): void { loadBasicFees(): void {
this.isLoading = true; this.isLoading = true;
this.basicFeeService.getBasicFees(this.spid).subscribe({ this.basicFeeService.getBasicFees(this.spid).pipe(finalize(() => {
next: (fees) => {
this.dataSource.data = fees;
this.hasBasicFees.emit(fees.length > 0);
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (fees) => {
this.basicFees = fees;
this.dataSource.data = this.basicFees;
this.renderRecods();
this.hasBasicFees.emit(fees.length > 0);
if (this.dataSource.data.length == 0) { if (this.dataSource.data.length == 0) {
this.initializeDefaultFees(); this.initializeDefaultFees();
@ -80,21 +90,32 @@ export class BasicFeeComponent {
error: (error) => { error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic fees'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic fees');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading basic fees:', error); console.error('Error loading basic fees:', error);
} }
}); });
} }
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.basicFees.filter(record => record.expired);
} else {
this.dataSource.data = this.basicFees.filter(record => !record.expired);
}
}
initializeDefaultFees(): void { initializeDefaultFees(): void {
this.isLoading = true;
const defaultFees: BasicFee[] = [ const defaultFees: BasicFee[] = [
{ basicFeeId: 0, startCarnetValue: 1, endCarnetValue: 9999, fees: 255, effectiveDate: new Date() }, { 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: 10000, endCarnetValue: 49999, fees: 300, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 50000, endCarnetValue: 149999, fees: 365, 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: 150000, endCarnetValue: 399999, fees: 425, effectiveDate: new Date() },
{ basicFeeId: 0, startCarnetValue: 400000, endCarnetValue: 999999, fees: 480, 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() } { basicFeeId: 0, startCarnetValue: 1000000, endCarnetValue: 99999999, fees: 545, effectiveDate: new Date() }
]; ];
// Create an array of observables for each fee creation // Create an array of observables for each fee creation
@ -102,17 +123,21 @@ export class BasicFeeComponent {
this.basicFeeService.createBasicFee(this.spid, fee) this.basicFeeService.createBasicFee(this.spid, fee)
); );
// Execute all creations in parallel and wait for all to complete this.changeInProgress = true;
forkJoin(creationObservables).subscribe({
// Execute all creations in sequence
concat(...creationObservables).pipe(
finalize(() => {
this.changeInProgress = false;
})
).subscribe({
next: () => { next: () => {
},
complete: () => {
this.loadBasicFees(); // Refresh the list after all creations are done this.loadBasicFees(); // Refresh the list after all creations are done
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
this.isLoading = false;
console.error('Error initializing default fees:', error); console.error('Error initializing default fees:', error);
// Even if some failed, try to load what was created
this.loadBasicFees();
} }
}); });
} }
@ -175,7 +200,11 @@ export class BasicFeeComponent {
? this.basicFeeService.updateBasicFee(this.currentFeeId, feeData) ? this.basicFeeService.updateBasicFee(this.currentFeeId, feeData)
: this.basicFeeService.createBasicFee(this.spid, feeData); : this.basicFeeService.createBasicFee(this.spid, feeData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Basic fee ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Basic fee ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadBasicFees(); this.loadBasicFees();

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
<div class="fee-commission-container"> <div class="fee-commission-container">
<div class="actions-bar"> <div class="actions-bar">
<button mat-raised-button color="primary" *ngIf="!isEditMode" (click)="addNewFeeCommission()"> <mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewFeeCommission()">
<mat-icon>add</mat-icon> Add New Fee & Commission <mat-icon>add</mat-icon> Add New Fee & Commission
</button> </button>
</div> </div>
@ -20,7 +23,7 @@
<!-- Commission Rate Column --> <!-- Commission Rate Column -->
<ng-container matColumnDef="commissionRate"> <ng-container matColumnDef="commissionRate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Commission Rate</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Commission Rate</th>
<td mat-cell *matCellDef="let item">{{ item.commissionRate }}</td> <td mat-cell *matCellDef="let item">{{ item.commissionRate | number:'1.2-2' }}</td>
</ng-container> </ng-container>
<!-- Effective Date Column --> <!-- Effective Date Column -->
@ -29,6 +32,12 @@
<td mat-cell *matCellDef="let item">{{ item.effectiveDate | date:'mediumDate':'UTC' }}</td> <td mat-cell *matCellDef="let item">{{ item.effectiveDate | date:'mediumDate':'UTC' }}</td>
</ng-container> </ng-container>
<!-- Expired Date Column -->
<ng-container matColumnDef="expiredDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expired Date</th>
<td mat-cell *matCellDef="let fee">{{ fee.expiredDate | date:'mediumDate':'UTC' }}</td>
</ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
@ -54,8 +63,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Fee & Commission Form --> <!-- Fee & Commission Form -->
@ -77,15 +86,16 @@
Fee type is required Fee type is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Commission Rate</mat-label> <mat-label>Commission Rate</mat-label>
<input matInput type="number" formControlName="commissionRate"> <input matInput type="number" formControlName="commissionRate">
<mat-error *ngIf="feeCommissionForm.get('commissionRate')?.errors?.['required']"> <mat-error *ngIf="feeCommissionForm.get('commissionRate')?.errors?.['required']">
Commission rate is required Commission rate is required
</mat-error> </mat-error>
<mat-error *ngIf="feeCommissionForm.get('commissionRate')?.errors?.['pattern']">
Please enter a valid number with up to 2 decimal places
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@ -123,10 +133,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="feeCommissionForm.invalid"> [disabled]="feeCommissionForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.fee-commission-container { .fee-commission-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -8,6 +8,11 @@
clear: both; clear: both;
margin-bottom: -16px; margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button { button {
float: right; float: right;
} }
@ -76,7 +81,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -93,6 +98,7 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
@ -140,7 +146,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -7,13 +7,14 @@ import { MatTableDataSource } from '@angular/material/table';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator'; import { CustomPaginator } from '../../shared/custom-paginator';
import { CarnetFee } from '../../core/models/service-provider/carnet-fee'; import { CarnetFee } from '../../core/models/fees/carnet-fee';
import { FeeType } from '../../core/models/fee-type'; import { FeeType } from '../../core/models/fee-type';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { CommonService } from '../../core/services/common/common.service'; import { CommonService } from '../../core/services/common/common.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { CarnetFeeService } from '../../core/services/service-provider/carnet-fee.service'; import { CarnetFeeService } from '../../core/services/fees/carnet-fee.service';
import { finalize } from 'rxjs';
@Component({ @Component({
selector: 'app-carnet-fee', selector: 'app-carnet-fee',
@ -23,18 +24,24 @@ import { CarnetFeeService } from '../../core/services/service-provider/carnet-fe
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class CarnetFeeComponent implements OnInit { export class CarnetFeeComponent implements OnInit {
@ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['feeType', 'commissionRate', 'effectiveDate', 'actions']; displayedColumns: string[] = ['feeType', 'commissionRate', 'effectiveDate', 'expiredDate', 'actions'];
dataSource = new MatTableDataSource<CarnetFee>(); dataSource = new MatTableDataSource<CarnetFee>();
feeCommissionForm: FormGroup; feeCommissionForm: FormGroup;
isEditing = false; isEditing = false;
currentFeeCommissionId: number | null = null; currentFeeCommissionId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
feeTypes: FeeType[] = []; feeTypes: FeeType[] = [];
showExpiredRecords = false;
carnetFees: CarnetFee[] = [];
readOnlyFields: any = { readOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -55,7 +62,7 @@ export class CarnetFeeComponent implements OnInit {
constructor() { constructor() {
this.feeCommissionForm = this.fb.group({ this.feeCommissionForm = this.fb.group({
feeType: ['', Validators.required], feeType: ['', Validators.required],
commissionRate: [0, [Validators.required]], commissionRate: [0, [Validators.required, Validators.pattern(/^\d+\.?\d{0,2}$/)]],
effectiveDate: ['', Validators.required] effectiveDate: ['', Validators.required]
}); });
} }
@ -72,21 +79,36 @@ export class CarnetFeeComponent implements OnInit {
loadFeeCommissions(): void { loadFeeCommissions(): void {
this.isLoading = true; this.isLoading = true;
this.feeCommissionService.getFeeCommissions(this.spid).subscribe({ this.feeCommissionService.getFeeCommissions(this.spid).pipe(finalize(() => {
next: (fees) => {
this.dataSource.data = fees;
this.hasFeeCommissions.emit(fees.length > 0);
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (fees) => {
this.carnetFees = fees;
this.dataSource.data = this.carnetFees;
this.renderRecods();
this.hasFeeCommissions.emit(fees.length > 0);
}, },
error: (error) => { error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load fee & commission data'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load fee & commission data');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading fee & commission data:', error); console.error('Error loading fee & commission data:', error);
} }
}); });
} }
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.carnetFees.filter(record => record.expired);
} else {
this.dataSource.data = this.carnetFees.filter(record => !record.expired);
}
}
loadFeeTypes(): void { loadFeeTypes(): void {
this.commonService.getFeeTypes().subscribe({ this.commonService.getFeeTypes().subscribe({
next: (types) => { next: (types) => {
@ -151,7 +173,10 @@ export class CarnetFeeComponent implements OnInit {
? this.feeCommissionService.updateFeeCommission(this.currentFeeCommissionId, feeCommissionData) ? this.feeCommissionService.updateFeeCommission(this.currentFeeCommissionId, feeCommissionData)
: this.feeCommissionService.createFeeCommission(this.spid, feeCommissionData); : this.feeCommissionService.createFeeCommission(this.spid, feeCommissionData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Fee & commission ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Fee & commission ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadFeeCommissions(); this.loadFeeCommissions();

View File

@ -1,5 +1,8 @@
<div class="continuation-sheet-container"> <div class="continuation-sheet-container">
<div class="actions-bar"> <div class="actions-bar">
<mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewContinuationSheet()"> <button mat-raised-button color="primary" (click)="addNewContinuationSheet()">
<mat-icon>add</mat-icon> Add New Continuation Sheet <mat-icon>add</mat-icon> Add New Continuation Sheet
</button> </button>
@ -41,6 +44,12 @@
</td> </td>
</ng-container> </ng-container>
<!-- Expired Date Column -->
<ng-container matColumnDef="expiredDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expired Date</th>
<td mat-cell *matCellDef="let fee">{{ fee.expiredDate | date:'mediumDate':'UTC' }}</td>
</ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
@ -65,8 +74,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Continuation Sheet Form --> <!-- Continuation Sheet Form -->
@ -151,10 +160,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="continuationSheetForm.invalid"> [disabled]="continuationSheetForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.continuation-sheet-container { .continuation-sheet-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -8,6 +8,11 @@
clear: both; clear: both;
margin-bottom: -16px; margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button { button {
float: right; float: right;
} }
@ -76,7 +81,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -93,10 +98,15 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
} }
mat-label {
font-size: 0.875rem;
}
} }
.form-error { .form-error {
@ -158,7 +168,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -5,12 +5,12 @@ import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { of } from 'rxjs'; import { finalize, of } from 'rxjs';
import { CustomPaginator } from '../../shared/custom-paginator'; import { CustomPaginator } from '../../shared/custom-paginator';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { ContinuationSheetFeeService } from '../../core/services/service-provider/continuation-sheet-fee.service'; import { ContinuationSheetFeeService } from '../../core/services/fees/continuation-sheet-fee.service';
@Component({ @Component({
selector: 'app-continuation-sheet-fee', selector: 'app-continuation-sheet-fee',
@ -20,16 +20,23 @@ import { ContinuationSheetFeeService } from '../../core/services/service-provide
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class ContinuationSheetFeeComponent implements OnInit { export class ContinuationSheetFeeComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['customerType', 'carnetType', 'rate', 'effectiveDate', 'actions']; displayedColumns: string[] = ['customerType', 'carnetType', 'rate', 'effectiveDate', 'expiredDate', 'actions'];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
continuationSheetForm: FormGroup; continuationSheetForm: FormGroup;
isEditing = false; isEditing = false;
currentContinuationSheetId: number | null = null; currentContinuationSheetId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
showExpiredRecords = false;
continuationSheets: any[] = [];
readOnlyFields: any = { readOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -81,23 +88,36 @@ export class ContinuationSheetFeeComponent implements OnInit {
} }
loadContinuationSheets(): void { loadContinuationSheets(): void {
if (!this.spid) return;
this.isLoading = true; this.isLoading = true;
this.continuationSheetFeeService.getContinuationSheets(this.spid).subscribe({ this.continuationSheetFeeService.getContinuationSheets(this.spid).pipe(finalize(() => {
next: (continuationSheets) => {
this.dataSource.data = continuationSheets;
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (continuationSheets) => {
this.continuationSheets = continuationSheets;
this.dataSource.data = this.continuationSheets;
this.renderRecods();
}, },
error: (error) => { error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load continuation sheets'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load continuation sheets');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
return of([]); return of([]);
} }
}); });
} }
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.continuationSheets.filter(record => record.expired);
} else {
this.dataSource.data = this.continuationSheets.filter(record => !record.expired);
}
}
// applyFilter(event: Event): void { // applyFilter(event: Event): void {
// const filterValue = (event.target as HTMLInputElement).value; // const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase(); // this.dataSource.filter = filterValue.trim().toLowerCase();
@ -153,7 +173,10 @@ export class ContinuationSheetFeeComponent implements OnInit {
? this.continuationSheetFeeService.updateContinuationSheet(this.currentContinuationSheetId, continuationSheetData) ? this.continuationSheetFeeService.updateContinuationSheet(this.currentContinuationSheetId, continuationSheetData)
: this.continuationSheetFeeService.addContinuationSheet(this.spid, continuationSheetData); : this.continuationSheetFeeService.addContinuationSheet(this.spid, continuationSheetData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Continuation Sheet ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Continuation Sheet ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadContinuationSheets(); this.loadContinuationSheets();

View File

@ -1,5 +1,8 @@
<div class="counterfoil-container"> <div class="counterfoil-container">
<div class="actions-bar"> <div class="actions-bar">
<mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewCounterfoil()"> <button mat-raised-button color="primary" (click)="addNewCounterfoil()">
<mat-icon>add</mat-icon> Add New Counterfoil <mat-icon>add</mat-icon> Add New Counterfoil
</button> </button>
@ -53,6 +56,12 @@
</td> </td>
</ng-container> </ng-container>
<!-- Expired Date Column -->
<ng-container matColumnDef="expiredDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expired Date</th>
<td mat-cell *matCellDef="let fee">{{ fee.expiredDate | date:'mediumDate':'UTC' }}</td>
</ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
@ -77,8 +86,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Counterfoil Form --> <!-- Counterfoil Form -->
@ -197,10 +206,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="counterfoilForm.invalid"> [disabled]="counterfoilForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.counterfoil-container { .counterfoil-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -8,6 +8,11 @@
clear: both; clear: both;
margin-bottom: -16px; margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button { button {
float: right; float: right;
} }
@ -76,7 +81,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -93,10 +98,15 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
} }
mat-label {
font-size: 0.875rem;
}
} }
.form-error { .form-error {
@ -158,7 +168,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -5,12 +5,13 @@ import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CounterfoilFee } from '../../core/models/service-provider/counterfoil-fee'; import { CounterfoilFee } from '../../core/models/fees/counterfoil-fee';
import { CustomPaginator } from '../../shared/custom-paginator'; import { CustomPaginator } from '../../shared/custom-paginator';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { CounterfoilFeeService } from '../../core/services/service-provider/counterfoil-fee.service'; import { CounterfoilFeeService } from '../../core/services/fees/counterfoil-fee.service';
import { finalize } from 'rxjs';
@Component({ @Component({
selector: 'app-counterfoil-fee', selector: 'app-counterfoil-fee',
@ -20,16 +21,23 @@ import { CounterfoilFeeService } from '../../core/services/service-provider/coun
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class CounterfoilFeeComponent implements OnInit { export class CounterfoilFeeComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['customerType', 'carnetType', 'startSets', 'endSets', 'rate', 'effectiveDate', 'actions']; displayedColumns: string[] = ['customerType', 'carnetType', 'startSets', 'endSets', 'rate', 'effectiveDate', 'expiredDate', 'actions'];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
counterfoilForm: FormGroup; counterfoilForm: FormGroup;
isEditing = false; isEditing = false;
currentCounterfoilId: number | null = null; currentCounterfoilId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
showExpiredRecords = false;
counterfoilFees: CounterfoilFee[] = [];
readOnlyFields: any = { readOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -97,25 +105,38 @@ export class CounterfoilFeeComponent implements OnInit {
} }
loadCounterfoils(): void { loadCounterfoils(): void {
if (!this.spid) return;
this.isLoading = true; this.isLoading = true;
this.counterfoilFeeService.getCounterfoils(this.spid) this.counterfoilFeeService.getCounterfoils(this.spid).pipe(finalize(() => {
this.isLoading = false;
}))
.subscribe({ .subscribe({
next: ( next: (
counterfoils: CounterfoilFee[]) => { counterfoils: CounterfoilFee[]) => {
this.dataSource.data = counterfoils; this.counterfoilFees = counterfoils;
this.isLoading = false; this.dataSource.data = this.counterfoilFees;
this.renderRecods();
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load counterfoils'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load counterfoils');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading counterfoils:', error); console.error('Error loading counterfoils:', error);
} }
}); });
} }
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.counterfoilFees.filter(record => record.expired);
} else {
this.dataSource.data = this.counterfoilFees.filter(record => !record.expired);
}
}
// applyFilter(event: Event): void { // applyFilter(event: Event): void {
// const filterValue = (event.target as HTMLInputElement).value; // const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase(); // this.dataSource.filter = filterValue.trim().toLowerCase();
@ -177,7 +198,10 @@ export class CounterfoilFeeComponent implements OnInit {
? this.counterfoilFeeService.updateCounterfoil(this.currentCounterfoilId, counterfoilData) ? this.counterfoilFeeService.updateCounterfoil(this.currentCounterfoilId, counterfoilData)
: this.counterfoilFeeService.addCounterfoil(this.spid, counterfoilData); : this.counterfoilFeeService.addCounterfoil(this.spid, counterfoilData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Counterfoil ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Counterfoil ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadCounterfoils(); this.loadCounterfoils();

View File

@ -1,5 +1,8 @@
<div class="expedited-fee-container"> <div class="expedited-fee-container">
<div class="actions-bar"> <div class="actions-bar">
<mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewFee()"> <button mat-raised-button color="primary" (click)="addNewFee()">
<mat-icon>add</mat-icon> Add New Expedited Fee <mat-icon>add</mat-icon> Add New Expedited Fee
</button> </button>
@ -41,6 +44,12 @@
<td mat-cell *matCellDef="let fee">{{ fee.effectiveDate | date:'mediumDate':'UTC' }}</td> <td mat-cell *matCellDef="let fee">{{ fee.effectiveDate | date:'mediumDate':'UTC' }}</td>
</ng-container> </ng-container>
<!-- Expired Date Column -->
<ng-container matColumnDef="expiredDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expired Date</th>
<td mat-cell *matCellDef="let fee">{{ fee.expiredDate | date:'mediumDate':'UTC' }}</td>
</ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
@ -65,8 +74,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Fee Form --> <!-- Fee Form -->
@ -195,10 +204,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="feeForm.invalid"> [disabled]="feeForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.expedited-fee-container { .expedited-fee-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -8,6 +8,11 @@
clear: both; clear: both;
margin-bottom: -16px; margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button { button {
float: right; float: right;
} }
@ -76,7 +81,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -93,10 +98,15 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
} }
mat-label {
font-size: 0.875rem;
}
} }
.form-error { .form-error {
@ -158,7 +168,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -6,16 +6,16 @@ import { MatTableDataSource } from '@angular/material/table';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator'; import { CustomPaginator } from '../../shared/custom-paginator';
import { ExpeditedFee } from '../../core/models/service-provider/expedited-fee'; import { ExpeditedFee } from '../../core/models/fees/expedited-fee';
import { DeliveryType } from '../../core/models/delivery-type'; import { DeliveryType } from '../../core/models/delivery-type';
import { TimeZone } from '../../core/models/timezone'; import { TimeZone } from '../../core/models/timezone';
import { Subject, takeUntil } from 'rxjs'; import { finalize, Subject, takeUntil } from 'rxjs';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { CommonService } from '../../core/services/common/common.service'; import { CommonService } from '../../core/services/common/common.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { TimeFormatService } from '../../core/services/common/timeformat.service'; import { TimeFormatService } from '../../core/services/common/timeformat.service';
import { ExpeditedFeeService } from '../../core/services/service-provider/expedited-fee.service'; import { ExpeditedFeeService } from '../../core/services/fees/expedited-fee.service';
@Component({ @Component({
selector: 'app-expedited-fee', selector: 'app-expedited-fee',
@ -25,16 +25,23 @@ import { ExpeditedFeeService } from '../../core/services/service-provider/expedi
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class ExpeditedFeeComponent implements OnInit, OnDestroy { export class ExpeditedFeeComponent implements OnInit, OnDestroy {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['customerType', 'deliveryType', 'time', 'fee', 'effectiveDate', 'actions']; displayedColumns: string[] = ['customerType', 'deliveryType', 'time', 'fee', 'effectiveDate', 'expiredDate', 'actions'];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
feeForm: FormGroup; feeForm: FormGroup;
isEditing = false; isEditing = false;
currentFeeId: number | null = null; currentFeeId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
showExpiredRecords = false;
expeditedFees: ExpeditedFee[] = [];
readOnlyFields: any = { readOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -99,20 +106,35 @@ export class ExpeditedFeeComponent implements OnInit, OnDestroy {
loadExpeditedFees(): void { loadExpeditedFees(): void {
this.isLoading = true; this.isLoading = true;
this.expeditedFeeService.getExpeditedFees(this.spid).subscribe({ this.expeditedFeeService.getExpeditedFees(this.spid).pipe(finalize(() => {
next: (fees: ExpeditedFee[]) => {
this.dataSource.data = fees;
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (fees: ExpeditedFee[]) => {
this.expeditedFees = fees;
this.dataSource.data = this.expeditedFees;
this.renderRecods();
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load expedited fees'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load expedited fees');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading expedited fees:', error); console.error('Error loading expedited fees:', error);
} }
}); });
} }
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.expeditedFees.filter(record => record.expired);
} else {
this.dataSource.data = this.expeditedFees.filter(record => !record.expired);
}
}
loadLookupData(): void { loadLookupData(): void {
this.loadDeliveryTypes(); this.loadDeliveryTypes();
this.loadTimeZones(); this.loadTimeZones();
@ -124,17 +146,15 @@ export class ExpeditedFeeComponent implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (timeZones) => { next: (timeZones) => {
this.timeZones = timeZones; this.timeZones = timeZones;
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load time zones', error); console.error('Failed to load time zones', error);
this.isLoading = false;
} }
}); });
} }
loadDeliveryTypes(): void { loadDeliveryTypes(): void {
this.commonService.getDeliveryTypes(this.spid) this.commonService.getDeliveryTypes()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (deliveryTypes) => { next: (deliveryTypes) => {
@ -142,7 +162,6 @@ export class ExpeditedFeeComponent implements OnInit, OnDestroy {
}, },
error: (error) => { error: (error) => {
console.error('Failed to load delivery types', error); console.error('Failed to load delivery types', error);
this.isLoading = false;
} }
}); });
} }
@ -207,7 +226,10 @@ export class ExpeditedFeeComponent implements OnInit, OnDestroy {
? this.expeditedFeeService.updateExpeditedFee(this.currentFeeId, feeData) ? this.expeditedFeeService.updateExpeditedFee(this.currentFeeId, feeData)
: this.expeditedFeeService.createExpeditedFee(this.spid, feeData); : this.expeditedFeeService.createExpeditedFee(this.spid, feeData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Expedited fee ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Expedited fee ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadExpeditedFees(); this.loadExpeditedFees();

View File

@ -0,0 +1,62 @@
<h2 class="page-header">Manage Fees</h2>
<div class="manage-fees-action-buttons">
<button mat-button (click)="accordion().openAll()">Expand All</button>
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
</div>
<mat-accordion class="manage-fees-headers-align" multi>
<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-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Cargo Rate Setup </mat-panel-title>
</mat-expansion-panel-header>
<app-cargo-rate [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences">
</app-cargo-rate>
</mat-expansion-panel>
</mat-accordion>

View File

@ -0,0 +1,18 @@
.page-header {
margin: 0.5rem 0px;
color: var(--mat-sys-primary);
font-weight: 500;
}
.manage-fees-action-buttons {
padding-bottom: 20px;
}
.manage-fees-headers-align .mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.manage-fees-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
margin-left: 8px;
}

View File

@ -0,0 +1,35 @@
import { afterNextRender, Component, viewChild } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { MatAccordion } from '@angular/material/expansion';
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 { CommonModule } from '@angular/common';
import { UserPreferencesService } from '../../core/services/user-preference.service';
import { CargoRateComponent } from '../cargo-rate/cargo-rate.component';
@Component({
selector: 'app-manage-fee',
imports: [AngularMaterialModule, CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ContinuationSheetFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, CommonModule, CargoRateComponent],
templateUrl: './manage-fee.component.html',
styleUrl: './manage-fee.component.scss'
})
export class ManageFeeComponent {
accordion = viewChild.required(MatAccordion);
isEditMode = true;
spid = 0;
serviceProviderName: string | null = null;
userPreferences: UserPreferences;
constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
afterNextRender(() => {
// Open all panels
this.accordion().openAll();
});
}
}

View File

@ -1,5 +1,8 @@
<div class="security-deposit-container"> <div class="security-deposit-container">
<div class="actions-bar"> <div class="actions-bar">
<mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewDeposit()"> <button mat-raised-button color="primary" (click)="addNewDeposit()">
<mat-icon>add</mat-icon> Add New Security Deposit <mat-icon>add</mat-icon> Add New Security Deposit
</button> </button>
@ -21,28 +24,32 @@
<ng-container matColumnDef="uscibMember"> <ng-container matColumnDef="uscibMember">
<th mat-header-cell *matHeaderCellDef mat-sort-header>USCIB Member</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>USCIB Member</th>
<td mat-cell *matCellDef="let deposit"> <td mat-cell *matCellDef="let deposit">
<mat-icon [color]="deposit.uscibMember ? 'primary' : ''"> {{ deposit.uscibMember }}
{{ deposit.uscibMember ? 'check_circle' : 'cancel' }}
</mat-icon>
</td> </td>
</ng-container> </ng-container>
<!-- Special Commodity Column --> <!-- Special Commodity Column -->
<ng-container matColumnDef="specialCommodity"> <ng-container matColumnDef="specialCommodity">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Special Commodity</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Special Commodity</th>
<td mat-cell *matCellDef="let deposit">{{ deposit.specialCommodity || 'N/A' }}</td> <td mat-cell *matCellDef="let deposit">{{ deposit.specialCommodity }}</td>
</ng-container> </ng-container>
<!-- Special Country Column --> <!-- Special Country Column -->
<ng-container matColumnDef="specialCountry"> <ng-container matColumnDef="specialCountry">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Special Country</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Special Country</th>
<td mat-cell *matCellDef="let deposit">{{ getCountryName(deposit.specialCountry) || 'N/A' }}</td> <td mat-cell *matCellDef="let deposit">{{ getCountryName(deposit.specialCountry) }}</td>
</ng-container> </ng-container>
<!-- Rate Column --> <!-- Rate Column -->
<ng-container matColumnDef="rate"> <ng-container matColumnDef="rate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Rate</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Rate/Value</th>
<td mat-cell *matCellDef="let deposit">{{ deposit.rate | currency }}</td> <td mat-cell *matCellDef="let deposit">{{ deposit.rate }}</td>
</ng-container>
<!-- Rate Type Column -->
<ng-container matColumnDef="rateType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Rate In % or $</th>
<td mat-cell *matCellDef="let deposit">{{ deposit.rateType }}</td>
</ng-container> </ng-container>
<!-- Effective Date Column --> <!-- Effective Date Column -->
@ -51,6 +58,12 @@
<td mat-cell *matCellDef="let deposit">{{ deposit.effectiveDate | date:'mediumDate':'UTC' }}</td> <td mat-cell *matCellDef="let deposit">{{ deposit.effectiveDate | date:'mediumDate':'UTC' }}</td>
</ng-container> </ng-container>
<!-- Expired Date Column -->
<ng-container matColumnDef="expiredDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expired Date</th>
<td mat-cell *matCellDef="let fee">{{ fee.expiredDate | date:'mediumDate':'UTC' }}</td>
</ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
@ -79,8 +92,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Deposit Form --> <!-- Deposit Form -->
@ -92,20 +105,20 @@
<div class="form-row"> <div class="form-row">
<mat-label>Holder Type</mat-label> <mat-label>Holder Type</mat-label>
<mat-radio-group formControlName="holderType" required class="horizontal-group"> <mat-radio-group formControlName="holderType" class="horizontal-group">
<mat-radio-button *ngFor="let type of holderTypes" [value]="type.value" class="radio-button"> <mat-radio-button *ngFor="let type of holderTypes" [value]="type.value" class="radio-button">
{{ type.label }} {{ type.name }}
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
<mat-error *ngIf="depositForm.get('holderType')?.errors?.['required']"> <!-- <mat-error *ngIf="depositForm.get('holderType')?.errors?.['required']">
Holder type is required Holder type is required
</mat-error> </mat-error> -->
</div> </div>
<div class="form-row"> <div class="form-row">
<mat-label>USCIB Member</mat-label> <mat-label>USCIB Member</mat-label>
<mat-radio-group formControlName="uscibMember" required class="horizontal-group"> <mat-radio-group formControlName="uscibMember" required class="horizontal-group">
<mat-radio-button *ngFor="let type of yesNoOptions" [value]="type.value" class="radio-button"> <mat-radio-button *ngFor="let type of uscibMemberOptions" [value]="type.value" class="radio-button">
{{ type.label }} {{ type.label }}
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
@ -133,9 +146,9 @@
<div class="form-row"> <div class="form-row">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Rate</mat-label> <mat-label>Rate / Value</mat-label>
<input matInput type="number" formControlName="rate" required min="0" step="1"> <input matInput type="number" formControlName="rate" required min="0" step="1">
<span class="dollar-prefix" matPrefix>$&nbsp;</span> <!-- <span class="dollar-prefix" matPrefix>$&nbsp;</span> -->
<mat-error *ngIf="depositForm.get('rate')?.errors?.['required']"> <mat-error *ngIf="depositForm.get('rate')?.errors?.['required']">
Rate is required Rate is required
</mat-error> </mat-error>
@ -144,6 +157,14 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Rate in % or $</mat-label>
<input matInput formControlName="rateType" required>
<mat-error *ngIf="depositForm.get('rateType')?.errors?.['required']">
Rate in % or $ is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Effective Date</mat-label> <mat-label>Effective Date</mat-label>
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required> <input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>
@ -179,10 +200,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="depositForm.invalid"> [disabled]="depositForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.security-deposit-container { .security-deposit-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -8,6 +8,11 @@
clear: both; clear: both;
margin-bottom: -16px; margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button { button {
float: right; float: right;
} }
@ -81,7 +86,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -98,10 +103,15 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
} }
mat-label {
font-size: 0.875rem;
}
} }
.form-error { .form-error {
@ -163,7 +173,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -7,12 +7,14 @@ import { AngularMaterialModule } from '../../shared/module/angular-material.modu
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CustomPaginator } from '../../shared/custom-paginator'; import { CustomPaginator } from '../../shared/custom-paginator';
import { Country } from '../../core/models/country'; import { Country } from '../../core/models/country';
import { SecurityDeposit } from '../../core/models/service-provider/security-deposit'; import { SecurityDeposit } from '../../core/models/fees/security-deposit';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { CommonService } from '../../core/services/common/common.service'; import { CommonService } from '../../core/services/common/common.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { SecurityDepositService } from '../../core/services/service-provider/security-deposit.service'; import { SecurityDepositService } from '../../core/services/fees/security-deposit.service';
import { finalize } from 'rxjs';
import { HolderType } from '../../core/models/holder-type';
@Component({ @Component({
selector: 'app-security-deposit', selector: 'app-security-deposit',
@ -22,16 +24,23 @@ import { SecurityDepositService } from '../../core/services/service-provider/sec
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class SecurityDepositComponent implements OnInit { export class SecurityDepositComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['holderType', 'uscibMember', 'specialCommodity', 'specialCountry', 'rate', 'effectiveDate', 'actions']; displayedColumns: string[] = ['holderType', 'uscibMember', 'specialCommodity', 'specialCountry', 'rate', 'rateType', 'effectiveDate', 'expiredDate', 'actions'];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
depositForm: FormGroup; depositForm: FormGroup;
isEditing = false; isEditing = false;
currentDepositId: number | null = null; currentDepositId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
showExpiredRecords = false;
securityDeposits: SecurityDeposit[] = [];
readOnlyFields: any = { readOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -39,21 +48,17 @@ export class SecurityDepositComponent implements OnInit {
}; };
countries: Country[] = []; countries: Country[] = [];
holderTypes: HolderType[] = [];
@Input() isEditMode = false; @Input() isEditMode = false;
@Input() spid: number = 0; @Input() spid: number = 0;
@Input() userPreferences!: UserPreferences; @Input() userPreferences!: UserPreferences;
@Output() hasSecurityDeposits = new EventEmitter<boolean>(); @Output() hasSecurityDeposits = new EventEmitter<boolean>();
holderTypes = [ uscibMemberOptions = [
{ value: 'CORP', label: 'Corporation' }, { value: 'Yes', label: 'Yes' },
{ value: 'INDIVIDUAL', label: 'Individual' }, { value: 'No', label: 'No' },
{ value: 'GOVERNMENT', label: 'Government Agency' } { value: 'Both', label: 'Both' }
];
yesNoOptions = [
{ value: 'Y', label: 'Yes' },
{ value: 'N', label: 'No' }
]; ];
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
@ -64,11 +69,12 @@ export class SecurityDepositComponent implements OnInit {
constructor() { constructor() {
this.depositForm = this.fb.group({ this.depositForm = this.fb.group({
holderType: ['CORP', Validators.required], holderType: ['CORP'],
uscibMember: ['Y', Validators.required], uscibMember: ['Yes', Validators.required],
specialCommodity: [''], specialCommodity: [''],
specialCountry: [''], specialCountry: [''],
rate: [0, [Validators.required, Validators.min(0)]], rate: [0, [Validators.required, Validators.min(0)]],
rateType: ['', [Validators.required]],
effectiveDate: ['', Validators.required] effectiveDate: ['', Validators.required]
}); });
} }
@ -76,6 +82,7 @@ export class SecurityDepositComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.loadSecurityDeposits(); this.loadSecurityDeposits();
this.loadCountries(); this.loadCountries();
this.loadHolderTypes();
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -85,20 +92,35 @@ export class SecurityDepositComponent implements OnInit {
loadSecurityDeposits(): void { loadSecurityDeposits(): void {
this.isLoading = true; this.isLoading = true;
this.securityDepositService.getSecurityDeposits(this.spid).subscribe({ this.securityDepositService.getSecurityDeposits(this.spid).pipe(finalize(() => {
next: (deposits) => {
this.dataSource.data = deposits;
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (deposits) => {
this.securityDeposits = deposits;
this.dataSource.data = this.securityDeposits;
this.renderRecods();
}, },
error: (error) => { error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load security deposits'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load security deposits');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading security deposits:', error); console.error('Error loading security deposits:', error);
} }
}); });
} }
toggleShowExpiredRecords(): void {
this.showExpiredRecords = !this.showExpiredRecords;
this.renderRecods();
}
renderRecods(): void {
if (this.showExpiredRecords) {
this.dataSource.data = this.securityDeposits.filter(record => record.expired);
} else {
this.dataSource.data = this.securityDeposits.filter(record => !record.expired);
}
}
loadCountries(): void { loadCountries(): void {
this.commonService.getCountries().subscribe({ this.commonService.getCountries().subscribe({
next: (countries) => { next: (countries) => {
@ -110,6 +132,16 @@ export class SecurityDepositComponent implements OnInit {
}); });
} }
loadHolderTypes(): void {
this.commonService.getHolderTypes().subscribe({
next: (holderTypes) => {
this.holderTypes = [...holderTypes, { value: '', name: 'All', id: "0" }];
},
error: (error) => {
console.error('Error loading holder types:', error);
}
});
}
// applyFilter(event: Event): void { // applyFilter(event: Event): void {
// const filterValue = (event.target as HTMLInputElement).value; // const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase(); // this.dataSource.filter = filterValue.trim().toLowerCase();
@ -125,7 +157,8 @@ export class SecurityDepositComponent implements OnInit {
this.currentDepositId = null; this.currentDepositId = null;
this.depositForm.reset({ this.depositForm.reset({
holderType: 'CORP', holderType: 'CORP',
uscibMember: 'N', uscibMember: 'No',
specialCountry: '',
}); });
this.depositForm.patchValue({ rate: 0 }); this.depositForm.patchValue({ rate: 0 });
@ -141,10 +174,11 @@ export class SecurityDepositComponent implements OnInit {
this.currentDepositId = deposit.securityDepositId; this.currentDepositId = deposit.securityDepositId;
this.depositForm.patchValue({ this.depositForm.patchValue({
holderType: deposit.holderType, holderType: deposit.holderType,
uscibMember: deposit.uscibMember ? 'Y' : 'N', uscibMember: deposit.uscibMember,
specialCommodity: deposit.specialCommodity, specialCommodity: deposit.specialCommodity,
specialCountry: deposit.specialCountry, specialCountry: deposit.specialCountry,
rate: deposit.rate, rate: deposit.rate,
rateType: deposit.rateType,
effectiveDate: deposit.effectiveDate effectiveDate: deposit.effectiveDate
}); });
@ -165,7 +199,6 @@ export class SecurityDepositComponent implements OnInit {
const depositData = { const depositData = {
...this.depositForm.value, ...this.depositForm.value,
uscibMember: this.depositForm.value.uscibMember === 'Y',
specialCommodity: this.depositForm.value.specialCommodity || null, specialCommodity: this.depositForm.value.specialCommodity || null,
specialCountry: this.depositForm.value.specialCountry || null specialCountry: this.depositForm.value.specialCountry || null
}; };
@ -174,7 +207,10 @@ export class SecurityDepositComponent implements OnInit {
? this.securityDepositService.updateSecurityDeposit(this.currentDepositId, depositData) ? this.securityDepositService.updateSecurityDeposit(this.currentDepositId, depositData)
: this.securityDepositService.createSecurityDeposit(this.spid, depositData); : this.securityDepositService.createSecurityDeposit(this.spid, depositData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Security deposit ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Security deposit ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadSecurityDeposits(); this.loadSecurityDeposits();
@ -221,13 +257,14 @@ export class SecurityDepositComponent implements OnInit {
this.currentDepositId = null; this.currentDepositId = null;
this.depositForm.reset({ this.depositForm.reset({
holderType: 'CORP', holderType: 'CORP',
uscibMember: 'N', uscibMember: 'No',
specialCountry: '',
}); });
} }
getHolderTypeLabel(value: string): string { getHolderTypeLabel(value: string): string {
const type = this.holderTypes.find(t => t.value === value); const type = this.holderTypes.find(t => t.value === value);
return type ? type.label : value; return type ? type.name : value;
} }
getCountryName(code: string): string { getCountryName(code: string): string {

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
import { AuthService } from '../core/services/common/auth.service'; import { AuthService } from '../core/services/common/auth.service';
import { NotificationService } from '../core/services/common/notification.service'; import { NotificationService } from '../core/services/common/notification.service';
import { ThemeService } from '../core/services/theme.service';
import { AngularMaterialModule } from '../shared/module/angular-material.module'; import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -26,6 +27,7 @@ export class ForgotPasswordComponent {
private router = inject(Router); private router = inject(Router);
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
private themeService = inject(ThemeService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
constructor() { constructor() {
@ -50,6 +52,10 @@ export class ForgotPasswordComponent {
} }
} }
ngOnInit(): void {
this.themeService.setTheme('default');
}
onRequest(): void { onRequest(): void {
if (this.forgotPasswordRequestForm.invalid) { if (this.forgotPasswordRequestForm.invalid) {
this.forgotPasswordRequestForm.markAllAsTouched(); this.forgotPasswordRequestForm.markAllAsTouched();

View File

@ -1,5 +1,3 @@
@use 'colors' as colors;
.dashboard-chart-container { .dashboard-chart-container {
padding: 24px 0px; padding: 24px 0px;
margin-bottom: 24px; margin-bottom: 24px;
@ -36,7 +34,7 @@
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
color: colors.$primary-color; color: var(--mat-sys-primary);
} }
.edit-icon { .edit-icon {
@ -44,7 +42,7 @@
transition: color 0.2s ease; transition: color 0.2s ease;
&:hover { &:hover {
color: colors.$primary-color; color: var(--mat-sys-primary);
} }
mat-icon { mat-icon {

View File

@ -8,7 +8,8 @@
<!-- Chart Section --> <!-- Chart Section -->
<div class="chart-section"> <div class="chart-section">
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses" (carnetStatusData)="onCarnetStatusClick($event)"></app-chart> <app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses"
(carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
</div> </div>
<!-- Carnet Details Section --> <!-- Carnet Details Section -->
@ -18,6 +19,34 @@
</div> </div>
<div class="table-actions"> <div class="table-actions">
<div class="filter-controls" *ngIf="dataSource.data.length > 0">
<!-- Column Selection Dropdown -->
<mat-form-field appearance="outline" class="filter-field">
<mat-label>Filter By</mat-label>
<mat-select [(value)]="selectedColumn" (selectionChange)="onColumnChange()">
<mat-option value=""></mat-option>
<mat-option *ngFor="let column of filterableColumns" [value]="column.value">
{{column.displayName}}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Search Input -->
<mat-form-field appearance="outline" class="filter-field">
<mat-label>Search Value</mat-label>
<input matInput [(ngModel)]="filterValue" (keyup)="applyFilter()"
placeholder="Enter search term...">
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
<!-- Clear Filter Button -->
<button mat-raised-button type="button" (click)="clearFilter()"
[disabled]="!selectedColumn && !filterValue">
<mat-icon>clear_all</mat-icon>
Clear Filter
</button>
</div>
<button mat-raised-button color="accent" (click)="exportData()" *ngIf="dataSource.data.length > 0" <button mat-raised-button color="accent" (click)="exportData()" *ngIf="dataSource.data.length > 0"
matTooltip="Export to Excel" [disabled]="dataSource.data.length === 0"> matTooltip="Export to Excel" [disabled]="dataSource.data.length === 0">
<mat-icon>download</mat-icon> Export <mat-icon>download</mat-icon> Export
@ -31,6 +60,11 @@
<td mat-cell *matCellDef="let item">{{ item.applicationName }}</td> <td mat-cell *matCellDef="let item">{{ item.applicationName }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Client Name</th>
<td mat-cell *matCellDef="let item">{{ item.clientName }}</td>
</ng-container>
<ng-container matColumnDef="holderName"> <ng-container matColumnDef="holderName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Name</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Name</th>
<td mat-cell *matCellDef="let item">{{ item.holderName }}</td> <td mat-cell *matCellDef="let item">{{ item.holderName }}</td>
@ -41,6 +75,11 @@
<td mat-cell *matCellDef="let item">{{ item.carnetNumber }}</td> <td mat-cell *matCellDef="let item">{{ item.carnetNumber }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="securityType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Security Type</th>
<td mat-cell *matCellDef="let item">{{ item.securityType }}</td>
</ng-container>
<ng-container matColumnDef="usSets"> <ng-container matColumnDef="usSets">
<th mat-header-cell *matHeaderCellDef mat-sort-header>US Sets</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>US Sets</th>
<td mat-cell *matCellDef="let item">{{ item.usSets }}</td> <td mat-cell *matCellDef="let item">{{ item.usSets }}</td>
@ -57,7 +96,7 @@
<ng-container matColumnDef="carnetValue"> <ng-container matColumnDef="carnetValue">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Value</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Value</th>
<td mat-cell *matCellDef="let item">{{ item.carnetValue }}</td> <td mat-cell *matCellDef="let item">{{ item.carnetValue | currency}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="issueDate"> <ng-container matColumnDef="issueDate">
@ -93,7 +132,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!"
[length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>

View File

@ -12,10 +12,13 @@
.table-actions { .table-actions {
display: flex; display: flex;
justify-content: end; justify-content: space-between;
button {
margin-bottom: 16px; margin-bottom: 16px;
.filter-controls {
display: flex;
gap: 16px;
align-items: center;
} }
} }

View File

@ -15,10 +15,11 @@ import { ApiErrorHandlerService } from '../core/services/common/api-error-handle
import { CommonService } from '../core/services/common/common.service'; import { CommonService } from '../core/services/common/common.service';
import { NotificationService } from '../core/services/common/notification.service'; import { NotificationService } from '../core/services/common/notification.service';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { FormsModule } from '@angular/forms';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
imports: [AngularMaterialModule, ChartComponent, RouterLink, CommonModule], imports: [AngularMaterialModule, ChartComponent, RouterLink, CommonModule, FormsModule],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss', styleUrl: './home.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
@ -30,6 +31,25 @@ export class HomeComponent {
userPreferences: UserPreferences; userPreferences: UserPreferences;
carnetStatuses: CarnetStatus[] = []; carnetStatuses: CarnetStatus[] = [];
// table filter variables
selectedColumn: string = '';
filterValue: string = '';
filterableColumns: any[] = [
{ value: 'applicationName', displayName: 'Application Name' },
{ value: 'clientName', displayName: 'Client Name' },
{ value: 'holderName', displayName: 'Holder Name' },
{ value: 'carnetNumber', displayName: 'Carnet Number' },
{ value: 'securityType', displayName: 'Security Type' },
{ value: 'usSets', displayName: 'US Sets' },
{ value: 'foreignSets', displayName: 'Foreign Sets' },
{ value: 'transitSets', displayName: 'Transit Sets' },
{ value: 'carnetValue', displayName: 'Carnet Value' },
{ value: 'issueDate', displayName: 'Issue Date' },
{ value: 'expiryDate', displayName: 'Expiry Date' },
{ value: 'orderType', displayName: 'Order Type' },
{ value: 'carnetStatus', displayName: 'Carnet Status' }
];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
@ViewChild(MatPaginator, { static: false }) @ViewChild(MatPaginator, { static: false })
@ -42,7 +62,7 @@ export class HomeComponent {
this.dataSource.sort = value; this.dataSource.sort = value;
} }
displayedColumns: string[] = ['applicationName', 'holderName', 'carnetNumber', 'usSets', 'foreignSets', 'transitSets', 'carnetValue', 'issueDate', 'expiryDate', 'orderType', 'carnetStatus']; displayedColumns: string[] = ['applicationName', 'clientName', 'holderName', 'carnetNumber', 'securityType', 'usSets', 'foreignSets', 'transitSets', 'carnetValue', 'issueDate', 'expiryDate', 'orderType', 'carnetStatus'];
private homeService = inject(HomeService); private homeService = inject(HomeService);
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
@ -86,13 +106,20 @@ export class HomeComponent {
onCarnetStatusClick(event: any): void { onCarnetStatusClick(event: any): void {
this.isLoading = true; this.isLoading = true;
this.showTable = false; this.showTable = false;
// Clear any existing filters when loading new data
this.clearFilter();
this.homeService.getCarnetDataByStatus(event.spid, event.carnetStatus).subscribe({ this.homeService.getCarnetDataByStatus(event.spid, event.carnetStatus).subscribe({
next: (carnetDetails) => { next: (carnetDetails) => {
this.isLoading = false; this.isLoading = false;
this.showTable = true; this.showTable = carnetDetails.length > 0;
this.dataSource.data = carnetDetails; this.dataSource.data = carnetDetails;
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
// Reset filter after new data is loaded
this.dataSource.filter = '';
}, },
error: (error) => { error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load carnet data for the selected status'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load carnet data for the selected status');
@ -127,8 +154,10 @@ export class HomeComponent {
prepareDownloadData(): any[] { prepareDownloadData(): any[] {
return this.dataSource.data.map(item => ({ return this.dataSource.data.map(item => ({
'Application Name': item.applicationName, 'Application Name': item.applicationName,
'Client Name': item.clientName,
'Holder Name': item.holderName, 'Holder Name': item.holderName,
'Carnet Number': item.carnetNumber, 'Carnet Number': item.carnetNumber,
'Security Type': item.securityType,
'US Sets': item.usSets, 'US Sets': item.usSets,
'Foreign Sets': item.foreignSets, 'Foreign Sets': item.foreignSets,
'Transit Sets': item.transitSets, 'Transit Sets': item.transitSets,
@ -144,4 +173,87 @@ export class HomeComponent {
const carnetStatus = this.carnetStatuses.find(t => t.value === value); const carnetStatus = this.carnetStatuses.find(t => t.value === value);
return carnetStatus ? carnetStatus.name : value; return carnetStatus ? carnetStatus.name : value;
} }
applyFilter(): void {
if (!this.dataSource) return;
if (this.selectedColumn && this.filterValue) {
this.dataSource.filterPredicate = (data: any, filter: string) => {
const columnValue = data[this.selectedColumn];
const searchTerm = filter.toLowerCase().trim();
// Handle different column types
switch (this.selectedColumn) {
case 'issueDate':
case 'expiryDate':
// Filter by date
if (columnValue) {
const formattedDate = this.formatDateForDisplay(columnValue);
return formattedDate.toLowerCase().includes(searchTerm);
}
return false;
case 'carnetStatus':
// Filter by status label instead of value
const statusLabel = this.getCarnetStatusLabel(columnValue);
return statusLabel.toLowerCase().includes(searchTerm);
default:
// Default string/number filter
if (typeof columnValue === 'string') {
return columnValue.toLowerCase().includes(searchTerm);
} else if (typeof columnValue === 'number') {
return columnValue.toString().includes(searchTerm);
}
return false;
}
};
this.dataSource.filter = this.filterValue.trim();
} else if (!this.filterValue) {
this.clearFilterData();
}
}
onColumnChange(): void {
this.clearFilterData();
}
clearFilter(): void {
this.selectedColumn = '';
this.clearFilterData();
}
clearFilterData(): void {
this.filterValue = '';
if (this.dataSource) {
this.dataSource.filter = '';
}
}
private formatDateForDisplay(dateValue: any): string {
if (!dateValue) return '';
try {
const date = new Date(dateValue);
// Return in multiple formats for better filtering
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const fullMonthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const month = monthNames[date.getUTCMonth()];
const fullMonth = fullMonthNames[date.getUTCMonth()];
const day = date.getUTCDate();
const year = date.getUTCFullYear();
const monthNumber = date.getUTCMonth() + 1;
// Return multiple formats separated by delimiter for filtering
return `${month} ${day}, ${year}|${fullMonth} ${day}, ${year}|${monthNumber}/${day}/${year}|${year}-${monthNumber.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
} catch (error) {
return '';
}
}
} }

View File

@ -1,5 +1,3 @@
@use 'colors' as colors;
.login-container { .login-container {
display: flex; display: flex;
min-height: 85vh; min-height: 85vh;
@ -26,7 +24,7 @@
.welcome-title { .welcome-title {
text-align: center; text-align: center;
color: colors.$primary-color; color: var(--mat-sys-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -76,7 +74,7 @@
.info-section { .info-section {
flex: 1; flex: 1;
padding: 4rem; padding: 4rem;
background-color: colors.$primary-color; background-color: var(--mat-sys-primary);
color: white; color: white;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -7,6 +7,7 @@ import { AngularMaterialModule } from '../shared/module/angular-material.module'
import { User } from '../core/models/user'; import { User } from '../core/models/user';
import { UserService } from '../core/services/common/user.service'; import { UserService } from '../core/services/common/user.service';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ThemeService } from '../core/services/theme.service';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -23,6 +24,7 @@ export class LoginComponent {
private userService = inject(UserService); private userService = inject(UserService);
private navigationService = inject(NavigationService); private navigationService = inject(NavigationService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private themeService = inject(ThemeService);
constructor() { constructor() {
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
@ -32,6 +34,8 @@ export class LoginComponent {
} }
ngOnInit(): void { ngOnInit(): void {
this.themeService.setTheme('default');
if (this.userService.isLoggedIn()) { if (this.userService.isLoggedIn()) {
this.navigationService.navigate(['/home']); this.navigationService.navigate(['/home']);
} }

View File

@ -0,0 +1,81 @@
<h2 class="page-header">Manage Country Messages</h2>
<div class="manage-container">
<!-- Country Messages Form -->
<div class="form-container">
<form [formGroup]="countryForm" (ngSubmit)="saveRecord()">
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Country</mat-label>
<mat-select (selectionChange)="onCountrySelectionChanged($event)">
<mat-option *ngFor="let country of countries" [value]="country.paramId">
{{ country.paramDesc }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Message</mat-label>
<textarea matInput formControlName="countryMessage" placeholder="Enter a message" rows="10"
required></textarea>
<mat-error *ngIf="countryForm.get('countryMessage')?.errors?.['required']">
Message is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Type of Action</mat-label>
<mat-select (selectionChange)="onActionTypeSelectionChanged($event)" formControlName="actionType">
<mat-option *ngFor="let actionType of actionTypes" [value]="actionType">
{{ actionType }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Action </mat-label>
<input matInput formControlName="action">
</mat-form-field>
</div>
<!-- <div class="readonly-section top-divider">
<div class="readonly-fields">
<div class="field-column">
<div class="readonly-field">
<label>Action Type</label>
<div class="readonly-value">
{{paramReadOnlyFields.actionType || 'N/A'}}
</div>
</div>
</div>
<div class="field-column">
<div class="readonly-field">
<label>Action </label>
<div class="readonly-value">
{{paramReadOnlyFields.action || 'N/A'}}
</div>
</div>
</div>
</div>
</div> -->
<div class="form-actions">
<button mat-raised-button color="primary" type="submit"
[disabled]="countryForm.invalid || changeInProgress">
Save
</button>
<button mat-raised-button color="primary" type="button" *ngIf="!currentCountry?.isInactive"
(click)="inActivateParamRecord()">Inactivate
Record</button>
<button mat-raised-button color="primary" type="button" *ngIf="currentCountry?.isInactive"
(click)="reActivateParamRecord()">Reactivate
Record</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,105 @@
.page-header {
margin: 0.5rem 0px;
color: var(--mat-sys-primary);
font-weight: 500;
}
.manage-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
.form-container {
margin-top: 0.5rem;
.form-header {
margin-bottom: 24px;
h3 {
margin: 0;
color: var(--mat-sys-primary);
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
align-items: start;
mat-form-field {
flex: 1;
}
.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.875rem;
display: flex;
align-items: center;
}
}
}
}
}
.results-section {
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;
}
}

View File

@ -0,0 +1,258 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormGroup, Validators, FormBuilder } from '@angular/forms';
import { finalize, forkJoin } from 'rxjs';
import { ParamProperties } from '../../core/models/param/parameters';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service';
import { ParamService } from '../../core/services/param/param.service';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { MatSelectChange } from '@angular/material/select';
import { CountryMessage } from '../../core/models/param/country-message';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-manage-country',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './manage-country.component.html',
styleUrl: './manage-country.component.scss'
})
export class ManageCountryComponent {
isLoading: boolean = false;
changeInProgress: boolean = false;
countries: CountryMessage[] = [];
countriesWithMessages: ParamProperties[] = [];
currentCountryId: number = 0;
currentCountry: CountryMessage | undefined | null = null;
actionTypes: string[] = ['WARN', 'FYI', 'ACTION'];
private paramService = inject(ParamService);
private errorHandler = inject(ApiErrorHandlerService);
private notificationService = inject(NotificationService);
private fb = inject(FormBuilder);
private dialog = inject(MatDialog);
countryForm: FormGroup;
constructor() {
this.countryForm = this.initializeForm();
}
initializeForm(): FormGroup {
return this.fb.group({
countryMessage: ['', [Validators.required]],
actionType: [{ value: '' }],
action: [{ value: '', disabled: true }]
});
}
ngOnInit(): void {
this.getCountries();
}
onCountrySelectionChanged(event: MatSelectChange) {
this.renderCountry(+event.value);
}
renderCountry(countryid: number): void {
this.currentCountry = this.countries.find(c => c.paramId === countryid);
this.currentCountryId = this.countriesWithMessages.find(c => c.paramValue === this.currentCountry?.paramValue)?.paramId ?? 0;
this.patchCountryData();
}
onActionTypeSelectionChanged(event: MatSelectChange) {
if (event.value === 'ACTION') {
this.countryForm.get('action')?.setValue('TRANSIT');
}
}
getCountries(): void {
this.isLoading = true;
forkJoin({
countries: this.paramService.getValidParameters('002'),
countriesWithMessages: this.paramService.getValidParameters('014')
}).pipe(finalize(() => {
this.isLoading = false;
})).subscribe({
next: (results) => {
this.countries = this.joinCountriesWithMessages(results.countries, results.countriesWithMessages);
this.countriesWithMessages = results.countriesWithMessages;
if (this.currentCountry) {
this.renderCountry(this.currentCountry.paramId!);
}
},
error: (error) => {
console.error('Error loading countries messages data', error);
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load countries messages data');
this.notificationService.showError(errorMessage);
}
});
}
patchCountryData(): void {
if (!this.currentCountry) {
return;
}
this.countryForm.patchValue({
countryMessage: this.currentCountry.countryMessage,
actionType: this.currentCountry.addlParamValue1,
action: this.currentCountry.addlParamValue2
});
if (this.currentCountry.addlParamValue1 === 'ACTION') {
this.countryForm.get('action')?.setValue('TRANSIT');
} else {
this.countryForm.get('action')?.setValue('');
}
}
saveRecord(): void {
if (this.countryForm.invalid) {
this.countryForm.markAllAsTouched();
return;
}
const paramData: ParamProperties = {
paramDesc: this.countryForm.value.countryMessage,
paramType: '014',
paramValue: this.currentCountry?.paramValue ?? '',
addlParamValue1: this.countryForm.value.actionType,
addlParamValue2: this.countryForm.value.actionType === 'ACTION' ? 'TRANSIT' : null,
sortSeq: 1,
paramId: this.currentCountryId ?? 0
};
const saveObservable = this.currentCountryId > 0
? this.paramService.updateParamRecord(paramData)
: this.paramService.createParamRecord(paramData);
this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => {
this.notificationService.showSuccess(`Country message updated successfully`);
this.getCountries();
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to update country message`);
this.notificationService.showError(errorMessage);
console.error('Error saving country message:', error);
}
});
}
inActivateParamRecord(): void {
if (this.currentCountryId === 0) {
this.notificationService.showError('Not a valid country to inactivate');
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '500px',
data: {
title: 'Inactivate Country Message',
message: 'Are you sure you want to inactivate this message?',
confirmText: 'Yes',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.paramService.inActivateParamRecord(this.currentCountryId).subscribe(
{
next: (data: any) => {
this.notificationService.showSuccess(`Country message inactivated successfully`);
this.getCountries();
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to inactivate country message`);
this.notificationService.showError(errorMessage);
console.error('Error inactivating country message:', error);
}
}
);
}
});
}
reActivateParamRecord(): void {
if (this.currentCountryId === 0) {
this.notificationService.showError('Not a valid country to reactivate');
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '500px',
data: {
title: 'Reactivate Country Message',
message: 'Are you sure you want to reactivate this message?',
confirmText: 'Yes',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.paramService.reActivateParamRecord(this.currentCountryId).subscribe(
{
next: (data: any) => {
this.notificationService.showSuccess(`Country message reactivated successfully`)
this.getCountries();
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to reactivate country message`);
this.notificationService.showError(errorMessage);
console.error('Error reactivating country message:', error);
}
}
);
}
});
}
joinCountriesWithMessages(countries: ParamProperties[], countryMessages: ParamProperties[]): CountryMessage[] {
// Group messages by paramvalue
const messagesByCountry = countryMessages.reduce((acc, message) => {
if (!acc[message.paramValue]) {
acc[message.paramValue] = {
messages: [],
// paramId: message.paramId, // Store one paramid from messages
isInactive: message.inactiveCodeFlag === 'Y',
addlParamValue1: message.addlParamValue1,
addlParamValue2: message.addlParamValue2
};
}
acc[message.paramValue].messages.push(message.paramDesc);
return acc;
}, {} as Record<string, {
messages: string[]; //paramId?: number;
isInactive: boolean;
addlParamValue1?: string | null;
addlParamValue2?: string | null;
}>);
// Join countries with their messages
return countries.map(country => {
const countryMessages = messagesByCountry[country.paramValue] || {
messages: [], //paramId: null,
isInactive: false,
addlParamValue1: null,
addlParamValue2: null
};
return {
...country,
countryMessage: countryMessages.messages.join(' '),
isInactive: countryMessages.isInactive,
addlParamValue1: countryMessages.addlParamValue1,
addlParamValue2: countryMessages.addlParamValue2,
paramType: '014',
};
});
}
}

View File

@ -11,15 +11,12 @@ import { ActivatedRoute } from '@angular/router';
}) })
export class ManageParamRecordComponent { export class ManageParamRecordComponent {
table_mode: TABLE_MODE = 'PARAM-RECORD' table_mode: TABLE_MODE = 'PARAM-RECORD'
paramHeading: string = ''; paramHeading: string = 'Parameters';
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
ngOnInit() { ngOnInit() {
const param = this.route.snapshot.queryParamMap.get('param'); const param = this.route.snapshot.queryParamMap.get('param');
this.paramHeading = param || ''; this.paramHeading = param || '';
} }
} }

View File

@ -1 +1 @@
<app-param-table [table_mode]="table_mode"></app-param-table> <app-param-table [table_mode]="table_mode" [paramHeading]="paramHeading"></app-param-table>

View File

@ -9,5 +9,6 @@ import { TABLE_MODE } from '../../core/models/param/types';
styleUrl: './manage-table-record.component.scss' styleUrl: './manage-table-record.component.scss'
}) })
export class ManageTableRecordComponent { export class ManageTableRecordComponent {
table_mode: TABLE_MODE = 'TABLE-RECORD' table_mode: TABLE_MODE = 'TABLE-RECORD';
paramHeading: string = 'Table Records';
} }

View File

@ -1,10 +1,5 @@
<h2 class="page-header">Manage - {{paramHeading | properCase}}</h2> <h2 class="page-header">Manage - {{paramHeading | properCase}}</h2>
<div class="manage-container">
<div class="results-section">
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<div class="search-bar"> <div class="search-bar">
<mat-form-field appearance="outline" floatLabel="auto" class="full-width"> <mat-form-field appearance="outline" floatLabel="auto" class="full-width">
<mat-label>Search</mat-label> <mat-label>Search</mat-label>
@ -13,7 +8,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="actions-bar" [ngClass]="{ 'mb-40': !isParamRecord, 'mb-10': isParamRecord }"> <div class="actions-bar">
<mat-slide-toggle (change)="toggleShowInactiveData()" *ngIf="isParamRecord" [disabled]="paramData.length === 0"> <mat-slide-toggle (change)="toggleShowInactiveData()" *ngIf="isParamRecord" [disabled]="paramData.length === 0">
<div class="icon-text">Show Inactive Records</div> <div class="icon-text">Show Inactive Records</div>
</mat-slide-toggle> </mat-slide-toggle>
@ -24,52 +19,57 @@
</button> </button>
</div> </div>
<table mat-table [dataSource]="dataSource" class="results-table" matSort> <div class="results-section">
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<table mat-table [dataSource]="dataSource" matSort>
<!-- PARAMTYPE Column --> <!-- PARAMTYPE Column -->
<ng-container matColumnDef="ParamType"> <ng-container matColumnDef="paramType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
<td mat-cell *matCellDef="let client">{{ client.paramType }}</td> <td mat-cell *matCellDef="let client">{{ client.paramType }}</td>
</ng-container> </ng-container>
<!-- PARAMDESC Column --> <!-- PARAMDESC Column -->
<ng-container matColumnDef="ParamDesc"> <ng-container matColumnDef="paramDesc">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
<td mat-cell *matCellDef="let client">{{ client.paramDesc || '--'}}</td> <td mat-cell *matCellDef="let client">{{ client.paramDesc || '--'}}</td>
</ng-container> </ng-container>
<!-- PARAMVALUE Column --> <!-- PARAMVALUE Column -->
<ng-container matColumnDef="paramvalue"> <ng-container matColumnDef="paramValue">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th>
<td mat-cell *matCellDef="let client">{{ client.paramValue }}</td> <td mat-cell *matCellDef="let client">{{ client.paramValue }}</td>
</ng-container> </ng-container>
<!-- ADDLPARAMVALUE1 Column --> <!-- ADDLPARAMVALUE1 Column -->
<ng-container matColumnDef="addlparamvalue1"> <ng-container matColumnDef="addlParamValue1">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 1</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 1</th>
<td mat-cell *matCellDef="let client">{{ client.addlParamValue1 || '--' }}</td> <td mat-cell *matCellDef="let client">{{ client.addlParamValue1 || '--' }}</td>
</ng-container> </ng-container>
<!-- ADDLPARAMVALUE2 Column --> <!-- ADDLPARAMVALUE2 Column -->
<ng-container matColumnDef="addlparamvalue2"> <ng-container matColumnDef="addlParamValue2">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 2</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 2</th>
<td mat-cell *matCellDef="let client">{{ client.addlParamValue2 || '--'}}</td> <td mat-cell *matCellDef="let client">{{ client.addlParamValue2 || '--'}}</td>
</ng-container> </ng-container>
<!-- ADDLPARAMVALUE3 Column --> <!-- ADDLPARAMVALUE3 Column -->
<ng-container matColumnDef="addlparamvalue3"> <ng-container matColumnDef="addlParamValue3">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 3</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 3</th>
<td mat-cell *matCellDef="let client">{{ client.addlParamValue3 || '--' }}</td> <td mat-cell *matCellDef="let client">{{ client.addlParamValue3 || '--' }}</td>
</ng-container> </ng-container>
<!-- ADDLPARAMVALUE4 Column --> <!-- ADDLPARAMVALUE4 Column -->
<ng-container matColumnDef="addlparamvalue4"> <ng-container matColumnDef="addlParamValue4">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 4</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 4</th>
<td mat-cell *matCellDef="let client">{{ client.addlParamValue4 || '--' }}</td> <td mat-cell *matCellDef="let client">{{ client.addlParamValue4 || '--' }}</td>
</ng-container> </ng-container>
<!-- ADDLPARAMVALUE5 Column --> <!-- ADDLPARAMVALUE5 Column -->
<ng-container matColumnDef="addlparamvalue5"> <ng-container matColumnDef="addlParamValue5">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 5</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Additional Value 5</th>
<td mat-cell *matCellDef="let client">{{ client.addlParamValue5 || '--'}}</td> <td mat-cell *matCellDef="let client">{{ client.addlParamValue5 || '--'}}</td>
</ng-container> </ng-container>
@ -85,18 +85,19 @@
<mat-icon>chevron_right</mat-icon> <mat-icon>chevron_right</mat-icon>
</button> </button>
<button mat-icon-button color="primary" *ngIf="isParamRecord" (click)="onEditParam(client)" <button mat-icon-button color="primary" *ngIf="isParamRecord
[matTooltip]="'edit'"> && allowEdit" (click)="onEditParam(client)" [matTooltip]="'edit'">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button color="warn" <button mat-icon-button color="warn"
*ngIf="isParamRecord && (client.inactiveCodeFlag === 'N' || !client.inactiveCodeFlag)" *ngIf="isParamRecord && (client.inactiveCodeFlag === 'N' || !client.inactiveCodeFlag) && allowEdit"
matTooltip="Inactivate" (click)="inActivateParamRecord(client.paramId)"> matTooltip="Inactivate" (click)="inActivateParamRecord(client.paramId)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<button mat-icon-button color="warn" *ngIf="(isParamRecord) && client.inactiveCodeFlag === 'Y'" <button mat-icon-button color="warn"
*ngIf="(isParamRecord) && client.inactiveCodeFlag === 'Y' && allowEdit"
matTooltip="Reactivate" (click)="reActivateParamRecord(client.paramId)"> matTooltip="Reactivate" (click)="reActivateParamRecord(client.paramId)">
<mat-icon>delete_outline</mat-icon> <mat-icon>delete_outline</mat-icon>
</button> </button>
@ -117,12 +118,13 @@
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize || 2]" <mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize || 2]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Contact Form --> <!-- Contact Form -->
<div class="form-container" *ngIf="showForm"> <div class="form-container" *ngIf="showForm">
<form [formGroup]="paramForm" (ngSubmit)="saveRecord()"> <form [formGroup]="paramForm" (ngSubmit)="saveRecord()">
<div class="form-header"> <div class="form-header">
<h3>{{ !isParamRecord ? 'Add New Table' : isEditing ? 'Edit Param' : 'Add New Param' }} <h3> {{ !isParamRecord ? 'Add New Table' : isEditing ? `Edit Parameter` :
`Add New Parameter`}}
</h3> </h3>
</div> </div>
@ -253,7 +255,8 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-raised-button color="primary" type="submit" [disabled]="paramForm.invalid"> <button mat-raised-button color="primary" type="submit"
[disabled]="paramForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-button type="button" (click)="cancelEdit()">Cancel</button>

View File

@ -4,17 +4,15 @@
font-weight: 500; font-weight: 500;
} }
.manage-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
.actions-bar { .actions-bar {
clear: both; clear: both;
margin-bottom: -16px;
&.mb-40 {
margin-bottom: 40px;
}
&.mb-10 {
margin-bottom: 10px;
}
mat-slide-toggle { mat-slide-toggle {
transform: scale(0.8); transform: scale(0.8);
@ -27,7 +25,6 @@
.icon-text { .icon-text {
padding-left: 10px !important; padding-left: 10px !important;
font-size: 18px !important;
} }
} }
@ -55,6 +52,7 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
@ -98,7 +96,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }
@ -107,7 +105,6 @@
} }
} }
.results-section { .results-section {
position: relative; position: relative;
overflow: auto; overflow: auto;
@ -155,7 +152,7 @@
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
mat-icon { mat-icon {
font-size: 1rem; font-size: 0.875rem;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
margin-bottom: -3px; margin-bottom: -3px;

View File

@ -2,7 +2,7 @@ import { Component, inject, Input, ViewChild } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service'; import { UserPreferencesService } from '../../core/services/user-preference.service';
@ -15,27 +15,32 @@ import { ParamProperties } from '../../core/models/param/parameters';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { TABLE_MODE } from '../../core/models/param/types'; import { TABLE_MODE } from '../../core/models/param/types';
import { ProperCasePipe } from '../../shared/pipes/properCase.pipe'; import { ProperCasePipe } from '../../shared/pipes/proper-case.pipe';
import { finalize } from 'rxjs';
import { CustomPaginator } from '../../shared/custom-paginator';
@Component({ @Component({
selector: 'app-param-table', selector: 'app-param-table',
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule, MatSlideToggleModule, ProperCasePipe], imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule, MatSlideToggleModule, ProperCasePipe],
templateUrl: './param-table.component.html', templateUrl: './param-table.component.html',
styleUrl: './param-table.component.scss' styleUrl: './param-table.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class ParamTableComponent { export class ParamTableComponent {
@ViewChild(MatPaginator) paginator!: MatPaginator; //@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort; //@ViewChild(MatSort) sort!: MatSort;
@Input() table_mode: TABLE_MODE = 'TABLE-RECORD'; @Input() table_mode: TABLE_MODE = 'TABLE-RECORD';
@Input() paramHeading: string = 'Table Records'; @Input() paramHeading: string = 'Table Records';
isLoading: boolean = false; isLoading: boolean = false;
changeInProgress: boolean = false;
userPreferences: UserPreferences; userPreferences: UserPreferences;
paramType: string | null = null; paramType: string | null = null;
paramData: any = []; paramData: any = [];
showInactiveData: boolean = false; showInactiveData: boolean = false;
isEditing: boolean = false; isEditing: boolean = false;
allowEdit: boolean = true;
showForm: boolean = false; showForm: boolean = false;
currentParamid: number | null = null; currentParamid: number | null = null;
@ -51,16 +56,41 @@ export class ParamTableComponent {
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
dataSource = new MatTableDataSource<any>([]); dataSource = new MatTableDataSource<any>([]);
@ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort, { static: false })
set sort(value: MatSort) {
if (value) {
// 1. Assign the sort object to the data source immediately
this.dataSource.sort = value;
// 2. Apply default settings inside a timeout to avoid the NG0100 error
setTimeout(() => {
if (this.table_mode === 'TABLE-RECORD' && !value.active) {
// Only set default if a sort isn't already active
value.active = 'paramValue';
value.direction = 'asc';
// 3. Instead of emitting an event, manually trigger the sort logic
// through the DataSource's internal sortingDataAccessor
this.dataSource.sort = value;
}
});
}
}
paramForm: FormGroup; paramForm: FormGroup;
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource.data = this.paramData; this.dataSource.data = this.paramData;
} }
displayedColumns: string[] = ['ParamType', 'ParamDesc', 'paramvalue', 'addlparamvalue1', 'addlparamvalue2', 'addlparamvalue3', displayedColumns: string[] = ['paramType', 'paramDesc', 'paramValue', 'addlParamValue1', 'addlParamValue2', 'addlParamValue3',
'addlparamvalue4', 'addlparamvalue5', 'actions']; 'addlParamValue4', 'addlParamValue5', 'actions'];
formControls: any = {}; formControls: any = {};
@ -114,6 +144,7 @@ export class ParamTableComponent {
ngOnInit(): void { ngOnInit(): void {
this.paramType = this.route.snapshot.paramMap.get('id'); this.paramType = this.route.snapshot.paramMap.get('id');
this.allowEdit = this.paramType !== '014';
this.getParameters(); this.getParameters();
} }
@ -180,17 +211,17 @@ export class ParamTableComponent {
const P_PARAMTYPE: string = this.paramType ? this.paramType : '000'; const P_PARAMTYPE: string = this.paramType ? this.paramType : '000';
this.paramService.getParameters(P_PARAMTYPE).subscribe({ this.paramService.getParameters(P_PARAMTYPE).pipe(finalize(() => {
next: (paramData: ParamProperties[]) => {
this.paramData = paramData
this.renderParamData()
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (paramData: ParamProperties[]) => {
this.paramData = paramData;
this.renderParamData();
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to search preparers'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load parameters');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false; console.error('Error loading parameters:', error);
console.error('Error loading preparers:', error);
} }
}); });
} }
@ -213,7 +244,10 @@ export class ParamTableComponent {
? this.paramService.updateParamRecord(paramData as ParamProperties) ? this.paramService.updateParamRecord(paramData as ParamProperties)
: this.paramService.createParamRecord(paramData as ParamProperties); : this.paramService.createParamRecord(paramData as ParamProperties);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Record ${this.isEditing && (this.table_mode !== 'TABLE-RECORD') ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Record ${this.isEditing && (this.table_mode !== 'TABLE-RECORD') ? 'updated' : 'added'} successfully`);
this.getParameters(); this.getParameters();

View File

@ -0,0 +1,113 @@
<div class="region-container">
<h2 class="page-header">Manage Regions</h2>
<div class="actions-bar">
<!-- <mat-slide-toggle (change)="toggleShowExpiredRecords()">
Show Expired Records
</mat-slide-toggle> -->
<button mat-raised-button color="primary" (click)="addNewRegion()">
<mat-icon>add</mat-icon> Add New Region
</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>
<!-- Value Column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Region</th>
<td mat-cell *matCellDef="let item">{{ item.value }}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Region Name</th>
<td mat-cell *matCellDef="let item">{{ item.name }}</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)="editRegion(item)" 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 regions available</span>
</td>
</tr>
</table>
<mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Region Form -->
<div class="form-container" *ngIf="showForm">
<form [formGroup]="regionForm" (ngSubmit)="saveRegion()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Region' : 'Add New Region' }}</h3>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Region</mat-label>
<input matInput formControlName="value" required [readonly]="isEditing">
<mat-error *ngIf="regionForm.get('value')?.errors?.['required']">
Region is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Region Name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="regionForm.get('name')?.errors?.['required']">
Region Name is required
</mat-error>
</mat-form-field>
</div>
<!-- <div *ngIf="isEditing" class="readonly-section">
<div class="readonly-fields">
<div class="field-column"> -->
<!-- Last Changed By -->
<!-- <div class="readonly-field">
<label>Last Changed By</label>
<div class="readonly-value">
{{readOnlyFields.lastChangedBy || 'N/A'}}
</div>
</div>
</div>
<div class="field-column"> -->
<!-- Last Changed Date -->
<!-- <div class="readonly-field">
<label>Last Changed Date</label>
<div class="readonly-value">
{{(readOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
</div>
</div>
</div>
</div>
</div> -->
<div class="form-actions">
<button mat-raised-button color="primary" type="submit"
[disabled]="regionForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }}
</button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,165 @@
.region-container {
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 24px;
.page-header {
margin: 0px;
color: var(--mat-sys-primary);
font-weight: 500;
}
.actions-bar {
clear: both;
margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button {
float: right;
}
}
.table-container {
position: relative;
overflow: auto;
border-radius: 8px;
.loading-shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.7);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
mat-table {
width: 100%;
mat-icon {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
.mat-column-actions {
width: 120px;
text-align: center;
}
}
.no-data-message {
text-align: center;
padding: 0.9rem;
color: rgba(0, 0, 0, 0.54);
mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
margin-bottom: -3px;
}
}
mat-paginator {
border-top: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 0 0 8px 8px;
padding-top: 4px;
}
}
.form-container {
background-color: white;
padding: 24px;
border-radius: 8px;
margin-top: 16px;
.form-header {
h3 {
margin: 0;
color: var(--mat-sys-primary);
font-weight: 500;
}
}
form {
display: flex;
flex-direction: column;
gap: 16px;
.form-row {
display: flex;
gap: 16px;
align-items: start;
mat-form-field {
flex: 1;
}
}
.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.875rem;
display: flex;
align-items: center;
}
}
}
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.region-container {
padding: 16px;
.form-row {
flex-direction: column;
gap: 16px !important;
}
}
}

View File

@ -0,0 +1,179 @@
import { AfterViewInit, Component, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { Region } from '../core/models/region/region';
import { CustomPaginator } from '../shared/custom-paginator';
import { UserPreferences } from '../core/models/user-preference';
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
import { NotificationService } from '../core/services/common/notification.service';
import { RegionService } from '../core/services/region/region.service';
import { UserPreferencesService } from '../core/services/user-preference.service';
import { finalize } from 'rxjs';
@Component({
selector: 'app-region',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './region.component.html',
styleUrl: './region.component.scss',
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
})
export class RegionComponent implements OnInit, AfterViewInit {
@ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['value', 'name', 'actions'];
dataSource = new MatTableDataSource<any>();
regionForm: FormGroup;
isEditing = false;
currentRegionId: number | null = null;
isLoading = false;
changeInProgress = false;
showForm = false;
showExpiredRecords = false;
regions: Region[] = [];
userPreferences!: UserPreferences;
// readOnlyFields: any = {
// lastChangedDate: null,
// lastChangedBy: null
// };
@Input() isEditMode = false;
@Input() spid: number = 0;
@Output() hasRegions = new EventEmitter<boolean>();
private fb = inject(FormBuilder);
private regionService = inject(RegionService);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
this.regionForm = this.fb.group({
value: ['', Validators.required],
name: ['', Validators.required]
});
}
ngOnInit(): void {
this.loadRegions();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadRegions(): void {
this.isLoading = true;
this.regionService.getRegions(this.spid).pipe(finalize(() => {
this.isLoading = false;
}))
.subscribe({
next: (regions: Region[]) => {
this.regions = regions;
this.dataSource.data = this.regions;
// this.renderRecords();
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load regions');
this.notificationService.showError(errorMessage);
console.error('Error loading regions:', error);
}
});
}
// toggleShowExpiredRecords(): void {
// this.showExpiredRecords = !this.showExpiredRecords;
// this.renderRecords();
// }
// renderRecords(): void {
// // if (this.showExpiredRecords) {
// // this.dataSource.data = this.regions;
// // } else {
// // this.dataSource.data = this.regions; // Adjust if you have expired logic
// //}
// }
addNewRegion(): void {
this.showForm = true;
this.isEditing = false;
this.currentRegionId = null;
this.regionForm.reset({
value: '',
name: ''
});
this.regionForm.get('value')?.enable();
this.regionForm.get('name')?.enable();
}
editRegion(region: Region): void {
this.showForm = true;
this.isEditing = true;
this.currentRegionId = region.id;
this.regionForm.patchValue({
value: region.value,
name: region.name
});
// // Set readonly fields if available
// this.readOnlyFields.lastChangedDate = null; // Update based on your API response
// this.readOnlyFields.lastChangedBy = null; // Update based on your API response
this.regionForm.get('value')?.disable();
}
saveRegion(): void {
if (this.regionForm.invalid) {
this.regionForm.markAllAsTouched();
return;
}
const regionData: Region = {
id: this.currentRegionId || 0,
value: this.regionForm.get('value')?.value,
name: this.regionForm.get('name')?.value
};
const saveObservable = this.isEditing && this.currentRegionId
? this.regionService.updateRegion(this.currentRegionId, regionData)
: this.regionService.createRegion(regionData, this.spid);
this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => {
this.notificationService.showSuccess(`Region ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadRegions();
this.cancelEdit();
this.hasRegions.emit(true);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} region`);
this.notificationService.showError(errorMessage);
console.error('Error saving region:', error);
}
});
}
cancelEdit(): void {
this.showForm = false;
this.isEditing = false;
this.currentRegionId = null;
this.regionForm.reset();
this.regionForm.get('value')?.enable();
this.regionForm.get('name')?.enable();
}
}

View File

@ -6,6 +6,7 @@ import { NotificationService } from '../core/services/common/notification.servic
import { AngularMaterialModule } from '../shared/module/angular-material.module'; import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ThemeService } from '../core/services/theme.service';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',
@ -28,6 +29,7 @@ export class RegisterComponent {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
private themeService = inject(ThemeService);
constructor() { constructor() {
this.registrationForm = this.fb.group({ this.registrationForm = this.fb.group({
@ -47,6 +49,10 @@ export class RegisterComponent {
// } // }
} }
ngOnInit(): void {
this.themeService.setTheme('default');
}
onSubmit(): void { onSubmit(): void {
if (this.registrationForm.invalid) { if (this.registrationForm.invalid) {
this.registrationForm.markAllAsTouched(); this.registrationForm.markAllAsTouched();

View File

@ -1,5 +1,5 @@
<div class="service-provider-container"> <div class="service-provider-container">
<mat-stepper orientation="vertical" [linear]="true" (selectionChange)="onStepChange($event)" <mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
[selectedIndex]="currentStep"> [selectedIndex]="currentStep">
<!-- Basic Details Step --> <!-- Basic Details Step -->
@ -19,8 +19,16 @@
</app-contacts> </app-contacts>
</mat-step> </mat-step>
<!-- Regions Step -->
<mat-step [completed]="regionCompleted" [editable]="!!serviceProviderId && basicDetailsCompleted">
<ng-template matStepLabel>Regions</ng-template>
<app-sp-region *ngIf="serviceProviderId" [spid]="serviceProviderId" (hasRegions)="onRegionsSaved($event)">
</app-sp-region>
</mat-step>
<!-- Carnet Sequence Step --> <!-- Carnet Sequence Step -->
<mat-step [editable]="!!serviceProviderId && contactsCompleted" [completed]="carnetSequenceCompleted"> <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted" [completed]="carnetSequenceCompleted">
<ng-template matStepLabel>Carnet Sequence</ng-template> <ng-template matStepLabel>Carnet Sequence</ng-template>
<app-carnet-sequence *ngIf="serviceProviderId" [spid]="serviceProviderId" <app-carnet-sequence *ngIf="serviceProviderId" [spid]="serviceProviderId"
@ -29,7 +37,7 @@
</mat-step> </mat-step>
<!-- Fees & Commission Step --> <!-- Fees & Commission Step -->
<mat-step [editable]="!!serviceProviderId && carnetSequenceCompleted" <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted"
[completed]="feeCommissionCompleted && carnetSequenceCompleted"> [completed]="feeCommissionCompleted && carnetSequenceCompleted">
<ng-template matStepLabel>Carnet Fee & Commission Setup</ng-template> <ng-template matStepLabel>Carnet Fee & Commission Setup</ng-template>
@ -38,7 +46,7 @@
</mat-step> </mat-step>
<!-- Basic Fee Step --> <!-- Basic Fee Step -->
<mat-step [editable]="!!serviceProviderId && feeCommissionCompleted" <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted"
[completed]="basicFeeCompleted && feeCommissionCompleted"> [completed]="basicFeeCompleted && feeCommissionCompleted">
<ng-template matStepLabel>Basic Fee Setup</ng-template> <ng-template matStepLabel>Basic Fee Setup</ng-template>
@ -47,7 +55,7 @@
</mat-step> </mat-step>
<!-- Counterfoil Fee Step --> <!-- Counterfoil Fee Step -->
<mat-step [editable]="!!serviceProviderId && basicFeeCompleted" [completed]="counterfoilFeeCompleted"> <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted" [completed]="counterfoilFeeCompleted">
<ng-template matStepLabel>Counterfoil Setup</ng-template> <ng-template matStepLabel>Counterfoil Setup</ng-template>
<app-counterfoil-fee *ngIf="serviceProviderId" [spid]="serviceProviderId" <app-counterfoil-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
@ -56,8 +64,7 @@
</mat-step> </mat-step>
<!-- Continuation Sheet Fee Step --> <!-- Continuation Sheet Fee Step -->
<mat-step [editable]="!!serviceProviderId && counterfoilFeeCompleted" <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted" [completed]="continuationSheetFeeCompleted">
[completed]="continuationSheetFeeCompleted">
<ng-template matStepLabel>Continuation Sheet Fee Setup</ng-template> <ng-template matStepLabel>Continuation Sheet Fee Setup</ng-template>
<app-continuation-sheet-fee *ngIf="serviceProviderId" [spid]="serviceProviderId" <app-continuation-sheet-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
@ -66,7 +73,7 @@
</mat-step> </mat-step>
<!-- Expedited Fee Step --> <!-- Expedited Fee Step -->
<mat-step [editable]="!!serviceProviderId && continuationSheetFeeCompleted" [completed]="expeditedFeeCompleted"> <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted" [completed]="expeditedFeeCompleted">
<ng-template matStepLabel>Expedited Fee Setup</ng-template> <ng-template matStepLabel>Expedited Fee Setup</ng-template>
<app-expedited-fee *ngIf="serviceProviderId" [spid]="serviceProviderId" <app-expedited-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
@ -75,7 +82,7 @@
</mat-step> </mat-step>
<!-- Security Deposit Step --> <!-- Security Deposit Step -->
<mat-step [editable]="!!serviceProviderId && expeditedFeeCompleted" [completed]="securityDepositCompleted"> <mat-step [editable]="!!serviceProviderId && basicDetailsCompleted" [completed]="securityDepositCompleted">
<ng-template matStepLabel>Security Deposit</ng-template> <ng-template matStepLabel>Security Deposit</ng-template>
<app-security-deposit *ngIf="serviceProviderId" [spid]="serviceProviderId" <app-security-deposit *ngIf="serviceProviderId" [spid]="serviceProviderId"
@ -83,5 +90,14 @@
[userPreferences]="userPreferences"></app-security-deposit> [userPreferences]="userPreferences"></app-security-deposit>
</mat-step> </mat-step>
<!-- Cargo Rate Step -->
<mat-step [editable]="!!serviceProviderId && basicDetailsCompleted" [completed]="cargoRateCompleted">
<ng-template matStepLabel>Cargo Rate Setup</ng-template>
<app-cargo-rate *ngIf="serviceProviderId" [spid]="serviceProviderId"
(hasCargoRates)="onCargoRateSaved($event)" [userPreferences]="userPreferences">
</app-cargo-rate>
</mat-step>
</mat-stepper> </mat-stepper>
</div> </div>

View File

@ -5,24 +5,27 @@ import { AngularMaterialModule } from '../../shared/module/angular-material.modu
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ContactsComponent } from '../contacts/contacts.component'; import { ContactsComponent } from '../contacts/contacts.component';
import { CarnetSequenceComponent } from '../carnet-sequence/carnet-sequence.component'; import { CarnetSequenceComponent } from '../carnet-sequence/carnet-sequence.component';
import { CarnetFeeComponent } from '../carnet-fee/carnet-fee.component'; import { CarnetFeeComponent } from '../../fees/carnet-fee/carnet-fee.component';
import { BasicFeeComponent } from '../basic-fee/basic-fee.component'; import { BasicFeeComponent } from '../../fees/basic-fee/basic-fee.component';
import { CounterfoilFeeComponent } from '../counterfoil-fee/counterfoil-fee.component'; import { CounterfoilFeeComponent } from '../../fees/counterfoil-fee/counterfoil-fee.component';
import { ExpeditedFeeComponent } from '../expedited-fee/expedited-fee.component'; import { ExpeditedFeeComponent } from '../../fees/expedited-fee/expedited-fee.component';
import { SecurityDepositComponent } from '../security-deposit/security-deposit.component'; import { SecurityDepositComponent } from '../../fees/security-deposit/security-deposit.component';
import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component"; import { ContinuationSheetFeeComponent } from "../../fees/continuation-sheet-fee/continuation-sheet-fee.component";
import { UserPreferences } from '../../core/models/user-preference'; import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service'; import { UserPreferencesService } from '../../core/services/user-preference.service';
import { CargoRateComponent } from '../../fees/cargo-rate/cargo-rate.component';
import { RegionComponent } from '../region/region.component';
@Component({ @Component({
selector: 'app-add-service-provider', selector: 'app-add-service-provider',
imports: [BasicDetailsComponent, AngularMaterialModule, CommonModule, ContactsComponent, CarnetSequenceComponent, imports: [BasicDetailsComponent, AngularMaterialModule, CommonModule, ContactsComponent, CarnetSequenceComponent,
CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, ContinuationSheetFeeComponent], CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, ContinuationSheetFeeComponent, CargoRateComponent, RegionComponent],
templateUrl: './add-service-provider.component.html', templateUrl: './add-service-provider.component.html',
styleUrl: './add-service-provider.component.scss' styleUrl: './add-service-provider.component.scss'
}) })
export class AddServiceProviderComponent { export class AddServiceProviderComponent {
isEditMode = false; isEditMode = false;
isLinear = true;
serviceProviderId: number | null = null; serviceProviderId: number | null = null;
currentStep: number = 0; currentStep: number = 0;
isLoading: boolean = false; isLoading: boolean = false;
@ -37,6 +40,8 @@ export class AddServiceProviderComponent {
continuationSheetFeeCompleted: boolean = false; continuationSheetFeeCompleted: boolean = false;
expeditedFeeCompleted: boolean = false; expeditedFeeCompleted: boolean = false;
securityDepositCompleted: boolean = false; securityDepositCompleted: boolean = false;
cargoRateCompleted: boolean = false;
regionCompleted: boolean = false;
constructor(userPrefenceService: UserPreferencesService) { constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences(); this.userPreferences = userPrefenceService.getPreferences();
@ -45,6 +50,7 @@ export class AddServiceProviderComponent {
onBasicDetailsSaved(event: string): void { onBasicDetailsSaved(event: string): void {
this.serviceProviderId = +event; this.serviceProviderId = +event;
this.basicDetailsCompleted = true; this.basicDetailsCompleted = true;
this.isLinear = false; // Disable linear mode after basic detail is created
} }
onContactsSaved(event: boolean): void { onContactsSaved(event: boolean): void {
@ -79,6 +85,14 @@ export class AddServiceProviderComponent {
this.securityDepositCompleted = event; this.securityDepositCompleted = event;
} }
onCargoRateSaved(event: boolean): void {
this.cargoRateCompleted = event;
}
onRegionsSaved(event: boolean): void {
this.regionCompleted = event;
}
onStepChange(event: StepperSelectionEvent): void { onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex; this.currentStep = event.selectedIndex;
} }

View File

@ -139,45 +139,37 @@
<div class="form-row"> <div class="form-row">
<mat-form-field appearance="outline" class="cargo-surety"> <mat-form-field appearance="outline" class="cargo-surety">
<mat-label>Cargo Surety</mat-label> <mat-label>Cargo Surety</mat-label>
<mat-select formControlName="cargoSurety" required> <mat-select formControlName="cargoSurety">
<mat-option *ngFor="let cargoSurety of cargoSuretys" [value]="cargoSurety.value"> <mat-option *ngFor="let cargoSurety of cargoSuretys" [value]="cargoSurety.value">
{{ cargoSurety.name }} {{ cargoSurety.name }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="f['cargoSurety'].errors?.['required']">
Cargo Surety is required
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="cargo-policy"> <mat-form-field appearance="outline" class="cargo-policy">
<mat-label>Cargo Policy</mat-label> <mat-label>Cargo Policy</mat-label>
<mat-select formControlName="cargoPolicyNo" required> <mat-select formControlName="cargoPolicyNo">
<mat-option *ngFor="let cargoPolicy of cargoPolicies" [value]="cargoPolicy.value"> <mat-option *ngFor="let cargoPolicy of cargoPolicies" [value]="cargoPolicy.value">
{{ cargoPolicy.name }} {{ cargoPolicy.name }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="f['cargoPolicyNo'].errors?.['required']">
Cargo Policy is required
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="bond-surety"> <mat-form-field appearance="outline" class="bond-surety">
<mat-label>Bond Surety</mat-label> <mat-label>Bond Surety</mat-label>
<mat-select formControlName="bondSurety" required> <mat-select formControlName="bondSurety">
<mat-option *ngFor="let bondSurety of bondSuretys" [value]="bondSurety.value"> <mat-option *ngFor="let bondSurety of bondSuretys" [value]="bondSurety.value">
{{ bondSurety.name }} {{ bondSurety.name }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="f['bondSurety'].errors?.['required']">
Bond Surety is required
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-raised-button color="primary" type="submit" [disabled]="basicDetailsForm.invalid"> <button mat-raised-button color="primary" type="submit"
Save [disabled]="basicDetailsForm.invalid || changeInProgress">
{{ isEditMode ? 'Update' : 'Save' }}
</button> </button>
</div> </div>
</form> </form>

View File

@ -1,7 +1,7 @@
import { Component, Input, OnInit, Output, EventEmitter, OnDestroy, inject } from '@angular/core'; import { Component, Input, OnInit, Output, EventEmitter, OnDestroy, inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { finalize, takeUntil } from 'rxjs/operators';
import { BasicDetail } from '../../core/models/service-provider/basic-detail'; import { BasicDetail } from '../../core/models/service-provider/basic-detail';
import { Country } from '../../core/models/country'; import { Country } from '../../core/models/country';
import { Region } from '../../core/models/region'; import { Region } from '../../core/models/region';
@ -38,6 +38,7 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
cargoPolicies: CargoPolicy[] = []; cargoPolicies: CargoPolicy[] = [];
isLoading = true; isLoading = true;
changeInProgress = false;
countriesHasStates = ['US', 'CA', 'MX']; countriesHasStates = ['US', 'CA', 'MX'];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -57,21 +58,24 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
// this.spidCreated.emit(this.spid?.toString()); // this.spidCreated.emit(this.spid?.toString());
// Patch edit form data // Patch edit form data
if (this.spid > 0) { if (this.spid > 0) {
this.basicDetailService.getBasicDetailsById(this.spid).subscribe({ this.isLoading = true;
this.basicDetailService.getBasicDetailsById(this.spid).pipe(finalize(() => {
this.isLoading = false;
})).subscribe({
next: (basicDetail: BasicDetail) => { next: (basicDetail: BasicDetail) => {
if (basicDetail?.spid > 0) { if (basicDetail?.spid > 0) {
this.patchFormData(basicDetail); this.patchFormData(basicDetail);
this.serviceProviderName.emit(basicDetail.companyName); this.serviceProviderName.emit(basicDetail.companyName);
} }
this.isLoading = false;
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic details'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic details');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading basic details:', error); console.error('Error loading basic details:', error);
} }
}); });
} else {
this.loadStates('US');
} }
} }
@ -87,19 +91,19 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
address1: ['', [Validators.required, Validators.maxLength(100)]], address1: ['', [Validators.required, Validators.maxLength(100)]],
address2: ['', [Validators.maxLength(100)]], address2: ['', [Validators.maxLength(100)]],
city: ['', [Validators.required, Validators.maxLength(50)]], city: ['', [Validators.required, Validators.maxLength(50)]],
country: ['', Validators.required], country: ['US', Validators.required],
state: ['', Validators.required], state: ['', Validators.required],
zip: ['', [Validators.required, ZipCodeValidator('country')]], zip: ['', [Validators.required, ZipCodeValidator('country')]],
issuingRegion: ['', Validators.required], issuingRegion: ['', Validators.required],
replacementRegion: ['', Validators.required], replacementRegion: ['', Validators.required],
cargoSurety: ['', Validators.required], cargoSurety: [''],
cargoPolicyNo: ['', Validators.required], cargoPolicyNo: [''],
bondSurety: ['', Validators.required] bondSurety: ['']
}); });
} }
loadLookupData(): void { loadLookupData(): void {
this.commonService.getCountries(this.spid) this.commonService.getCountries()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (countries) => { next: (countries) => {
@ -107,7 +111,6 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
}, },
error: (error) => { error: (error) => {
console.error('Failed to load countries', error); console.error('Failed to load countries', error);
this.isLoading = false;
} }
}); });
@ -123,11 +126,9 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (regions) => { next: (regions) => {
this.regions = regions; this.regions = regions;
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load regions', error); console.error('Failed to load regions', error);
this.isLoading = false;
} }
}); });
} }
@ -135,9 +136,10 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
loadStates(country: string): void { loadStates(country: string): void {
this.isLoading = true; this.isLoading = true;
country = this.countriesHasStates.includes(country) ? country : 'FN'; country = this.countriesHasStates.includes(country) ? country : 'FN';
this.commonService.getStates(country, this.spid) this.commonService.getStates(country)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$), finalize(() => {
.subscribe({ this.isLoading = false;
})).subscribe({
next: (states) => { next: (states) => {
this.states = states; this.states = states;
const stateControl = this.basicDetailsForm.get('state'); const stateControl = this.basicDetailsForm.get('state');
@ -147,11 +149,9 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
stateControl?.disable(); stateControl?.disable();
stateControl?.setValue('FN'); stateControl?.setValue('FN');
} }
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load states', error); console.error('Failed to load states', error);
this.isLoading = false;
} }
}); });
} }
@ -162,11 +162,9 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (bondSuretys) => { next: (bondSuretys) => {
this.bondSuretys = bondSuretys; this.bondSuretys = bondSuretys;
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load bond suretys', error); console.error('Failed to load bond suretys', error);
this.isLoading = false;
} }
}); });
} }
@ -177,11 +175,9 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (cargoSuretys) => { next: (cargoSuretys) => {
this.cargoSuretys = cargoSuretys; this.cargoSuretys = cargoSuretys;
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load cargo suretys', error); console.error('Failed to load cargo suretys', error);
this.isLoading = false;
} }
}); });
} }
@ -192,11 +188,9 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: (cargoPolicies) => { next: (cargoPolicies) => {
this.cargoPolicies = cargoPolicies; this.cargoPolicies = cargoPolicies;
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load cargo policies', error); console.error('Failed to load cargo policies', error);
this.isLoading = false;
} }
}); });
} }
@ -220,6 +214,8 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
if (data.country) { if (data.country) {
this.loadStates(data.country); this.loadStates(data.country);
} else {
this.loadStates('US');
} }
if (this.isEditMode) { if (this.isEditMode) {
@ -270,12 +266,17 @@ export class BasicDetailsComponent implements OnInit, OnDestroy {
? this.basicDetailService.updateBasicDetails(this.spid, basicDetailData) ? this.basicDetailService.updateBasicDetails(this.spid, basicDetailData)
: this.basicDetailService.createBasicDetails(basicDetailData); : this.basicDetailService.createBasicDetails(basicDetailData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => this.changeInProgress = false)).subscribe({
next: (basicData: any) => { next: (basicData: any) => {
this.notificationService.showSuccess(`Basic details ${this.isEditMode ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Basic details ${this.isEditMode ? 'updated' : 'added'} successfully`);
if (!this.isEditMode) { if (!this.isEditMode) {
this.spidCreated.emit(basicData.SPID); this.spidCreated.emit(basicData.SPID);
// change to edit mode after creation
this.isEditMode = true;
this.spid = basicData.SPID;
} }
}, },
error: (error) => { error: (error) => {

View File

@ -1,6 +1,6 @@
<div class="carnet-sequence-container"> <div class="carnet-sequence-container">
<div class="actions-bar"> <div class="actions-bar">
<button mat-raised-button color="primary" *ngIf="!isEditMode" (click)="addNewSequence()"> <button mat-raised-button color="primary" (click)="addNewSequence()">
<mat-icon>add</mat-icon> Add New Sequence <mat-icon>add</mat-icon> Add New Sequence
</button> </button>
</div> </div>
@ -65,8 +65,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Sequence Form --> <!-- Sequence Form -->
@ -138,10 +138,11 @@
</mat-error> </mat-error>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="sequenceForm.invalid"> [disabled]="sequenceForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.carnet-sequence-container { .carnet-sequence-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -99,6 +99,7 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core'; import { Component, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { CarnetSequence } from '../../core/models/service-provider/carnet-sequence'; import { CarnetSequence } from '../../core/models/service-provider/carnet-sequence';
import { Subject, takeUntil } from 'rxjs'; import { finalize, Subject, takeUntil } from 'rxjs';
import { Region } from '../../core/models/region'; import { Region } from '../../core/models/region';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
@ -23,7 +23,11 @@ import { CarnetSequenceService } from '../../core/services/service-provider/carn
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class CarnetSequenceComponent implements OnInit { export class CarnetSequenceComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['carnetType', 'region', 'startNumber', 'endNumber', 'lastNumber']; displayedColumns: string[] = ['carnetType', 'region', 'startNumber', 'endNumber', 'lastNumber'];
@ -32,6 +36,7 @@ export class CarnetSequenceComponent implements OnInit {
isEditing = false; isEditing = false;
currentSequenceId: string | null = null; currentSequenceId: string | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
carnetTypes = [ carnetTypes = [
@ -98,11 +103,9 @@ export class CarnetSequenceComponent implements OnInit {
.subscribe({ .subscribe({
next: (regions) => { next: (regions) => {
this.regions = regions; this.regions = regions;
this.isLoading = false;
}, },
error: (error) => { error: (error) => {
console.error('Failed to load regions', error); console.error('Failed to load regions', error);
this.isLoading = false;
} }
}); });
} }
@ -113,17 +116,15 @@ export class CarnetSequenceComponent implements OnInit {
this.isLoading = true; this.isLoading = true;
this.carnetSequenceService.getCarnetSequenceById(this.spid).subscribe({ this.carnetSequenceService.getCarnetSequenceById(this.spid).pipe(finalize(() => {
this.isLoading = false;
})).subscribe({
next: (carnetSequences: CarnetSequence[]) => { next: (carnetSequences: CarnetSequence[]) => {
// this.sequences = carnetSequences;
this.isLoading = false;
this.dataSource.data = carnetSequences; this.dataSource.data = carnetSequences;
this.isLoading = false;
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load sequences'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load sequences');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading sequences:', error); console.error('Error loading sequences:', error);
} }
}); });
@ -141,7 +142,10 @@ export class CarnetSequenceComponent implements OnInit {
lastNumber: this.sequenceForm.value.startNumber lastNumber: this.sequenceForm.value.startNumber
}; };
this.carnetSequenceService.createCarnetSequence(sequenceData).subscribe({ this.changeInProgress = true;
this.carnetSequenceService.createCarnetSequence(sequenceData).pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess('Sequence added successfully'); this.notificationService.showSuccess('Sequence added successfully');
this.loadSequences(); this.loadSequences();
@ -182,7 +186,10 @@ export class CarnetSequenceComponent implements OnInit {
spid: this.spid spid: this.spid
}; };
this.carnetSequenceService.createCarnetSequence(sequenceData) this.changeInProgress = true;
this.carnetSequenceService.createCarnetSequence(sequenceData).pipe(finalize(() => {
this.changeInProgress = false;
}))
.subscribe({ .subscribe({
next: () => { next: () => {
this.notificationService.showSuccess('Sequence added successfully'); this.notificationService.showSuccess('Sequence added successfully');

View File

@ -4,6 +4,10 @@
<mat-icon matPrefix>search</mat-icon> <mat-icon matPrefix>search</mat-icon>
<input matInput (keyup)="applyFilter($event)" placeholder="Search contacts..."> <input matInput (keyup)="applyFilter($event)" placeholder="Search contacts...">
</mat-form-field> --> </mat-form-field> -->
<mat-slide-toggle (change)="toggleShowInactiveContacts()">
Show Inactive Contacts
</mat-slide-toggle>
<button mat-raised-button color="primary" (click)="addNewContact()"> <button mat-raised-button color="primary" (click)="addNewContact()">
<mat-icon>add</mat-icon> Add New Contact <mat-icon>add</mat-icon> Add New Contact
</button> </button>
@ -21,6 +25,12 @@
<td mat-cell *matCellDef="let contact">{{ contact.firstName }}</td> <td mat-cell *matCellDef="let contact">{{ contact.firstName }}</td>
</ng-container> </ng-container>
<!-- Middle Initial Column -->
<ng-container matColumnDef="middleInitial">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Middle Initial</th>
<td mat-cell *matCellDef="let contact">{{ contact.middleInitial }}</td>
</ng-container>
<!-- Last Name Column --> <!-- Last Name Column -->
<ng-container matColumnDef="lastName"> <ng-container matColumnDef="lastName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>
@ -30,7 +40,7 @@
<!-- Title Column --> <!-- Title Column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Title</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Title</th>
<td mat-cell *matCellDef="let contact">{{ contact.title }}</td> <td mat-cell *matCellDef="let contact">{{ getContactTitleLabel(contact.title) }}</td>
</ng-container> </ng-container>
<!-- Phone Column --> <!-- Phone Column -->
@ -39,6 +49,18 @@
<td mat-cell *matCellDef="let contact">{{ contact.phone | phone }}</td> <td mat-cell *matCellDef="let contact">{{ contact.phone | phone }}</td>
</ng-container> </ng-container>
<!-- Mobile Column -->
<ng-container matColumnDef="mobile">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Mobile</th>
<td mat-cell *matCellDef="let contact">{{ contact.mobile }}</td>
</ng-container>
<!-- Fax Column -->
<ng-container matColumnDef="fax">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Fax</th>
<td mat-cell *matCellDef="let contact">{{ contact.fax }}</td>
</ng-container>
<!-- Email Column --> <!-- Email Column -->
<ng-container matColumnDef="email"> <ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
@ -62,15 +84,21 @@
<button mat-icon-button color="primary" (click)="editContact(contact)" matTooltip="Edit"> <button mat-icon-button color="primary" (click)="editContact(contact)" matTooltip="Edit">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button color="primary" *ngIf="!contact.isInactive || !contact.hasLogin"
[hidden]="contact.isInactive || contact.hasLogin" (click)="createLogin(contact.email)"
matTooltip="Login">
<mat-icon>person</mat-icon>
</button>
<button mat-icon-button color="warn" *ngIf="!contact.defaultContact || !contact.isInactive" (click)=" <button mat-icon-button color="warn" *ngIf="!contact.defaultContact || !contact.isInactive" (click)="
deleteContact(contact.spContactId)" [hidden]="contact.defaultContact || contact.isInactive" deleteContact(contact.spContactId)" [hidden]="contact.defaultContact || contact.isInactive"
matTooltip="Delete"> matTooltip="Delete">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<!-- <button mat-icon-button (click)="setDefaultContact(contact.contactId)" <button mat-icon-button (click)="setDefaultContact(contact.spContactId)"
[color]="contact.defaultContact ? 'primary' : ''" matTooltip="Set as default"> *ngIf="!contact.defaultContact || !contact.isInactive"
[hidden]="contact.defaultContact || contact.isInactive" matTooltip="Set as default contact">
<mat-icon>star</mat-icon> <mat-icon>star</mat-icon>
</button> --> </button>
</td> </td>
</ng-container> </ng-container>
@ -85,8 +113,8 @@
</tr> </tr>
</table> </table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]" <mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
[hidePageSize]="true" showFirstLastButtons></mat-paginator> [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div> </div>
<!-- Contact Form --> <!-- Contact Form -->
@ -131,17 +159,16 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Title</mat-label> <mat-label>Title</mat-label>
<input matInput formControlName="title" required> <mat-select formControlName="title" required>
<mat-icon matSuffix>work</mat-icon> <mat-option *ngFor="let contactTitle of contactTitles" [value]="contactTitle.value">
{{ contactTitle.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="contactForm.get('title')?.errors?.['required']"> <mat-error *ngIf="contactForm.get('title')?.errors?.['required']">
Title is required Title is required
</mat-error> </mat-error>
<mat-error *ngIf="contactForm.get('title')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
@ -243,10 +270,11 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button> <button mat-raised-button color="primary" type="submit"
<button mat-raised-button color="primary" type="submit" [disabled]="contactForm.invalid"> [disabled]="contactForm.invalid || changeInProgress">
{{ isEditing ? 'Update' : 'Save' }} {{ isEditing ? 'Update' : 'Save' }}
</button> </button>
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,5 @@
.contacts-container { .contacts-container {
padding: 24px; padding: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
@ -8,6 +8,11 @@
clear: both; clear: both;
margin-bottom: -16px; margin-bottom: -16px;
mat-slide-toggle {
transform: scale(0.8);
margin-left: -0.5rem;
}
button { button {
float: right; float: right;
} }
@ -81,7 +86,7 @@
margin-top: 16px; margin-top: 16px;
.form-header { .form-header {
margin-bottom: 24px; // margin-bottom: 24px;
h3 { h3 {
margin: 0; margin: 0;
@ -98,6 +103,7 @@
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: start;
mat-form-field { mat-form-field {
flex: 1; flex: 1;
@ -141,7 +147,7 @@
.readonly-value { .readonly-value {
padding: 0.25rem; padding: 0.25rem;
font-size: 0.9375rem; font-size: 0.875rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -14,6 +14,10 @@ import { UserPreferences } from '../../core/models/user-preference';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { NotificationService } from '../../core/services/common/notification.service'; import { NotificationService } from '../../core/services/common/notification.service';
import { ContactService } from '../../core/services/service-provider/contact.service'; import { ContactService } from '../../core/services/service-provider/contact.service';
import { finalize, Subject, takeUntil } from 'rxjs';
import { ContactLogin } from '../../core/models/service-provider/contact-login';
import { CommonService } from '../../core/services/common/common.service';
import { ContactTitle } from '../../core/models/contact-title';
@Component({ @Component({
selector: 'app-contacts', selector: 'app-contacts',
@ -23,16 +27,24 @@ import { ContactService } from '../../core/services/service-provider/contact.ser
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
}) })
export class ContactsComponent implements OnInit { export class ContactsComponent implements OnInit {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator, { static: false })
set paginator(value: MatPaginator) {
this.dataSource.paginator = value;
}
@ViewChild(MatSort) sort!: MatSort; @ViewChild(MatSort) sort!: MatSort;
displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'email', 'defaultContact', 'actions']; displayedColumns: string[] = ['firstName', 'middleInitial', 'lastName', 'title', 'phone', 'mobile', 'fax', 'email', 'defaultContact', 'actions'];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
contactForm: FormGroup; contactForm: FormGroup;
isEditing = false; isEditing = false;
currentContactId: number | null = null; currentContactId: number | null = null;
isLoading = false; isLoading = false;
changeInProgress = false;
showForm = false; showForm = false;
showInactiveContacts = false;
contacts: Contact[] = [];
contactTitles: ContactTitle[] = [];
contactReadOnlyFields: any = { contactReadOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
@ -45,8 +57,11 @@ export class ContactsComponent implements OnInit {
@Input() userPreferences: UserPreferences = {}; @Input() userPreferences: UserPreferences = {};
@Output() hasContacts = new EventEmitter<boolean>(); @Output() hasContacts = new EventEmitter<boolean>();
private destroy$ = new Subject<void>();
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private contactService = inject(ContactService); private contactService = inject(ContactService);
private commonService = inject(CommonService);
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@ -56,19 +71,25 @@ export class ContactsComponent implements OnInit {
firstName: ['', [Validators.required, Validators.maxLength(50)]], firstName: ['', [Validators.required, Validators.maxLength(50)]],
lastName: ['', [Validators.required, Validators.maxLength(50)]], lastName: ['', [Validators.required, Validators.maxLength(50)]],
middleInitial: ['', [Validators.maxLength(1)]], middleInitial: ['', [Validators.maxLength(1)]],
title: ['', [Validators.required, Validators.maxLength(100)]], title: ['', [Validators.required]],
phone: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], phone: ['', [Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]],
mobile: ['', [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}$/)]], fax: ['', [Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]],
email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]], email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]],
defaultContact: [false] defaultContact: [false]
}); });
} }
ngOnInit(): void { ngOnInit(): void {
this.loadContactTitles();
this.loadContacts(); this.loadContacts();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
@ -77,20 +98,47 @@ export class ContactsComponent implements OnInit {
loadContacts(): void { loadContacts(): void {
this.isLoading = true; this.isLoading = true;
this.contactService.getContactsById(this.spid).subscribe({ this.contactService.getContactsById(this.spid).pipe(finalize(() => {
next: (contacts: Contact[]) => {
this.dataSource.data = contacts;
this.isLoading = false; this.isLoading = false;
})).subscribe({
next: (contacts: Contact[]) => {
this.contacts = contacts;
this.renderContacts();
}, },
error: (error: any) => { error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts'); let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts');
this.notificationService.showError(errorMessage); this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading contacts:', error); console.error('Error loading contacts:', error);
} }
}); });
} }
loadContactTitles(): void {
this.commonService.getContactTitles()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (contactTitles) => {
this.contactTitles = contactTitles;
},
error: (error) => {
console.error('Failed to load contact titles', error);
}
});
}
toggleShowInactiveContacts(): void {
this.showInactiveContacts = !this.showInactiveContacts;
this.renderContacts();
}
renderContacts(): void {
if (this.showInactiveContacts) {
this.dataSource.data = this.contacts.filter(contact => contact.isInactive);
} else {
this.dataSource.data = this.contacts.filter(contact => !contact.isInactive);
}
}
// applyFilter(event: Event): void { // applyFilter(event: Event): void {
// const filterValue = (event.target as HTMLInputElement).value; // const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase(); // this.dataSource.filter = filterValue.trim().toLowerCase();
@ -144,7 +192,10 @@ export class ContactsComponent implements OnInit {
? this.contactService.updateContact(this.currentContactId!, contactData) ? this.contactService.updateContact(this.currentContactId!, contactData)
: this.contactService.createContact(this.spid, contactData); : this.contactService.createContact(this.spid, contactData);
saveObservable.subscribe({ this.changeInProgress = true;
saveObservable.pipe(finalize(() => {
this.changeInProgress = false;
})).subscribe({
next: () => { next: () => {
this.notificationService.showSuccess(`Contact ${this.isEditing ? 'updated' : 'added'} successfully`); this.notificationService.showSuccess(`Contact ${this.isEditing ? 'updated' : 'added'} successfully`);
this.loadContacts(); this.loadContacts();
@ -194,16 +245,70 @@ export class ContactsComponent implements OnInit {
this.contactForm.reset(); this.contactForm.reset();
} }
// setDefaultContact(contactId: string): void { createLogin(emailAddress: string): void {
// this.contactService.setDefaultServiceProviderContact(this.spid, contactId).subscribe({ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// next: () => { width: '450px',
// this.notificationService.showSuccess('Default contact updated successfully'); data: {
// this.loadContacts(); title: 'Confirm Login Creation',
// }, message: 'Are you sure you want to create a login for this contact?',
// error: (error) => { confirmText: 'Yes',
// this.notificationService.showError('Failed to set default contact'); cancelText: 'Cancel'
// console.error('Error setting default contact:', error); }
// } });
// });
// } dialogRef.afterClosed().subscribe(result => {
if (result) {
const contactLoginData: ContactLogin = {
spid: this.spid,
emailAddress: emailAddress
};
this.contactService.createContactLogin(contactLoginData).subscribe({
next: () => {
this.notificationService.showSuccess(`Login created successfully`);
this.loadContacts();
this.cancelEdit();
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to create login`);
this.notificationService.showError(errorMessage);
console.error('Error saving login:', error);
}
});
}
});
}
setDefaultContact(contactId: string): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '450px',
data: {
title: 'Confirm Default Contact',
message: 'Are you sure you want to set this contact as default contact?',
confirmText: 'Yes',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.contactService.setDefaultContact(contactId).subscribe({
next: () => {
this.notificationService.showSuccess('Default contact updated successfully');
this.loadContacts();
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to set default contact');
this.notificationService.showError(errorMessage);
console.error('Error setting default contact:', error);
}
});
}
});
}
getContactTitleLabel(value: string): string {
const type = this.contactTitles.find(t => t.value === value);
return type ? type.name : value;
}
} }

View File

@ -20,6 +20,13 @@
<app-contacts [spid]="spid" [userPreferences]="userPreferences"></app-contacts> <app-contacts [spid]="spid" [userPreferences]="userPreferences"></app-contacts>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Regions </mat-panel-title>
</mat-expansion-panel-header>
<app-sp-region [spid]="spid"></app-sp-region>
</mat-expansion-panel>
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> Carnet Sequence </mat-panel-title> <mat-panel-title> Carnet Sequence </mat-panel-title>
@ -73,4 +80,12 @@
<app-security-deposit [spid]="spid" [isEditMode]="isEditMode" <app-security-deposit [spid]="spid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-security-deposit> [userPreferences]="userPreferences"></app-security-deposit>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Cargo Rate Setup </mat-panel-title>
</mat-expansion-panel-header>
<app-cargo-rate [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences">
</app-cargo-rate>
</mat-expansion-panel>
</mat-accordion> </mat-accordion>

View File

@ -1,8 +1,6 @@
@use 'colors' as colors;
.page-header { .page-header {
margin: 0.5rem 0px; margin: 0.5rem 0px;
color: colors.$primary-color; color: var(--mat-sys-primary);
font-weight: 500; font-weight: 500;
} }

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