initial version

This commit is contained in:
Cyril Joseph 2025-06-06 07:38:11 -03:00
commit fadfa87cdb
163 changed files with 26481 additions and 0 deletions

17
.editorconfig Normal file
View File

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

42
.gitignore vendored Normal file
View File

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

59
README.md Normal file
View File

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

120
angular.json Normal file
View File

@ -0,0 +1,120 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"service-provider-app": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/service-provider-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": [],
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "service-provider-app:build:production"
},
"development": {
"buildTarget": "service-provider-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": "b249b897-1dd5-450b-9f05-d8dfa49b1876"
}
}

14944
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

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

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

236
public/themes/Theme187.css Normal file
View File

@ -0,0 +1,236 @@
/* 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(#486363, #afcccc);
--mat-sys-on-primary: light-dark(#ffffff, #193535);
--mat-sys-primary-container: light-dark(#cae8e8, #304b4c);
--mat-sys-on-primary-container: light-dark(#022020, #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(#4c6266, #b3cbcf);
--mat-sys-on-secondary: light-dark(#ffffff, #1d3437);
--mat-sys-secondary-container: light-dark(#cee7eb, #344a4e);
--mat-sys-on-secondary-container: light-dark(#071f22, #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;
}

236
public/themes/Theme188.css Normal file
View File

@ -0,0 +1,236 @@
/* Note: Color palettes are generated from primary: #103454, secondary: #18749c */
html {
/* COLOR SYSTEM VARIABLES */
color-scheme: light;
/* Primary palette variables */
--mat-sys-primary: light-dark(#416183, #aac9f1);
--mat-sys-on-primary: light-dark(#ffffff, #0d3252);
--mat-sys-primary-container: light-dark(#d1e4ff, #29496a);
--mat-sys-on-primary-container: light-dark(#001d35, #d1e4ff);
--mat-sys-inverse-primary: light-dark(#aac9f1, #416183);
--mat-sys-primary-fixed: light-dark(#d1e4ff, #d1e4ff);
--mat-sys-primary-fixed-dim: light-dark(#aac9f1, #aac9f1);
--mat-sys-on-primary-fixed: light-dark(#001d35, #001d35);
--mat-sys-on-primary-fixed-variant: light-dark(#29496a, #29496a);
/* Secondary palette variables */
--mat-sys-secondary: light-dark(#00658b, #84cffc);
--mat-sys-on-secondary: light-dark(#ffffff, #00344a);
--mat-sys-secondary-container: light-dark(#c5e7ff, #004c6a);
--mat-sys-on-secondary-container: light-dark(#001e2d, #c5e7ff);
--mat-sys-secondary-fixed: light-dark(#c5e7ff, #c5e7ff);
--mat-sys-secondary-fixed-dim: light-dark(#84cffc, #84cffc);
--mat-sys-on-secondary-fixed: light-dark(#001e2d, #001e2d);
--mat-sys-on-secondary-fixed-variant: light-dark(#004c6a, #004c6a);
/* Tertiary palette variables */
--mat-sys-tertiary: light-dark(#73537b, #e1b9e8);
--mat-sys-on-tertiary: light-dark(#ffffff, #42254a);
--mat-sys-tertiary-container: light-dark(#fbd7ff, #5a3b62);
--mat-sys-on-tertiary-container: light-dark(#2b1034, #fbd7ff);
--mat-sys-tertiary-fixed: light-dark(#fbd7ff, #fbd7ff);
--mat-sys-tertiary-fixed-dim: light-dark(#e1b9e8, #e1b9e8);
--mat-sys-on-tertiary-fixed: light-dark(#2b1034, #2b1034);
--mat-sys-on-tertiary-fixed-variant: light-dark(#5a3b62, #5a3b62);
/* Neutral palette variables */
--mat-sys-background: light-dark(#faf9fc, #121316);
--mat-sys-on-background: light-dark(#1a1c1e, #e3e2e5);
--mat-sys-surface: light-dark(#faf9fc, #121316);
--mat-sys-surface-dim: light-dark(#dad9dd, #121316);
--mat-sys-surface-bright: light-dark(#faf9fc, #38393c);
--mat-sys-surface-container-low: light-dark(#f4f3f6, #1a1c1e);
--mat-sys-surface-container-lowest: light-dark(#ffffff, #0d0e11);
--mat-sys-surface-container: light-dark(#eeedf0, #1e2022);
--mat-sys-surface-container-high: light-dark(#e8e8eb, #292a2c);
--mat-sys-surface-container-highest: light-dark(#e3e2e5, #333537);
--mat-sys-on-surface: light-dark(#1a1c1e, #e3e2e5);
--mat-sys-shadow: light-dark(#000000, #000000);
--mat-sys-scrim: light-dark(#000000, #000000);
--mat-sys-surface-tint: light-dark(#416183, #aac9f1);
--mat-sys-inverse-surface: light-dark(#2f3033, #e3e2e5);
--mat-sys-inverse-on-surface: light-dark(#f1f0f3, #2f3033);
--mat-sys-outline: light-dark(#73777e, #8d9198);
--mat-sys-outline-variant: light-dark(#c3c7ce, #43474e);
--mat-sys-neutral10: light-dark(#1a1c1e, #1a1c1e); /* 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(#dfe2eb, #43474e);
--mat-sys-on-surface-variant: light-dark(#43474e, #c3c7ce);
--mat-sys-neutral-variant20: light-dark(#2c3137, #2c3137); /* 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;
}

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

@ -0,0 +1,195 @@
/* Note: Color palettes copied from node_modules\@angular\material\prebuilt-themes\azure-blue.css */
html {
--mat-sys-background: #faf9fd;
--mat-sys-error: #ba1a1a;
--mat-sys-error-container: #ffdad6;
--mat-sys-inverse-on-surface: #f2f0f4;
--mat-sys-inverse-primary: #abc7ff;
--mat-sys-inverse-surface: #2f3033;
--mat-sys-on-background: #1a1b1f;
--mat-sys-on-error: #ffffff;
--mat-sys-on-error-container: #93000a;
--mat-sys-on-primary: #ffffff;
--mat-sys-on-primary-container: #00458f;
--mat-sys-on-primary-fixed: #001b3f;
--mat-sys-on-primary-fixed-variant: #00458f;
--mat-sys-on-secondary: #ffffff;
--mat-sys-on-secondary-container: #3e4759;
--mat-sys-on-secondary-fixed: #131c2b;
--mat-sys-on-secondary-fixed-variant: #3e4759;
--mat-sys-on-surface: #1a1b1f;
--mat-sys-on-surface-variant: #44474e;
--mat-sys-on-tertiary: #ffffff;
--mat-sys-on-tertiary-container: #0000ef;
--mat-sys-on-tertiary-fixed: #00006e;
--mat-sys-on-tertiary-fixed-variant: #0000ef;
--mat-sys-outline: #74777f;
--mat-sys-outline-variant: #c4c6d0;
--mat-sys-primary: #005cbb;
--mat-sys-primary-container: #d7e3ff;
--mat-sys-primary-fixed: #d7e3ff;
--mat-sys-primary-fixed-dim: #abc7ff;
--mat-sys-scrim: #000000;
--mat-sys-secondary: #565e71;
--mat-sys-secondary-container: #dae2f9;
--mat-sys-secondary-fixed: #dae2f9;
--mat-sys-secondary-fixed-dim: #bec6dc;
--mat-sys-shadow: #000000;
--mat-sys-surface: #faf9fd;
--mat-sys-surface-bright: #faf9fd;
--mat-sys-surface-container: #efedf0;
--mat-sys-surface-container-high: #e9e7eb;
--mat-sys-surface-container-highest: #e3e2e6;
--mat-sys-surface-container-low: #f4f3f6;
--mat-sys-surface-container-lowest: #ffffff;
--mat-sys-surface-dim: #dbd9dd;
--mat-sys-surface-tint: #005cbb;
--mat-sys-surface-variant: #e0e2ec;
--mat-sys-tertiary: #343dff;
--mat-sys-tertiary-container: #e0e0ff;
--mat-sys-tertiary-fixed: #e0e0ff;
--mat-sys-tertiary-fixed-dim: #bec2ff;
--mat-sys-neutral-variant20: #2d3038;
--mat-sys-neutral10: #1a1b1f
}
html {
--mat-sys-level0: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12)
}
html {
--mat-sys-level1: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12)
}
html {
--mat-sys-level2: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12)
}
html {
--mat-sys-level3: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12)
}
html {
--mat-sys-level4: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12)
}
html {
--mat-sys-level5: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12)
}
html {
--mat-sys-body-large: 400 1rem / 1.5rem Roboto;
--mat-sys-body-large-font: Roboto;
--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: 400;
--mat-sys-body-medium: 400 0.875rem / 1.25rem Roboto;
--mat-sys-body-medium-font: Roboto;
--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: 400;
--mat-sys-body-small: 400 0.75rem / 1rem Roboto;
--mat-sys-body-small-font: Roboto;
--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: 400;
--mat-sys-display-large: 400 3.562rem / 4rem Roboto;
--mat-sys-display-large-font: Roboto;
--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: 400;
--mat-sys-display-medium: 400 2.812rem / 3.25rem Roboto;
--mat-sys-display-medium-font: Roboto;
--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: 400;
--mat-sys-display-small: 400 2.25rem / 2.75rem Roboto;
--mat-sys-display-small-font: Roboto;
--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: 400;
--mat-sys-headline-large: 400 2rem / 2.5rem Roboto;
--mat-sys-headline-large-font: Roboto;
--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: 400;
--mat-sys-headline-medium: 400 1.75rem / 2.25rem Roboto;
--mat-sys-headline-medium-font: Roboto;
--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: 400;
--mat-sys-headline-small: 400 1.5rem / 2rem Roboto;
--mat-sys-headline-small-font: Roboto;
--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: 400;
--mat-sys-label-large: 500 0.875rem / 1.25rem Roboto;
--mat-sys-label-large-font: Roboto;
--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: 500;
--mat-sys-label-large-weight-prominent: 700;
--mat-sys-label-medium: 500 0.75rem / 1rem Roboto;
--mat-sys-label-medium-font: Roboto;
--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: 500;
--mat-sys-label-medium-weight-prominent: 700;
--mat-sys-label-small: 500 0.688rem / 1rem Roboto;
--mat-sys-label-small-font: Roboto;
--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: 500;
--mat-sys-title-large: 400 1.375rem / 1.75rem Roboto;
--mat-sys-title-large-font: Roboto;
--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: 400;
--mat-sys-title-medium: 500 1rem / 1.5rem Roboto;
--mat-sys-title-medium-font: Roboto;
--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: 500;
--mat-sys-title-small: 500 0.875rem / 1.25rem Roboto;
--mat-sys-title-small-font: Roboto;
--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: 500
}
html {
--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
}
html {
--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
}

View File

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

View File

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

View File

@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'service-provider-app' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('service-provider-app');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, service-provider-app');
});
});

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

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

View File

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

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

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

View File

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Prerender
}
];

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
<nav class="navbar">
<div class="navbar-brand">
<img *ngIf="logoUrl" [src]="logoUrl" alt="App Logo" class="logo">
<!-- <span class="app-title">USCIB Carnet Portal</span> -->
</div>
<div class="navbar-menu">
<button mat-button (click)="navigateTo('home')">Home</button>
<button mat-button [matMenuTriggerFor]="users">Users</button>
<mat-menu #users="matMenu">
<button mat-menu-item (click)="navigateTo('usersettings')">User Settings</button>
</mat-menu>
<button mat-button [matMenuTriggerFor]="maintenance">Maintenance</button>
<mat-menu #maintenance="matMenu">
<button mat-menu-item (click)="navigateTo('preparer')">Preparer</button>
<button mat-menu-item (click)="navigateTo('holder')">Holder</button>
<button mat-menu-item (click)="navigateTo('carnet')">Carnet</button>
</mat-menu>
<button mat-button [matMenuTriggerFor]="admin">Admin</button>
<mat-menu #admin="matMenu">
<button mat-menu-item (click)="navigateTo('sequence')">Sequence</button>
<button mat-menu-item (click)="navigateTo('regions')">Regions</button>
<button mat-menu-item (click)="navigateTo('users')">Users</button>
</mat-menu>
<!--
<button mat-icon-button [matMenuTriggerFor]="profile">
<mat-icon>account_circle</mat-icon>
</button>
<mat-menu #profile="matMenu">
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
<span>Logout</span>
</button>
</mat-menu> -->
<div class="profile-container">
<button mat-icon-button (click)="toggleProfileMenu()" class="profile-button">
<mat-icon>account_circle</mat-icon>
</button>
<div class="profile-menu" *ngIf="showProfileMenu">
<div class="profile-info">
<mat-icon>email</mat-icon>
<span>{{ userEmail }}</span>
</div>
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
<span>Logout</span>
</button>
</div>
</div>
</div>
</nav>

View File

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

View File

@ -0,0 +1,70 @@
import { Component, effect, OnInit } from '@angular/core';
import { UserService } from '../../core/services/common/user.service';
import { Router } from '@angular/router';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { NavigationService } from '../../core/services/common/navigation.service';
import { User } from '../../core/models/user';
import { ThemeService } from '../../core/services/theme.service';
import { StorageService } from '../../core/services/common/storage.service';
@Component({
selector: 'app-secured-header',
imports: [AngularMaterialModule, CommonModule],
templateUrl: './secured-header.component.html',
styleUrl: './secured-header.component.scss'
})
export class SecuredHeaderComponent implements OnInit {
userEmail: string = '';
logoUrl: string | undefined = '';
showProfileMenu: boolean = false;
userDetails: User | null = {};
constructor(
private userService: UserService,
private router: Router,
private navigationService: NavigationService,
private themeService: ThemeService,
private storageService: StorageService
) {
effect(() => {
this.userDetails = this.userService.userDetailsSignal();
if (this.userDetails?.userDetails) {
this.setApplicationDetails();
}
});
}
ngOnInit(): void {
this.userEmail = this.userService.getSafeUser();
this.userDetails = this.userService.getUserDetails();
this.setApplicationDetails();
}
setApplicationDetails() {
if (this.userDetails?.userDetails) {
this.logoUrl = `images/logos/${this.userDetails?.userDetails?.logoName}`;
this.themeService.setTheme(this.userDetails?.userDetails?.themeName);
} else {
this.themeService.setTheme('default');
}
}
toggleProfileMenu(): void {
this.showProfileMenu = !this.showProfileMenu;
}
logout(): void {
this.userService.clearUser();
this.storageService.clear();
this.router.navigate(['/login']);
this.showProfileMenu = false;
this.themeService.setTheme('default');
}
navigateTo(route: string): void {
this.navigationService.navigate([route]);
this.showProfileMenu = false;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
export interface BasicDetail {
clientid: number;
spid: number
name: string;
lookupCode: string;
address1: string;
address2?: string | null;
city: string;
state: string;
country: string;
zip: string;
carnetIssuingRegion: string;
revenueLocation: string;
}

View File

@ -0,0 +1,20 @@
export interface Contact {
clientContactId: number;
spid: number
clientid: number;
defaultContact: boolean;
firstName: string;
lastName: string;
middleInitial?: string | null;
title: string;
phone: string;
mobile: string;
fax?: string | null;
email: string;
dateCreated?: Date | null;
createdBy?: string | null;
lastUpdatedBy?: string | null;
lastUpdatedDate?: Date | null;
isInactive?: boolean | null; // TODO
inactivatedDate?: Date | null; // TODO
}

View File

@ -0,0 +1,10 @@
export interface Location {
id: number;
clientId: number;
locationName: string;
address1: string;
address2?: string | null;
city: string;
state: string;
country: string;
}

View File

@ -0,0 +1,7 @@
export interface PreparerFilter {
name?: string;
address1?: string;
city?: string;
state?: string;
lookupCode?: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
export interface User {
roles?: string[] | null;
menus?: string[] | null;
menuDetails?: Menu[] | null;
userDetails?: UserDetail | null;
}
export interface Menu {
name: string;
pageName: string
}
export interface UserDetail {
spid: number;
urlKey: string;
logoName: string;
themeName: string;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { StorageService } from './storage.service';
@Injectable({
providedIn: 'root'
})
export class NavigationService {
private readonly USER_APPID_KEY = 'CurrentAppId';
constructor(private router: Router,
private location: Location,
private storageService: StorageService) { }
setCurrentAppId(appId: string): void {
this.storageService.setItem(this.USER_APPID_KEY, appId);
}
getCurrentAppId(): string {
return this.storageService.getItem(this.USER_APPID_KEY) ?? '';
}
navigate(commands: any[], extras?: any): void {
const currentAppId = this.getCurrentAppId();
// Prepend appId to all navigations
this.router.navigate([currentAppId, ...commands], extras);
}
navigateByUrl(url: string, extras?: any): void {
const currentAppId = this.getCurrentAppId();
// Ensure URL starts with current appId
const fullUrl = `/${currentAppId}${url.startsWith('/') ? url : `/${url}`}`;
this.router.navigateByUrl(fullUrl, extras);
}
goBack(): void {
this.location.back();
}
goForward(): void {
this.location.forward();
}
}

View File

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

View File

@ -0,0 +1,50 @@
import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private readonly platformId = inject(PLATFORM_ID);
constructor() { }
setItem(key: string, value: string): void {
sessionStorage.setItem(key, value);
}
getItem(key: string): string | null {
if (isPlatformBrowser(this.platformId)) {
return sessionStorage.getItem(key);
}
return null;
}
get<T>(key: string): T | null {
try {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) as T : null;
} catch (e) {
console.error(`Error getting ${key} from session storage`, e);
return null;
}
}
set(key: string, value: any): void {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error(`Error setting ${key} in session storage`, e);
}
}
removeItem(key: string): void {
sessionStorage.removeItem(key);
}
clear(): void {
sessionStorage.clear();
}
}

View File

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

View File

@ -0,0 +1,130 @@
import { Injectable, signal } from '@angular/core';
import { StorageService } from './storage.service';
import { BehaviorSubject, map, Observable, of, Subject, tap } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { Menu, User, UserDetail } from '../../models/user';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
private spid: number = 0;
userDetailsSignal = signal<User>({});
private readonly USER_EMAIL_KEY = 'CurrentUserEmail';
private readonly USER_DETAILS_KEY = 'CurrentUserData';
private userLoggedInSubject = new BehaviorSubject<boolean>(true);
constructor(private http: HttpClient, private storageService: StorageService) { }
watchUser(): Observable<boolean> {
return this.userLoggedInSubject.asObservable();
}
setUser(email: string): void {
if (!email) {
console.error('Cannot set empty user email');
return;
}
this.storageService.setItem(this.USER_EMAIL_KEY, email);
this.userLoggedInSubject.next(true);
}
getUser(): string | null {
const user = this.storageService.getItem(this.USER_EMAIL_KEY);
if (!user) {
this.userLoggedInSubject.next(false);
}
return user;
}
setUserDetails(): Observable<User> {
const userDetails = this.getUserDetails();
if (userDetails) {
return of(userDetails);
}
const email = this.getUser();
if (!email) {
console.error('invalid user');
}
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetUserDetails/${email}`).pipe(
map(response => this.mapToUser(response)),
tap(user => this.saveUserToStorage(user)));
}
getUserDetails(): User | null {
return this.storageService.get<User>(this.USER_DETAILS_KEY);
}
clearUser(): void {
this.storageService.removeItem(this.USER_EMAIL_KEY);
this.storageService.removeItem(this.USER_DETAILS_KEY);
this.userLoggedInSubject.next(false);
}
isLoggedIn(): boolean {
return !!this.getUser();
}
getSafeUser(): string {
return this.getUser() || '';
}
getUserSpid(): number {
if (this.spid === 0) {
const userDetails = this.getUserDetails();
if (userDetails && userDetails.userDetails && userDetails.userDetails.spid) {
this.spid = userDetails.userDetails.spid;
}
}
return this.spid;
}
private mapToUser(data: any): User {
return {
roles: data.roleDetails || null,
menus: data.menuDetails || null,
menuDetails: this.mapMenuDetails(data.menuPageDetails),
userDetails: this.mapUserDetails(data.userDetails)
};
}
private mapMenuDetails(menuPageDetails: any[]): Menu[] | null {
if (!menuPageDetails) return null;
return menuPageDetails.map(item => ({
name: item.MENUNAME,
pageName: item.PAGENAME
}));
}
private mapUserDetails(userDetails: any): UserDetail | null {
if (!userDetails) return null;
return {
spid: userDetails.SPID,
urlKey: userDetails.ENCURLKEY,
logoName: userDetails.LOGONAME,
themeName: userDetails.THEMENAME
};
}
private saveUserToStorage(user: User): void {
try {
this.storageService.set(this.USER_DETAILS_KEY, user);
this.userDetailsSignal.set(user);
} catch (e) {
console.error('Error saving user details to session storage', e);
}
}
}

View File

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

View File

@ -0,0 +1,77 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UserService } from '../common/user.service';
import { environment } from '../../../../environments/environment';
import { BasicDetail } from '../../models/preparer/basic-detail';
import { filter, map, Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class BasicDetailService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService) { }
getBasicDetailsById(id: number): BasicDetail | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid?p_spid=${this.userService.getUserSpid()}&p_clientid=${id}`).pipe(
filter(response => response.length > 0),
map(response => this.mapToBasicDetail(response?.[0])));
}
private mapToBasicDetail(basicDetails: any): BasicDetail {
return {
clientid: basicDetails.CLIENTID,
spid: basicDetails.SPID,
name: basicDetails.NAMEOF,
lookupCode: basicDetails.LOOKUPCODE,
address1: basicDetails.ADDRESS1,
address2: basicDetails.ADDRESS2,
city: basicDetails.CITY,
state: basicDetails.STATE,
country: basicDetails.COUNTRY,
carnetIssuingRegion: basicDetails.ISSUINGREGION,
revenueLocation: basicDetails.REVENUELOCATION,
zip: basicDetails.ZIP,
};
}
createBasicDetails(data: BasicDetail): Observable<any> {
const basicDetails = {
p_spid: this.userService.getUserSpid(),
p_clientname: data.name,
p_lookupcode: data.lookupCode,
p_address1: data.address1,
p_address2: data.address2,
p_city: data.city,
p_state: data.state,
p_country: data.country,
p_zip: data.zip,
p_issuingregion: data.carnetIssuingRegion,
p_revenuelocation: data.revenueLocation,
p_userid: this.userService.getUser(),
}
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateNewClients`, basicDetails);
}
updateBasicDetails(id: number, data: BasicDetail): Observable<any> {
const basicDetails = {
p_spid: this.userService.getUserSpid(),
p_clientid: id,
p_clientname: data.name,
p_lookupcode: data.lookupCode,
p_address1: data.address1,
p_address2: data.address2,
p_city: data.city,
p_state: data.state,
p_country: data.country,
p_zip: data.zip,
p_revenuelocation: data.revenueLocation,
p_userid: this.userService.getUser(),
}
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateClient`, basicDetails);
}
}

View File

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

View File

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import { UserService } from '../common/user.service';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../../environments/environment';
import { map, Observable, of } from 'rxjs';
import { Contact } from '../../models/preparer/contact';
@Injectable({
providedIn: 'root'
})
export class ContactService {
private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb;
constructor(private http: HttpClient, private userService: UserService) { }
getContactsById(id: number): Observable<Contact[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerContactsByClientid?p_spid=${this.userService.getUserSpid()}&p_clientid=${id}`).pipe(
map(response => this.mapToContacts(response)));
}
private mapToContacts(data: any[]): Contact[] {
return data.map(contact => ({
clientContactId: contact.CLIENTCONTACTID,
spid: contact.SPID,
clientid: contact.CLIENTID,
defaultContact: contact.DEFCONTACTFLAG === 'Y',
firstName: contact.FIRSTNAME,
lastName: contact.LASTNAME,
title: contact.TITLE,
phone: contact.PHONENO,
mobile: contact.MOBILENO,
fax: contact.FAXNO || null,
email: contact.EMAILADDRESS,
middleInitial: contact.MIDDLEINITIAL || null,
createdBy: contact.CREATEDBY || null,
dateCreated: contact.DATECREATED || null,
lastUpdatedBy: contact.LASTUPDATEDBY || null,
lastUpdatedDate: contact.LASTUPDATEDDATE || null,
isInactive: contact.INACTIVEFLAG === 'Y' || false,
inactivatedDate: contact.INACTIVEDATE || null
}));
}
createContact(clientid: number, data: Contact): Observable<any> {
const contact = {
p_spid: this.userService.getUserSpid(),
p_clientid: clientid,
p_defcontactflag: data.defaultContact ? 'Y' : 'N',
p_contactstable: [{
FirstName: data.firstName,
LastName: data.lastName,
MiddleInitial: data.middleInitial,
Title: data.title,
EmailAddress: data.email,
MobileNo: data.mobile,
PhoneNo: data.phone,
FaxNo: data.fax
}],
p_user_id: this.userService.getUser()
}
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateClientContacts`, contact);
}
updateContact(spContactId: number, data: Contact): Observable<any> {
const contact = {
p_spid: this.userService.getUserSpid(),
p_clientcontactid: spContactId,
p_firstname: data.firstName,
p_lastname: data.lastName,
P_middleinitial: data.middleInitial,
p_title: data.title,
p_phone: data.phone,
p_mobileno: data.mobile,
p_fax: data.fax,
p_emailaddress: data.email,
p_user_id: this.userService.getUser()
}
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateClientContacts`, contact);
}
deleteContact(clientContactId: string): Observable<any> {
return this.http.post(`${this.apiUrl}/${this.apiDb}/InactivateSPContact?p_clientcontactid=${clientContactId}`, null);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StyleManagerService {
constructor() { }
/**
* 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,19 @@
import { Injectable } from '@angular/core';
import { StyleManagerService } from './stylemanager.service';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
constructor(private styleManager: StyleManagerService) {
}
setTheme(themeToSet: string) {
this.styleManager.setStyle(
"theme",
`themes/${themeToSet}.css`
);
}
}

View File

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

View File

@ -0,0 +1,24 @@
import { ActivatedRouteSnapshot, CanActivate, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';
import { NavigationService } from '../core/services/common/navigation.service';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AppIdGuard implements CanActivate {
constructor(private navigationService: NavigationService, private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
const appId = next.params['appId'];
const currentAppId = this.navigationService.getCurrentAppId();
if (appId === currentAppId) {
return true;
}
this.router.navigate(['/404']);
return false;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,63 @@
.dashboard-container {
padding: 24px;
.quick-actions {
display: flex;
justify-content: end;
button {
margin-bottom: 16px;
}
}
.table-container {
position: relative;
overflow: auto;
border-radius: 8px;
.loading-shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.7);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
mat-table {
width: 100%;
mat-icon {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
}
.no-data-message {
text-align: center;
padding: 0.9rem;
color: rgba(0, 0, 0, 0.54);
mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
margin-bottom: -3px;
}
}
mat-paginator {
border-top: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 0 0 8px 8px;
padding-top: 4px;
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,96 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { AuthService } from '../core/services/common/auth.service';
import { NavigationService } from '../core/services/common/navigation.service';
import { HomeService } from '../core/services/home.service';
import { UserService } from '../core/services/common/user.service';
import { User } from '../core/models/user';
import { ThemeService } from '../core/services/theme.service';
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {
loginForm: FormGroup;
isLoading = false;
errorMessage = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private userService: UserService,
private homeService: HomeService,
private navigationService: NavigationService,
private router: Router,
private themeService: ThemeService
) {
this.loginForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
ngOnInit(): void {
if (this.userService.isLoggedIn()) {
const appId = this.navigationService.getCurrentAppId();
if (!appId) {
this.setUserData();
} else {
this.navigationService.navigate(['home']);
}
} else {
this.themeService.setTheme('default');
}
}
onSubmit(): void {
if (this.loginForm.valid) {
this.isLoading = true;
this.errorMessage = '';
const { username, password } = this.loginForm.value;
this.authService.login(username, password).subscribe({
next: (response) => {
if (response?.msg) {
this.userService.setUser(username);
this.setUserData();
} else {
this.errorMessage = response?.error ?? response?.msg;
}
},
error: (error) => {
this.isLoading = false;
this.errorMessage = 'Invalid username or password';
console.error('Login error:', error);
}
});
}
}
setUserData() {
this.userService.setUserDetails().subscribe({
next: (data: User) => {
if (data?.userDetails) {
this.navigationService.setCurrentAppId(data.userDetails.urlKey);
this.navigationService.navigate(['home']);
} else {
this.errorMessage = "User doesn't have permissions.";
this.isLoading = false;
}
},
error: (error: any) => {
this.isLoading = false;
this.errorMessage = "User doesn't have permissions.";
console.error('Error retrieving app data:', error);
}
});
}
}

View File

@ -0,0 +1,32 @@
<div class="preparer-container">
<mat-stepper orientation="vertical" [linear]="true" (selectionChange)="onStepChange($event)"
[selectedIndex]="currentStep">
<!-- Basic Details Step -->
<mat-step [completed]="basicDetailsCompleted">
<ng-template matStepLabel>Basic Details</ng-template>
<app-basic-details [isEditMode]="isEditMode" (clientidCreated)="onBasicDetailsSaved($event)">
</app-basic-details>
</mat-step>
<!-- Contacts Step -->
<mat-step [completed]="contactsCompleted" [editable]="!!clientid && basicDetailsCompleted">
<ng-template matStepLabel>Contacts</ng-template>
<app-contacts *ngIf="clientid" [clientid]="clientid" (hasContacts)="onContactsSaved($event)"
[userPreferences]="userPreferences">
</app-contacts>
</mat-step>
<!-- Location Step -->
<!-- <mat-step [completed]="locationCompleted" [editable]="!!clientid && contactsCompleted">
<ng-template matStepLabel>Locations</ng-template>
<app-location *ngIf="clientid" [clientid]="clientid" (hasLocation)="onLocationSaved($event)"
[userPreferences]="userPreferences">
</app-location>
</mat-step> -->
</mat-stepper>
</div>

View File

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

View File

@ -0,0 +1,48 @@
import { Component } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { BasicDetailsComponent } from '../basic-details/basic-details.component';
import { ContactsComponent } from '../contacts/contacts.component';
import { LocationComponent } from '../location/location.component';
@Component({
selector: 'app-add-preparer',
imports: [AngularMaterialModule, CommonModule, BasicDetailsComponent, ContactsComponent, LocationComponent],
templateUrl: './add-preparer.component.html',
styleUrl: './add-preparer.component.scss'
})
export class AddPreparerComponent {
isEditMode = false;
clientid: number | null = null;
currentStep: number = 0;
isLoading: boolean = false;
userPreferences: UserPreferences;
basicDetailsCompleted: boolean = false;
contactsCompleted: boolean = false;
locationCompleted: boolean = false;
constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
}
onBasicDetailsSaved(event: string): void {
this.clientid = +event;
this.basicDetailsCompleted = true;
}
onContactsSaved(event: boolean): void {
this.contactsCompleted = event;
}
onLocationSaved(event: boolean): void {
this.locationCompleted = event;
}
onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex;
}
}

View File

@ -0,0 +1,149 @@
<div class="basic-details-container">
<mat-card class="details-card mat-elevation-z4">
<mat-card-content>
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<form [formGroup]="basicDetailsForm" class="details-form" *ngIf="!isLoading"
(ngSubmit)="saveBasicDetails()">
<!-- Client Information -->
<div class="form-row">
<mat-form-field appearance="outline" class="name">
<mat-label>Name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="f['name'].errors?.['required']">
Name is required
</mat-error>
<mat-error *ngIf="f['name'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="lookup-code">
<mat-label>Lookup Code</mat-label>
<input matInput formControlName="lookupCode" required>
<mat-error *ngIf="f['lookupCode'].errors?.['required']">
Lookup code is required
</mat-error>
<mat-error *ngIf="f['lookupCode'].errors?.['maxlength']">
Maximum 20 characters allowed
</mat-error>
</mat-form-field>
</div>
<!-- Address Information -->
<div class="form-row">
<mat-form-field appearance="outline" class="address1">
<mat-label>Address Line 1</mat-label>
<input matInput formControlName="address1" required>
<mat-error *ngIf="f['address1'].errors?.['required']">
Address is required
</mat-error>
<mat-error *ngIf="f['address1'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="address2">
<mat-label>Address Line 2 (Optional)</mat-label>
<input matInput formControlName="address2">
<mat-error *ngIf="f['address2'].errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<!-- Location Information -->
<div class="form-row">
<mat-form-field appearance="outline" class="city">
<mat-label>City</mat-label>
<input matInput formControlName="city" required>
<mat-error *ngIf="f['city'].errors?.['required']">
City is required
</mat-error>
<mat-error *ngIf="f['city'].errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="country">
<mat-label>Country</mat-label>
<mat-select formControlName="country" required
(selectionChange)="onCountryChange($event.value)">
<mat-option *ngFor="let country of countries" [value]="country.value">
{{ country.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['country'].errors?.['required']">
Country is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="state">
<mat-label>State/Province</mat-label>
<mat-select formControlName="state" required>
<mat-option *ngFor="let state of states" [value]="state.value">
{{ state.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['state'].errors?.['required']">
State is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="zip">
<mat-label>ZIP/Postal Code</mat-label>
<input matInput formControlName="zip" required>
<mat-error *ngIf="f['zip'].errors?.['required']">
ZIP/Postal code is required
</mat-error>
<mat-error
*ngIf="f['country']?.value === 'US' && f['zip']?.touched && f['zip']?.errors?.['invalidUSZip']">
Please enter a valid 5-digit US ZIP code
</mat-error>
<mat-error
*ngIf="f['country']?.value === 'CA' && f['zip']?.touched && f['zip']?.errors?.['invalidCanadaPostal']">
Please enter a valid postal code (e.g., A1B2C3)
</mat-error>
</mat-form-field>
</div>
<!-- Carnet Issuing Region -->
<div class="form-row">
<mat-form-field appearance="outline" class="carnet-issuing-region">
<mat-label>Carnet Issuing Region</mat-label>
<mat-select formControlName="carnetIssuingRegion" required>
<mat-option *ngFor="let region of regions" [value]="region.region">
{{ region.regionname }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['carnetIssuingRegion'].errors?.['required']">
Carnet issuing region is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="revenue-location">
<mat-label>Revenue Location</mat-label>
<mat-select formControlName="revenueLocation" required>
<mat-option *ngFor="let state of states" [value]="state.value">
{{ state.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="f['revenueLocation'].errors?.['required']">
Revenue location is required
</mat-error>
</mat-form-field>
</div>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit" [disabled]="basicDetailsForm.invalid">
Save
</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,98 @@
.basic-details-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
.details-card {
overflow: hidden;
transition: all 0.3s ease;
position: relative;
background: none;
box-shadow: none;
.loading-shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.7);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.details-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
align-items: start;
.name {
grid-column: span 2;
}
.lookup-code {
grid-column: span 1;
}
.address1,
.address2 {
grid-column: span 3;
}
.city,
.state,
.country,
.zip {
grid-column: span 1;
}
.carnet-issuing-region,
.revenue-location {
grid-column: span 1;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
}
mat-form-field {
width: 100%;
}
}
}
@media (max-width: 960px) {
.basic-details-container {
.details-card {
.form-row {
grid-template-columns: 1fr;
.name,
.lookup-code,
.address1,
.address2,
.city,
.state,
.zip,
.country,
.carnet-issuing-region,
.revenue-location {
grid-column: span 1;
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,30 @@
<h2 *ngIf="this.clientName" class="page-header">Manage {{this.clientName}}</h2>
<div class="preparer-action-buttons">
<button mat-button (click)="accordion().openAll()">Expand All</button>
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
</div>
<mat-accordion class="preparer-headers-align" multi>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Basic Details </mat-panel-title>
</mat-expansion-panel-header>
<app-basic-details [clientid]="clientid" [isEditMode]="isEditMode"
(clientName)="onClientNameUpdate($event)"></app-basic-details>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Contacts </mat-panel-title>
</mat-expansion-panel-header>
<app-contacts [clientid]="clientid" [userPreferences]="userPreferences"></app-contacts>
</mat-expansion-panel>
<!--
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Locations </mat-panel-title>
</mat-expansion-panel-header>
<app-location [clientid]="clientid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences"></app-location>
</mat-expansion-panel> -->
</mat-accordion>

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