initial setup

This commit is contained in:
Cyril Joseph 2025-07-02 22:40:35 -03:00
commit f9fe0569d3
133 changed files with 14148 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 @@
# ClientApp
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.4.
## 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.

149
angular.json Normal file
View File

@ -0,0 +1,149 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"client-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/build:application",
"options": {
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2MB",
"maximumError": "3MB"
},
{
"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/build:dev-server",
"options": {
"allowedHosts": [
"localhost",
"client.alphaomegainfosys.com"
]
},
"configurations": {
"production": {
"buildTarget": "client-app:build:production"
},
"development": {
"buildTarget": "client-app:build:development",
"port": 5173
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
]
}
}
}
}
},
"cli": {
"analytics": "ea8b3ea5-8b3d-49fc-9322-afc880e9e04a"
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

9551
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "client-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:client-app": "node dist/client-app/server/server.mjs"
},
"prettier": {
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true,
"dependencies": {
"@angular/cdk": "^20.0.4",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/material": "^20.0.4",
"@angular/material-moment-adapter": "^20.0.4",
"@angular/platform-browser": "^20.0.0",
"@angular/platform-server": "^20.0.0",
"@angular/router": "^20.0.0",
"@angular/ssr": "^20.0.4",
"chart.js": "^4.3.0",
"express": "^5.1.0",
"ng2-charts": "^8.0.0",
"ngx-cookie-service": "^20.0.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.4",
"@angular/cli": "^20.0.4",
"@angular/compiler-cli": "^20.0.0",
"@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0",
"@types/node": "^20.17.19",
"jasmine-core": "~5.7.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.8.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');
});
});

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

@ -0,0 +1,37 @@
import { Component, inject } 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 {
isUserLoggedIn = false;
private userSubscription!: Subscription;
private userService = inject(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,12 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes))
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

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

@ -0,0 +1,20 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, 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 { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AuthInterceptor } from './core/interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideHttpClient(withInterceptorsFromDi()),
provideCharts(withDefaultRegisterables()),
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
]
};

View File

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

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

@ -0,0 +1,24 @@
import { Routes } from '@angular/router';
import { AppIdGuard } from './guards/appid.guard';
import { AuthGuard } from './guards/auth.guard';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { NotFoundComponent } from './shared/components/not-found/not-found.component';
import { UserSettingsComponent } from './user-settings/user-settings.component';
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: '404', component: NotFoundComponent },
{
path: ':appId',
children: [
{ path: 'home', component: HomeComponent },
{ path: 'usersettings', component: UserSettingsComponent },
{ path: 'add-carnet', loadComponent: () => import('./carnet/add/add-carnet.component').then(m => m.AddCarnetComponent) },
{ path: '', redirectTo: 'home', pathMatch: 'full' }
],
canActivate: [AuthGuard, AppIdGuard]
},
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: '**', redirectTo: '/404' }
];

View File

@ -0,0 +1,109 @@
<div class="client-carnet-container">
<!-- Initial Questions Section -->
<div *ngIf="!questionsCompleted" class="questions-section">
<h2>Add New Carnet</h2>
<form [formGroup]="questionsForm" (ngSubmit)="onQuestionsSubmit()">
<div class="question-row">
<mat-radio-group formControlName="newCarnet">
<mat-label>Do you need a new carnet?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
</div>
<div class="question-row">
<mat-radio-group formControlName="additionalSets">
<mat-label>Do you need additional sets?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
</div>
<div class="question-row">
<mat-radio-group formControlName="duplicateCarnet">
<mat-label>Did you lose your carnet? Are you looking for duplicates?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
</div>
<div class="question-row">
<mat-radio-group formControlName="extendCarnet">
<mat-label>Do you need to extend the carnet?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
</div>
<div class="actions">
<button mat-raised-button color="primary" type="submit" [disabled]="!questionsForm.valid">
Continue
</button>
</div>
</form>
</div>
<!-- Stepper Section (shown after questions are answered) -->
<mat-stepper *ngIf="questionsCompleted" orientation="vertical" [linear]="isLinear"
(selectionChange)="onStepChange($event)" [selectedIndex]="currentStep">
<!-- Application Name Step -->
<mat-step [completed]="stepsCompleted.applicationDetail" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Application Name</ng-template>
<app-application [isEditMode]="isEditMode" (applicationIdCreated)="onApplicationDetailCreated($event)">
</app-application>
</mat-step>
<!-- Holder Selection Step -->
<mat-step [completed]="stepsCompleted.holderSelection" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Holder Selection</ng-template>
<app-holder (completed)="onHolderSelectionSaved($event)">
</app-holder>
</mat-step>
<!-- Goods Section Step -->
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Goods Section</ng-template>
<app-goods (completed)="onGoodsSectionSaved($event)">
</app-goods>
</mat-step>
<!-- Travel Plan Step -->
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend'"
[completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Travel Plan</ng-template>
<!-- <app-travel-plan (completed)="onTravelPlanSaved($event)">
</app-travel-plan> -->
</mat-step>
<!-- Insurance Step -->
<mat-step [completed]="stepsCompleted.insurance" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Insurance</ng-template>
<!-- <app-insurance (completed)="onInsuranceSaved($event)">
</app-insurance> -->
</mat-step>
<!-- Shipping Step -->
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping</ng-template>
<!-- <app-shipping (completed)="onShippingSaved($event)">
</app-shipping> -->
</mat-step>
<!-- Delivery Method Step -->
<mat-step [completed]="stepsCompleted.deliveryMethod" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Delivery Method</ng-template>
<!-- <app-delivery-method (completed)="onDeliveryMethodSaved($event)">
</app-delivery-method> -->
</mat-step>
<!-- Payment Step -->
<mat-step [completed]="stepsCompleted.payment" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Payment</ng-template>
<!-- <app-payment (completed)="onPaymentSaved($event)">
</app-payment> -->
</mat-step>
</mat-stepper>
</div>

View File

@ -0,0 +1,51 @@
.client-carnet-container {
// padding: 24px;
// max-width: 1200px;
// margin: 0 auto;
.questions-section {
// padding: 20px;
// border-radius: 8px;
// margin-bottom: 24px;
// h2 {
// margin-bottom: 24px;
// color: #3f51b5;
// }
.question-row {
margin-bottom: 1rem;
padding: 1rem;
background: #ffff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
mat-radio-group {
display: flex;
flex-direction: column;
gap: 8px;
// mat-label {
// margin-bottom: 8px;
// font-weight: 500;
// }
mat-radio-button {
margin-right: 1rem;
}
}
}
.actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
}
}
.actions {
margin-top: 24px;
display: flex;
justify-content: flex-start;
}
}

View File

@ -0,0 +1,102 @@
import { Component, inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { ApplicationComponent } from "../application/application.component";
import { HolderComponent } from '../holder/holder.component';
import { GoodsComponent } from '../goods/goods.component';
@Component({
selector: 'app-add-carnet',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent],
templateUrl: './add-carnet.component.html',
styleUrl: './add-carnet.component.scss'
})
export class AddCarnetComponent {
currentStep = 0;
questionsCompleted = false;
isLinear = true;
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = null;
isEditMode = false;
applicationid: number = 0;
private fb = inject(FormBuilder);
// Form group for initial questions
questionsForm: FormGroup = this.fb.group({
newCarnet: [null, Validators.required],
additionalSets: [false],
duplicateCarnet: [false],
extendCarnet: [false]
});
// Track completion of each step
stepsCompleted = {
applicationDetail: false,
holderSelection: false,
goodsSection: false,
travelPlan: false,
insurance: false,
shipping: false,
deliveryMethod: false,
payment: false
};
onQuestionsSubmit(): void {
const answers = this.questionsForm.value;
if (answers.newCarnet) {
this.applicationType = 'new';
} else if (answers.additionalSets) {
this.applicationType = 'additional';
} else if (answers.duplicateCarnet) {
this.applicationType = 'duplicate';
} else if (answers.extendCarnet) {
this.applicationType = 'extend';
}
// Move to the first step after questions
this.questionsCompleted = true;
this.currentStep = 0;
}
onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex;
}
onApplicationDetailCreated(applicationid: number): void {
this.applicationid = applicationid;
this.stepsCompleted.applicationDetail = true;
this.isLinear = false; // Disable linear mode after application detail is created
}
onHolderSelectionSaved(completed: boolean): void {
this.stepsCompleted.holderSelection = completed;
}
onGoodsSectionSaved(completed: boolean): void {
this.stepsCompleted.goodsSection = completed;
}
onTravelPlanSaved(completed: boolean): void {
this.stepsCompleted.travelPlan = completed;
}
onInsuranceSaved(completed: boolean): void {
this.stepsCompleted.insurance = completed;
}
onShippingSaved(completed: boolean): void {
this.stepsCompleted.shipping = completed;
}
onDeliveryMethodSaved(completed: boolean): void {
this.stepsCompleted.deliveryMethod = completed;
}
onPaymentSaved(completed: boolean): void {
this.stepsCompleted.payment = completed;
}
}

View File

@ -0,0 +1,32 @@
<div class="application-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]="applicationDetailsForm" class="details-form" *ngIf="!isLoading"
(ngSubmit)="saveApplicationDetails()">
<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>
</div>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit"
[disabled]="applicationDetailsForm.invalid || disableSaveButton ">
Save
</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,98 @@
.application-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) {
.application-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,115 @@
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ApplicationDetailService } from '../../core/services/carnet/application-detail.service';
import { NotificationService } from '../../core/services/common/notification.service';
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
import { ApplicationDetail } from '../../core/models/carnet/application-detail';
import { Subject } from 'rxjs';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-application',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
templateUrl: './application.component.html',
styleUrl: './application.component.scss'
})
export class ApplicationComponent {
@Input() isEditMode = false;
@Input() applicationid: number = 0;
@Output() applicationIdCreated = new EventEmitter<number>();
applicationDetailsForm: FormGroup;
isLoading = false;
disableSaveButton = false;
private destroy$ = new Subject<void>();
private fb = inject(FormBuilder);
private applicationDetailService = inject(ApplicationDetailService);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
constructor() {
this.applicationDetailsForm = this.createForm();
}
ngOnInit(): void {
// this.spidCreated.emit(this.spid?.toString());
// Patch edit form data
if (this.applicationid > 0) {
this.isLoading = true;
this.applicationDetailService.getApplicationDetailsById(this.applicationid).subscribe({
next: (applicationDetail: ApplicationDetail) => {
if (applicationDetail?.applicationId > 0) {
this.patchFormData(applicationDetail);
}
this.isLoading = false;
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load application details');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading application details:', error);
}
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
createForm(): FormGroup {
return this.fb.group({
name: ['', [Validators.required, Validators.maxLength(100)]],
});
}
patchFormData(data: ApplicationDetail): void {
this.applicationDetailsForm.patchValue({
name: data.name,
});
if (this.isEditMode) {
this.applicationDetailsForm.get('name')?.disable();
this.disableSaveButton = true;
}
}
// Convenience getter for easy access to form fields
get f() {
return this.applicationDetailsForm.controls;
}
saveApplicationDetails(): void {
if (this.applicationDetailsForm.invalid) {
this.applicationDetailsForm.markAllAsTouched();
return;
}
const applicationDetailData: ApplicationDetail = this.applicationDetailsForm.value;
if (!this.isEditMode && this.applicationid == 0) {
this.isLoading = true;
this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({
next: (applicationData: any) => {
this.notificationService.showSuccess(`Application details added successfully`);
this.applicationIdCreated.emit(applicationData.P_HEADERID);
this.applicationDetailsForm.get('name')?.disable();
this.disableSaveButton = true;
this.isLoading = false;
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Failed to add application details`);
this.notificationService.showError(errorMessage);
console.error('Error saving application details:', error);
this.isLoading = false;
}
});
}
}
}

View File

@ -0,0 +1 @@
<p>delivery works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-delivery',
imports: [],
templateUrl: './delivery.component.html',
styleUrl: './delivery.component.scss'
})
export class DeliveryComponent {
}

View File

@ -0,0 +1 @@
<p>edit-carnet works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-edit-carnet',
imports: [],
templateUrl: './edit-carnet.component.html',
styleUrl: './edit-carnet.component.scss'
})
export class EditCarnetComponent {
}

View File

@ -0,0 +1 @@
<p>goods works!</p>

View File

@ -0,0 +1,14 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-goods',
imports: [AngularMaterialModule, CommonModule],
templateUrl: './goods.component.html',
styleUrl: './goods.component.scss'
})
export class GoodsComponent {
@Input() applicationid: number = 0;
@Output() completed = new EventEmitter<boolean>();
}

View File

@ -0,0 +1 @@
<p>holder works!</p>

View File

@ -0,0 +1,12 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-holder',
imports: [],
templateUrl: './holder.component.html',
styleUrl: './holder.component.scss'
})
export class HolderComponent {
@Input() applicationid: number = 0;
@Output() completed = new EventEmitter<boolean>();
}

View File

@ -0,0 +1 @@
<p>insurance works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-insurance',
imports: [],
templateUrl: './insurance.component.html',
styleUrl: './insurance.component.scss'
})
export class InsuranceComponent {
}

View File

@ -0,0 +1 @@
<p>payment works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-payment',
imports: [],
templateUrl: './payment.component.html',
styleUrl: './payment.component.scss'
})
export class PaymentComponent {
}

View File

@ -0,0 +1 @@
<p>shipping works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-shipping',
imports: [],
templateUrl: './shipping.component.html',
styleUrl: './shipping.component.scss'
})
export class ShippingComponent {
}

View File

@ -0,0 +1 @@
<p>travel-plan works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-travel-plan',
imports: [],
templateUrl: './travel-plan.component.html',
styleUrl: './travel-plan.component.scss'
})
export class TravelPlanComponent {
}

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,39 @@
<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 (click)="navigateTo('usersettings')">User Settings</button>
<!--
<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,65 @@
import { Component, effect, inject, OnInit } from '@angular/core';
import { UserService } from '../../core/services/common/user.service';
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 { AuthService } from '../../core/services/common/auth.service';
import { ThemeService } from '../../core/services/theme.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 = {};
private userService = inject(UserService);
private authService = inject(AuthService);
private navigationService = inject(NavigationService);
private themeService = inject(ThemeService);
constructor(
) {
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.authService.logout();
this.showProfileMenu = false;
}
navigateTo(route: string): void {
this.navigationService.navigate([route]);
this.showProfileMenu = false;
}
}

View File

@ -0,0 +1,111 @@
// auth.interceptor.ts
import { inject, Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
import { AuthService } from '../services/common/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
private requestQueue: { request: HttpRequest<any>, next: HttpHandler }[] = [];
private authService = inject(AuthService);
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add withCredentials to all requests
request = request.clone({
withCredentials: true
});
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Handle 401 Unauthorized responses
if (error.url?.includes('refresh-tokens')) {
return throwError(() => error);
}
return this.handle401Error(request, next);
} else {
return throwError(() => error);
}
})
);
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
// Retry all queued requests with new token
this.retryQueuedRequests();
// Retry the original request
request = request.clone({
withCredentials: true
});
return next.handle(request);
}),
catchError((err) => {
this.isRefreshing = false;
this.authService.logout();
return throwError(() => err);
})
);
} else {
// If token refresh is already in progress, add to queue
return this.addRequestToQueue(request, next);
}
}
private addRequestToQueue(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return new Observable<HttpEvent<any>>(observer => {
const subscription = this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1)
).subscribe(token => {
// Remove from queue
const index = this.requestQueue.findIndex(item => item.request === request);
if (index > -1) {
this.requestQueue.splice(index, 1);
}
// Retry request with new token
next.handle(request).subscribe({
next: event => observer.next(event),
error: err => observer.error(err),
complete: () => observer.complete()
});
});
// Add to queue
this.requestQueue.push({ request, next });
});
}
private retryQueuedRequests(): void {
// Process all queued requests
while (this.requestQueue.length > 0) {
let { request, next } = this.requestQueue.shift()!;
request = request.clone({
withCredentials: true
});
next.handle(request).subscribe();
}
}
}

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,6 @@
export interface ApplicationDetail {
clientid: number;
spid: number;
applicationId: number;
name: string;
}

View File

@ -0,0 +1,2 @@
export interface Delivery {
}

View File

@ -0,0 +1,2 @@
export interface Goods {
}

View File

@ -0,0 +1,2 @@
export interface Holder {
}

View File

@ -0,0 +1,2 @@
export interface Insurance {
}

View File

@ -0,0 +1,2 @@
export interface Payment {
}

View File

@ -0,0 +1,2 @@
export interface Shipping {
}

View File

@ -0,0 +1,2 @@
export interface TravelPlan {
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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,19 @@
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;
clientid: number;
urlKey: string;
logoName: string;
themeName: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,51 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { UserService } from './user.service';
import { StorageService } from './storage.service';
import { Router } from '@angular/router';
import { ThemeService } from '../theme.service';
import { NotificationService } from './notification.service';
import { ApiErrorHandlerService } from './api-error-handler.service';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = environment.apiUrl;
private http = inject(HttpClient);
private userService = inject(UserService);
private storageService = inject(StorageService);
private router = inject(Router);
private themeService = inject(ThemeService);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
login(username: string, password: string): Observable<any> {
return this.http.post(`${this.apiUrl}/login`, { p_emailaddr: username, p_password: password });
}
refreshToken(): Observable<any> {
return this.http.get(`${this.apiUrl}/refresh-tokens`, {});
}
logout(): void {
if (this.userService.isLoggedIn()) {
this.http.post(`${this.apiUrl}/logout`, {}).subscribe({
next: (response) => {
this.userService.clearUser();
this.storageService.clear();
this.router.navigate(['/login']);
this.themeService.setTheme('default');;
}
, error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, `Logout failed`);
this.notificationService.showError(errorMessage);
console.error('Logout failed:', error);
}
});
}
}
}

View File

@ -0,0 +1,159 @@
import { HttpClient } from '@angular/common/http';
import { inject, 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;
private http = inject(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 { inject, Injectable } from '@angular/core';
import { CookieService as NgxCookieService } from 'ngx-cookie-service';
@Injectable({
providedIn: 'root'
})
export class CookieHelperService {
private ngxCookieService = inject(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 { inject, 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';
private router = inject(Router);
private location = inject(Location);
private storageService = inject(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,32 @@
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private snackBar = inject(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,49 @@
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);
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,158 @@
import { afterNextRender, inject, 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;
private clientid: number = 0;
userDetailsSignal = signal<User>({});
private readonly USER_EMAIL_KEY = 'CurrentUserEmail';
private readonly USER_DETAILS_KEY = 'CurrentUserData';
private userLoggedInSubject = new BehaviorSubject<boolean>(false);
private http = inject(HttpClient);
private storageService = inject(StorageService);
constructor() {
afterNextRender(() => {
// Initialize user details from storage if available
const userDetails = this.getUserDetails();
if (userDetails) {
this.userDetailsSignal.set(userDetails);
}
// Check if user is logged in
const userEmail = this.getUser();
this.userLoggedInSubject.next(!!userEmail);
});
}
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;
}
getUserClientid(): number {
if (this.clientid === 0) {
const userDetails = this.getUserDetails();
if (userDetails && userDetails.userDetails && userDetails.userDetails.clientid) {
this.clientid = userDetails.userDetails.clientid;
}
}
return this.clientid;
}
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,
clientid: userDetails.CLIENTID,
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,43 @@
import { HttpClient } from '@angular/common/http';
import { inject, 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;
private http = inject(HttpClient);
private userService = inject(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,44 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StyleManagerService {
/**
* Set the stylesheet with the specified key.
*/
setStyle(key: string, href: string) {
getLinkElementForKey(key).setAttribute('href', href);
}
/**
* Remove the stylesheet with the specified key.
*/
removeStyle(key: string) {
const existingLinkElement = getExistingLinkElementByKey(key);
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
}
function getLinkElementForKey(key: string) {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string) {
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
}
function createLinkElementWithKey(key: string) {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
}
function getClassNameForKey(key: string) {
return `style-manager-${key}`;
}

View File

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

View File

@ -0,0 +1,30 @@
import { inject, 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';
private cookieService = inject(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);
}
}

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