Version1
This commit is contained in:
commit
c0500c458b
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
.vscode/tasks.json
vendored
Normal file
42
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# CarnetPortalApp
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.5.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
132
angular.json
Normal file
132
angular.json
Normal file
@ -0,0 +1,132 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"carnet-portal-app": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/carnet-portal-app",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src/styles"
|
||||
]
|
||||
},
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": true,
|
||||
"ssr": {
|
||||
"entry": "src/server.ts"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1500kB",
|
||||
"maximumError": "5MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "carnet-portal-app:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "carnet-portal-app:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src/styles"
|
||||
]
|
||||
},
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"builder": "angular-cli-ghpages:deploy"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "7c8b4b49-3b4a-41d5-97c5-1671d7b09dae"
|
||||
}
|
||||
}
|
||||
15136
package-lock.json
generated
Normal file
15136
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
package.json
Normal file
49
package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "carnet-portal-app",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"serve:ssr:carnet-portal-app": "node dist/carnet-portal-app/server/server.mjs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.2.7",
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/material": "^19.2.7",
|
||||
"@angular/material-moment-adapter": "^19.2.7",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/platform-server": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@angular/ssr": "^19.2.5",
|
||||
"express": "^4.18.2",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.5",
|
||||
"@angular/cli": "^19.2.5",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^18.18.0",
|
||||
"angular-cli-ghpages": "^2.0.3",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/logo-white.png
Normal file
BIN
public/images/logo-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
public/images/logo.jpeg
Normal file
BIN
public/images/logo.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
7
src/app/app.component.html
Normal file
7
src/app/app.component.html
Normal 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>
|
||||
22
src/app/app.component.scss
Normal file
22
src/app/app.component.scss
Normal file
@ -0,0 +1,22 @@
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
position: relative; // Needed for footer positioning
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
padding-bottom: 80px; // Maintain bottom padding on mobile
|
||||
}
|
||||
}
|
||||
39
src/app/app.component.ts
Normal file
39
src/app/app.component.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { SecuredHeaderComponent } from './common/secured-header/secured-header.component';
|
||||
import { UserService } from './core/services/user.service';
|
||||
import { FooterComponent } from './common/footer/footer.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, SecuredHeaderComponent, FooterComponent, CommonModule],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'carnet-portal-app';
|
||||
isUserLoggedIn = false;
|
||||
private userSubscription!: Subscription;
|
||||
|
||||
constructor(private userService: UserService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.isUserLoggedIn = this.userService.isLoggedIn();
|
||||
|
||||
this.userSubscription = this.userService
|
||||
.watchUser()
|
||||
.subscribe(userLoggedIn => {
|
||||
this.isUserLoggedIn = userLoggedIn
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.userSubscription) {
|
||||
this.userSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/app/app.config.server.ts
Normal file
11
src/app/app.config.server.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
||||
import { provideServerRendering } from '@angular/platform-server';
|
||||
import { appConfig } from './app.config';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(),
|
||||
]
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||
21
src/app/app.config.ts
Normal file
21
src/app/app.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
|
||||
import {
|
||||
provideCharts,
|
||||
withDefaultRegisterables,
|
||||
} from 'ng2-charts';
|
||||
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideClientHydration(withEventReplay()),
|
||||
provideHttpClient(),
|
||||
provideCharts(withDefaultRegisterables())]
|
||||
};
|
||||
19
src/app/app.routes.ts
Normal file
19
src/app/app.routes.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { AuthGuard } from './auth/auth.guard';
|
||||
import { AddServiceProviderComponent } from './service-provider/add/add-service-provider.component';
|
||||
import { EditServiceProviderComponent } from './service-provider/edit/edit-service-provider.component';
|
||||
import { NotFoundComponent } from './shared/components/not-found/not-found.component';
|
||||
import { UserSettingsComponent } from './user-settings/user-settings.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'usersettings', component: UserSettingsComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'service-provider/:id', component: EditServiceProviderComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'add-service-provider', component: AddServiceProviderComponent, canActivate: [AuthGuard] },
|
||||
{ path: '404', component: NotFoundComponent },
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: '**', redirectTo: '/404' }
|
||||
];
|
||||
21
src/app/auth/auth.guard.ts
Normal file
21
src/app/auth/auth.guard.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||
import { UserService } from '../core/services/user.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router) { }
|
||||
|
||||
canActivate(
|
||||
next: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): boolean {
|
||||
if (this.userService.isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
13
src/app/common/footer/footer.component.html
Normal file
13
src/app/common/footer/footer.component.html
Normal 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">
|
||||
© {{ currentYear }} USCIB Carnet Portal. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
58
src/app/common/footer/footer.component.scss
Normal file
58
src/app/common/footer/footer.component.scss
Normal file
@ -0,0 +1,58 @@
|
||||
@import 'colors';
|
||||
|
||||
.footer {
|
||||
background-color: $background-header;
|
||||
padding: 8px;
|
||||
border-top: 1px solid $divider-color;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba($background-header, 0.9);
|
||||
|
||||
/* Fixed footer styles */
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
/* Ensure footer stays above content */
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
a {
|
||||
color: $text-secondary;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: $text-secondary;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.footer {
|
||||
padding: 16px;
|
||||
|
||||
.footer-links {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/app/common/footer/footer.component.ts
Normal file
12
src/app/common/footer/footer.component.ts
Normal 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();
|
||||
}
|
||||
35
src/app/common/secured-header/secured-header.component.html
Normal file
35
src/app/common/secured-header/secured-header.component.html
Normal file
@ -0,0 +1,35 @@
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<img src="images/logo-white.png" alt="App Logo" class="logo">
|
||||
<span class="app-title">USCIB Carnet Portal</span>
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu">
|
||||
<a class="nav-link" (click)="navigateTo('/home')">Home</a>
|
||||
<a class="nav-link" (click)="navigateTo('/manageusers')">Users</a>
|
||||
<a class="nav-link" (click)="navigateTo('/regions')">Regions</a>
|
||||
|
||||
<div class="profile-container">
|
||||
<button mat-icon-button (click)="toggleProfileMenu()" class="profile-button">
|
||||
<mat-icon>account_circle</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="profile-menu" *ngIf="showProfileMenu">
|
||||
<div class="profile-info">
|
||||
<mat-icon>email</mat-icon>
|
||||
<span>{{ userEmail }}</span>
|
||||
</div>
|
||||
|
||||
<button mat-menu-item (click)="navigateTo('/usersettings')">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>User Settings</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="logout()">
|
||||
<mat-icon>logout</mat-icon>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
110
src/app/common/secured-header/secured-header.component.scss
Normal file
110
src/app/common/secured-header/secured-header.component.scss
Normal file
@ -0,0 +1,110 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 64px;
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
@include box-shadow(2);
|
||||
position: relative;
|
||||
z-index: 100; // Higher than footer's z-index
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
|
||||
.nav-link {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
position: relative;
|
||||
|
||||
.profile-button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.profile-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 48px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
@include box-shadow(3);
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
z-index: 101;
|
||||
|
||||
.profile-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid $divider-color;
|
||||
color: $text-primary;
|
||||
|
||||
mat-icon {
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
color: $text-primary;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
mat-icon {
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar {
|
||||
padding: 0 12px;
|
||||
|
||||
.app-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/app/common/secured-header/secured-header.component.ts
Normal file
40
src/app/common/secured-header/secured-header.component.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { UserService } from '../../core/services/user.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-secured-header',
|
||||
imports: [AngularMaterialModule, CommonModule],
|
||||
templateUrl: './secured-header.component.html',
|
||||
styleUrl: './secured-header.component.scss'
|
||||
})
|
||||
export class SecuredHeaderComponent implements OnInit {
|
||||
userEmail: string = '';
|
||||
showProfileMenu: boolean = false;
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userEmail = this.userService.getSafeUser();
|
||||
}
|
||||
|
||||
toggleProfileMenu(): void {
|
||||
this.showProfileMenu = !this.showProfileMenu;
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.userService.clearUser();
|
||||
this.router.navigate(['/login']);
|
||||
this.showProfileMenu = false;
|
||||
}
|
||||
|
||||
navigateTo(route: string): void {
|
||||
this.router.navigate([route]);
|
||||
this.showProfileMenu = false;
|
||||
}
|
||||
}
|
||||
5
src/app/core/models/bond-surety.ts
Normal file
5
src/app/core/models/bond-surety.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface BondSurety {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/cargo-policy.ts
Normal file
5
src/app/core/models/cargo-policy.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CargoPolicy {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/cargo-surety.ts
Normal file
5
src/app/core/models/cargo-surety.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CargoSurety {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
6
src/app/core/models/carnet-status.ts
Normal file
6
src/app/core/models/carnet-status.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface CarnetStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
color: string;
|
||||
}
|
||||
5
src/app/core/models/country.ts
Normal file
5
src/app/core/models/country.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Country {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/delivery-type.ts
Normal file
5
src/app/core/models/delivery-type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface DeliveryType {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/fee-type.ts
Normal file
5
src/app/core/models/fee-type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface FeeType {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/region.ts
Normal file
5
src/app/core/models/region.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Region {
|
||||
id: number,
|
||||
region: string;
|
||||
regionname: string;
|
||||
}
|
||||
16
src/app/core/models/service-provider/basic-detail.ts
Normal file
16
src/app/core/models/service-provider/basic-detail.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface BasicDetail {
|
||||
cargoSurety: string;
|
||||
cargoPolicyNo: string;
|
||||
bondSurety: string;
|
||||
spid: number;
|
||||
companyName: string;
|
||||
lookupCode?: string;
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
country: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
issuingRegion: string;
|
||||
replacementRegion: string;
|
||||
}
|
||||
10
src/app/core/models/service-provider/basic-fee.ts
Normal file
10
src/app/core/models/service-provider/basic-fee.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface BasicFee {
|
||||
basicFeeId: number;
|
||||
startCarnetValue: number;
|
||||
endCarnetValue: number | null;
|
||||
fees: number;
|
||||
effectiveDate: Date;
|
||||
spid?: number;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
9
src/app/core/models/service-provider/carnet-fee.ts
Normal file
9
src/app/core/models/service-provider/carnet-fee.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface CarnetFee {
|
||||
feeCommissionId: number;
|
||||
feeType: string;
|
||||
commissionRate: number;
|
||||
effectiveDate: Date;
|
||||
spid: number;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
8
src/app/core/models/service-provider/carnet-sequence.ts
Normal file
8
src/app/core/models/service-provider/carnet-sequence.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface CarnetSequence {
|
||||
spid: number;
|
||||
carnetType: string;
|
||||
region: number;
|
||||
startNumber: number;
|
||||
endNumber: number;
|
||||
lastNumber: number;
|
||||
}
|
||||
19
src/app/core/models/service-provider/contact.ts
Normal file
19
src/app/core/models/service-provider/contact.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface Contact {
|
||||
spContactId: number;
|
||||
serviceProviderId: number;
|
||||
defaultContact: boolean;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleInitial: string;
|
||||
title: string;
|
||||
phone: string;
|
||||
mobile: string;
|
||||
fax: string;
|
||||
email: string;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
lastUpdatedBy?: string | null;
|
||||
lastUpdatedDate?: Date | null;
|
||||
isInactive?: boolean | null;
|
||||
inactivatedDate?: Date | null;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
export interface ContinuationSheetFee {
|
||||
id: number;
|
||||
spid: number;
|
||||
rate: number;
|
||||
effectiveDate: Date;
|
||||
customerType: string;
|
||||
carnetType: string;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
12
src/app/core/models/service-provider/counterfoil-fee.ts
Normal file
12
src/app/core/models/service-provider/counterfoil-fee.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface CounterfoilFee {
|
||||
id: number,
|
||||
spid: number,
|
||||
startSets: number,
|
||||
endSets: number,
|
||||
customerType: string,
|
||||
carnetType: string,
|
||||
effectiveDate: Date,
|
||||
rate: number,
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
12
src/app/core/models/service-provider/expedited-fee.ts
Normal file
12
src/app/core/models/service-provider/expedited-fee.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface ExpeditedFee {
|
||||
expeditedFeeId: number;
|
||||
customerType: string;
|
||||
deliveryType: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
timeZone: string;
|
||||
fee: number;
|
||||
effectiveDate: Date;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
12
src/app/core/models/service-provider/security-deposit.ts
Normal file
12
src/app/core/models/service-provider/security-deposit.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface SecurityDeposit {
|
||||
securityDepositId: number;
|
||||
holderType: string;
|
||||
uscibMember: boolean;
|
||||
specialCommodity: string;
|
||||
specialCountry: string;
|
||||
rate: number;
|
||||
effectiveDate: Date;
|
||||
spid: number;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
5
src/app/core/models/state.ts
Normal file
5
src/app/core/models/state.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface State {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/timezone.ts
Normal file
5
src/app/core/models/timezone.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface TimeZone {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
8
src/app/core/models/user-preference.ts
Normal file
8
src/app/core/models/user-preference.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface UserPreferences {
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// Default preferences
|
||||
export const DEFAULT_USER_PREFERENCES: UserPreferences = {
|
||||
pageSize: 5
|
||||
};
|
||||
63
src/app/core/services/api-error-handler.service.ts
Normal file
63
src/app/core/services/api-error-handler.service.ts
Normal 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;;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/app/core/services/auth.service.ts
Normal file
17
src/app/core/services/auth.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
login(username: string, password: string): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/login`, { p_emailaddr: username, p_password: password });
|
||||
}
|
||||
}
|
||||
86
src/app/core/services/basic-detail.service.ts
Normal file
86
src/app/core/services/basic-detail.service.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { BasicDetail } from '../models/service-provider/basic-detail';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BasicDetailService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService) { }
|
||||
|
||||
getBasicDetailsById(id: number): BasicDetail | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetSelectedServiceprovider?p_spid=${id}`).pipe(
|
||||
filter(response => response.length > 0),
|
||||
map(response => this.mapToBasicDetail(response?.[0])));
|
||||
}
|
||||
|
||||
private mapToBasicDetail(basicDetails: any): BasicDetail {
|
||||
return {
|
||||
spid: basicDetails.SPID,
|
||||
companyName: basicDetails.NAMEOF,
|
||||
lookupCode: basicDetails.LOOKUPCODE,
|
||||
address1: basicDetails.ADDRESS1,
|
||||
address2: basicDetails.ADDRESS2,
|
||||
city: basicDetails.CITY,
|
||||
state: basicDetails.STATE,
|
||||
country: basicDetails.COUNTRY,
|
||||
issuingRegion: basicDetails.ISSUINGREGION,
|
||||
replacementRegion: basicDetails.REPLACEMENTREGION,
|
||||
zip: basicDetails.ZIP,
|
||||
cargoSurety: basicDetails.CARGOSURETY,
|
||||
cargoPolicyNo: basicDetails.CARGOPOLICYNO,
|
||||
bondSurety: basicDetails.BONDSURETY
|
||||
};
|
||||
}
|
||||
|
||||
createBasicDetails(data: BasicDetail): Observable<any> {
|
||||
|
||||
const basicDetails = {
|
||||
p_name: data.companyName,
|
||||
p_lookupcode: data.lookupCode,
|
||||
p_address1: data.address1,
|
||||
p_address2: data.address2,
|
||||
p_city: data.city,
|
||||
p_state: data.state,
|
||||
p_zip: data.zip,
|
||||
p_country: data.country,
|
||||
p_issuingregion: data.issuingRegion,
|
||||
p_replacementregion: data.replacementRegion,
|
||||
p_bondsurety: data.bondSurety,
|
||||
p_cargopolicyno: data.cargoPolicyNo,
|
||||
p_cargosurety: data.cargoSurety,
|
||||
p_user_id: this.userService.getUser(),
|
||||
}
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/InsertNewServiceProvider`, basicDetails);
|
||||
}
|
||||
|
||||
updateBasicDetails(id: number, data: BasicDetail): Observable<any> {
|
||||
|
||||
const basicDetails = {
|
||||
p_spid: id,
|
||||
p_name: data.companyName,
|
||||
p_lookupcode: data.lookupCode,
|
||||
p_address1: data.address1,
|
||||
p_address2: data.address2,
|
||||
p_city: data.city,
|
||||
p_state: data.state,
|
||||
p_zip: data.zip,
|
||||
p_country: data.country,
|
||||
p_issuingregion: data.issuingRegion,
|
||||
p_replacementregion: data.replacementRegion,
|
||||
p_bondsurety: data.bondSurety,
|
||||
p_cargopolicyno: data.cargoPolicyNo,
|
||||
p_cargosurety: data.cargoSurety,
|
||||
p_user_id: this.userService.getUser(),
|
||||
}
|
||||
|
||||
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateServiceProvider`, basicDetails);
|
||||
}
|
||||
}
|
||||
67
src/app/core/services/basic-fee.service.ts
Normal file
67
src/app/core/services/basic-fee.service.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from './user.service';
|
||||
import { BasicFee } from '../models/service-provider/basic-fee';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BasicFeeService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
|
||||
|
||||
getBasicFees(spid: number): Observable<BasicFee[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetBasicFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
|
||||
map(response => this.mapToBasicFees(response)));
|
||||
}
|
||||
|
||||
private mapToBasicFees(data: any[]): BasicFee[] {
|
||||
return data.map(item => ({
|
||||
basicFeeId: item.BASICFEESETUPID,
|
||||
startCarnetValue: item.STARTCARNETVALUE,
|
||||
endCarnetValue: item.ENDCARNETVALUE,
|
||||
fees: item.FEES,
|
||||
effectiveDate: item.EFFDATE,
|
||||
createdBy: item.CREATEDBY || null,
|
||||
dateCreated: item.DATECREATED || null
|
||||
}));
|
||||
}
|
||||
|
||||
createBasicFee(spid: number, fee: BasicFee): Observable<any> {
|
||||
const payload = {
|
||||
P_SPID: spid,
|
||||
P_STARTCARNETVALUE: fee.startCarnetValue,
|
||||
P_ENDCARNETVALUE: fee.endCarnetValue,
|
||||
P_FEES: fee.fees,
|
||||
P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate),
|
||||
P_USERID: this.userService.getUser()
|
||||
};
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateBasicFee`, payload);
|
||||
}
|
||||
updateBasicFee(feeId: number, fee: BasicFee): Observable<any> {
|
||||
const payload = {
|
||||
P_BASICFEESETUPID: feeId,
|
||||
P_STARTCARNETVALUE: fee.startCarnetValue,
|
||||
P_ENDCARNETVALUE: fee.endCarnetValue,
|
||||
P_FEES: fee.fees,
|
||||
P_EFFDATE: this.commonService.formatUSDate(fee.effectiveDate),
|
||||
P_USERID: this.userService.getUser()
|
||||
};
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateBasicFee`, payload);
|
||||
}
|
||||
|
||||
// deleteBasicFee(feeId: number): Observable<any> {
|
||||
// return this.http.delete<any>(`${this.apiUrl}/${this.apiDb}/DeleteBasicFee/${feeId}`, {
|
||||
// params: {
|
||||
// p_userid: this.userService.getUser()
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
62
src/app/core/services/carnet-fee.service.ts
Normal file
62
src/app/core/services/carnet-fee.service.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CarnetFee } from '../models/service-provider/carnet-fee';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CarnetFeeService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
|
||||
|
||||
getFeeCommissions(spid: number): Observable<CarnetFee[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetFeeComm?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
|
||||
map(response => this.mapToFeeCommissions(response)));
|
||||
}
|
||||
|
||||
private mapToFeeCommissions(data: any[]): CarnetFee[] {
|
||||
return data.map(item => ({
|
||||
feeCommissionId: item.FEECOMMID,
|
||||
feeType: item.FEETYPEID,
|
||||
description: item.DESCRIPTION,
|
||||
commissionRate: item.COMMRATE,
|
||||
effectiveDate: item.EFFDATE,
|
||||
spid: item.SPID,
|
||||
createdBy: item.CREATEDBY || null,
|
||||
dateCreated: item.DATECREATED || null
|
||||
}));
|
||||
}
|
||||
|
||||
createFeeCommission(spid: number, data: CarnetFee): Observable<any> {
|
||||
const feeCommission = {
|
||||
P_SPID: spid,
|
||||
P_PARAMID: data.feeType,
|
||||
P_COMMRATE: data.commissionRate,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_USERID: this.userService.getUser()
|
||||
};
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateFeeComm`, feeCommission);
|
||||
}
|
||||
|
||||
updateFeeCommission(id: number, data: CarnetFee): Observable<any> {
|
||||
const feeCommission = {
|
||||
P_FEECOMMID: id,
|
||||
P_RATE: data.commissionRate,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_USERID: this.userService.getUser()
|
||||
};
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateFeeComm`, feeCommission);
|
||||
}
|
||||
|
||||
// deleteFeeCommission(id: number): Observable<void> {
|
||||
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/DeleteFeeCommission/${id}`);
|
||||
// }
|
||||
}
|
||||
45
src/app/core/services/carnet-sequence.service.ts
Normal file
45
src/app/core/services/carnet-sequence.service.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { UserService } from './user.service';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { CarnetSequence } from '../models/service-provider/carnet-sequence';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CarnetSequenceService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService) { }
|
||||
|
||||
getCarnetSequenceById(id: number): Observable<CarnetSequence[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCarnetSequence?p_spid=${id}`).pipe(
|
||||
map(response => this.mapToCarnetSequence(response)));
|
||||
}
|
||||
|
||||
private mapToCarnetSequence(data: any[]): CarnetSequence[] {
|
||||
return data.map(item => ({
|
||||
spid: item.SPID,
|
||||
region: item.REGIONID,
|
||||
carnetType: item.CARNETTYPE,
|
||||
startNumber: item.STARTNUMBER,
|
||||
endNumber: item.ENDNUMBER,
|
||||
lastNumber: item.LASTNUMBER
|
||||
}));
|
||||
}
|
||||
|
||||
createCarnetSequence(data: CarnetSequence): Observable<any> {
|
||||
|
||||
const carnetSequence = {
|
||||
p_spid: data.spid,
|
||||
p_regionid: data.region,
|
||||
p_startnumber: data.startNumber,
|
||||
p_endnumber: data.endNumber,
|
||||
p_carnettype: data.carnetType
|
||||
}
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateCarnetSequence`, carnetSequence);
|
||||
}
|
||||
}
|
||||
159
src/app/core/services/common.service.ts
Normal file
159
src/app/core/services/common.service.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { catchError, map, Observable, of } from 'rxjs';
|
||||
import { Region } from '../models/region';
|
||||
import { Country } from '../models/country';
|
||||
import { State } from '../models/state';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { DeliveryType } from '../models/delivery-type';
|
||||
import { FeeType } from '../models/fee-type';
|
||||
import { TimeZone } from '../models/timezone';
|
||||
import { BondSurety } from '../models/bond-surety';
|
||||
import { CargoPolicy } from '../models/cargo-policy';
|
||||
import { CargoSurety } from '../models/cargo-surety';
|
||||
import { CarnetStatus } from '../models/carnet-status';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CommonService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getCountries(spid: number = 0): Observable<Country[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=002&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getStates(country: string, spid: number = 0): Observable<State[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=001&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
countryValue: item.ADDLPARAMVALUE1,
|
||||
})).filter((state) => state.countryValue === country) // Filter by country value
|
||||
),
|
||||
catchError((error) => {
|
||||
console.error('Error fetching states:', error);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getRegions(): Observable<Region[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
id: item.REGIONID,
|
||||
region: item.REGION,
|
||||
regionname: item.REGIONNAME
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getDeliveryTypes(spid: number = 0): Observable<DeliveryType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=006&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getTimezones(spid: number = 0): Observable<TimeZone[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=010&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getFeeTypes(spid: number = 0): Observable<FeeType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=009&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getBondSuretys(spid: number = 0): Observable<BondSurety[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=003&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getCargoPolicies(spid: number = 0): Observable<CargoPolicy[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=004&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getCargoSuretys(spid: number = 0): Observable<CargoSurety[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=005&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getCarnetStatuses(spid: number = 0): Observable<CarnetStatus[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=011&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
color: item.ADDLPARAMVALUE1,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
formatUSDate(datetime: Date): string {
|
||||
const date = new Date(datetime);
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const year = date.getUTCFullYear();
|
||||
|
||||
return `${month}/${day}/${year}`;
|
||||
}
|
||||
}
|
||||
85
src/app/core/services/contact.service.ts
Normal file
85
src/app/core/services/contact.service.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { Contact } from '../models/service-provider/contact';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService) { }
|
||||
|
||||
getContactsById(id: number): Observable<Contact[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetSPAllContacts?p_SPid=${id}`).pipe(
|
||||
map(response => this.mapToContacts(response)));
|
||||
}
|
||||
|
||||
private mapToContacts(data: any[]): Contact[] {
|
||||
return data.map(contact => ({
|
||||
spContactId: contact.SPCONTACTID,
|
||||
serviceProviderId: contact.SPID,
|
||||
defaultContact: contact.DEFCONTACTFLAG === 'Y',
|
||||
firstName: contact.FIRSTNAME,
|
||||
lastName: contact.LASTNAME,
|
||||
title: contact.TITLE,
|
||||
phone: contact.PHONENO,
|
||||
mobile: contact.MOBILENO,
|
||||
fax: contact.FAXNO || null,
|
||||
email: contact.EMAILADDRESS,
|
||||
middleInitial: contact.MIDDLEINITIAL || null,
|
||||
createdBy: contact.CREATEDBY || null,
|
||||
dateCreated: contact.DATECREATED || null,
|
||||
lastUpdatedBy: contact.LASTUPDATEDBY || null,
|
||||
lastUpdatedDate: contact.LASTUPDATEDDATE || null,
|
||||
isInactive: contact.INACTIVEFLAG === 'Y' || false,
|
||||
inactivatedDate: contact.INACTIVEDATE || null
|
||||
}));
|
||||
}
|
||||
|
||||
createContact(spid: number, data: Contact): Observable<any> {
|
||||
|
||||
const contact = {
|
||||
p_spid: spid,
|
||||
p_defcontactflag: data.defaultContact ? 'Y' : 'N',
|
||||
p_firstname: data.firstName,
|
||||
p_lastname: data.lastName,
|
||||
P_MIDDLEINITIAL: data.middleInitial,
|
||||
p_title: data.title,
|
||||
p_phoneno: data.phone,
|
||||
p_mobileno: data.mobile,
|
||||
p_faxno: data.fax,
|
||||
p_emailaddress: data.email,
|
||||
p_user_id: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/InsertSPContacts`, contact);
|
||||
}
|
||||
|
||||
updateContact(spContactId: number, data: Contact): Observable<any> {
|
||||
|
||||
const contact = {
|
||||
p_spcontactid: spContactId,
|
||||
p_firstname: data.firstName,
|
||||
p_lastname: data.lastName,
|
||||
P_MIDDLEINITIAL: data.middleInitial,
|
||||
p_title: data.title,
|
||||
p_phoneno: data.phone,
|
||||
p_mobileno: data.mobile,
|
||||
p_faxno: data.fax,
|
||||
p_emailaddress: data.email,
|
||||
p_user_id: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateSPContacts`, contact);
|
||||
}
|
||||
|
||||
deleteContact(spContactId: string): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/InactivateSPContact?p_spcontactid=${spContactId}`, null);
|
||||
}
|
||||
|
||||
}
|
||||
66
src/app/core/services/continuation-sheet-fee.service.ts
Normal file
66
src/app/core/services/continuation-sheet-fee.service.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { ContinuationSheetFee } from '../models/service-provider/continuation-sheet-fee';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContinuationSheetFeeService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
|
||||
|
||||
getContinuationSheets(spid: number): Observable<ContinuationSheetFee[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCsFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
|
||||
map(response => this.mapToContinuationSheetFee(response)));
|
||||
}
|
||||
|
||||
private mapToContinuationSheetFee(data: any[]): ContinuationSheetFee[] {
|
||||
return data.map(item => ({
|
||||
id: item.CSFEESETUPID,
|
||||
spid: item.SPID,
|
||||
customerType: item.CUSTOMERTYPE,
|
||||
carnetType: item.CARNETTYPE,
|
||||
effectiveDate: item.EFFDATE,
|
||||
rate: item.RATE,
|
||||
createdBy: item.CREATEDBY || null,
|
||||
dateCreated: item.DATECREATED || null
|
||||
}));
|
||||
}
|
||||
|
||||
addContinuationSheet(spid: number, data: ContinuationSheetFee): Observable<any> {
|
||||
|
||||
const continuationSheet = {
|
||||
P_SPID: spid,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_CUSTOMERTYPE: data.customerType,
|
||||
P_CARNETTYPE: data.carnetType,
|
||||
P_RATE: data.rate,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateCsFee`, continuationSheet);
|
||||
}
|
||||
|
||||
updateContinuationSheet(id: number, data: ContinuationSheetFee): Observable<any> {
|
||||
|
||||
const continuationSheet = {
|
||||
P_CSFEESETUPID: id,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_RATE: data.rate,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateCsFee`, continuationSheet);
|
||||
}
|
||||
|
||||
// deleteContinuationSheet(id: string): Observable<void> {
|
||||
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
|
||||
// }
|
||||
|
||||
}
|
||||
60
src/app/core/services/cookie.service.ts
Normal file
60
src/app/core/services/cookie.service.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CookieService as NgxCookieService } from 'ngx-cookie-service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CookieHelperService {
|
||||
constructor(private ngxCookieService: NgxCookieService) { }
|
||||
|
||||
/**
|
||||
* Get a value from cookie
|
||||
* @param key Cookie key
|
||||
* @returns Parsed JSON value or null if not found
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
try {
|
||||
const value = this.ngxCookieService.get(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch (error) {
|
||||
console.error('Error parsing cookie value', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in cookie
|
||||
* @param key Cookie key
|
||||
* @param value Value to store (will be stringified)
|
||||
* @param expiresDays Number of days until cookie expires (default 365)
|
||||
*/
|
||||
set(key: string, value: any, expiresDays: number = 365): void {
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + expiresDays);
|
||||
this.ngxCookieService.set(
|
||||
key,
|
||||
JSON.stringify(value),
|
||||
expires,
|
||||
'/',
|
||||
undefined,
|
||||
false,
|
||||
'Lax'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cookie
|
||||
* @param key Cookie key to remove
|
||||
*/
|
||||
remove(key: string): void {
|
||||
this.ngxCookieService.delete(key, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cookie exists
|
||||
* @param key Cookie key to check
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return this.ngxCookieService.check(key);
|
||||
}
|
||||
}
|
||||
69
src/app/core/services/counterfoil-fee.service.ts
Normal file
69
src/app/core/services/counterfoil-fee.service.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CounterfoilFee } from '../models/service-provider/counterfoil-fee';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CounterfoilFeeService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
|
||||
|
||||
getCounterfoils(spid: number): Observable<CounterfoilFee[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCfFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
|
||||
map(response => this.mapToCounterFoilFee(response)));
|
||||
}
|
||||
|
||||
private mapToCounterFoilFee(data: any[]): CounterfoilFee[] {
|
||||
return data.map(item => ({
|
||||
id: item.CFFEESETUPID,
|
||||
spid: item.SPID,
|
||||
startSets: item.STARTSETS,
|
||||
endSets: item.ENDSETS,
|
||||
customerType: item.CUSTOMERTYPE,
|
||||
carnetType: item.CARNETTYPE,
|
||||
effectiveDate: item.EFFDATE,
|
||||
rate: item.RATE,
|
||||
createdBy: item.CREATEDBY || null,
|
||||
dateCreated: item.DATECREATED || null
|
||||
}));
|
||||
}
|
||||
|
||||
addCounterfoil(spid: number, data: CounterfoilFee): Observable<any> {
|
||||
|
||||
const counterfoilFee = {
|
||||
P_SPID: spid,
|
||||
P_STARTSETS: data.startSets,
|
||||
P_ENDSETS: data.endSets,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_CUSTOMERTYPE: data.customerType,
|
||||
P_CARNETTYPE: data.carnetType,
|
||||
P_RATE: data.rate,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateCfFee`, counterfoilFee);
|
||||
}
|
||||
|
||||
updateCounterfoil(id: number, data: CounterfoilFee): Observable<any> {
|
||||
|
||||
const counterfoilFee = {
|
||||
P_CFFEESETUPID: id,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_RATE: data.rate,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}//UpdateCfFee`, counterfoilFee);
|
||||
}
|
||||
|
||||
// deleteCounterfoil(id: string): Observable<void> {
|
||||
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
|
||||
// }
|
||||
}
|
||||
72
src/app/core/services/expedited-fee.service.ts
Normal file
72
src/app/core/services/expedited-fee.service.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ExpeditedFee } from '../models/service-provider/expedited-fee';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from './user.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ExpeditedFeeService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
|
||||
|
||||
getExpeditedFees(spid: number): Observable<ExpeditedFee[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetEfFeeRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
|
||||
map(response => this.mapToExpeditedFee(response)));
|
||||
}
|
||||
|
||||
private mapToExpeditedFee(data: any[]): ExpeditedFee[] {
|
||||
return data.map(item => ({
|
||||
expeditedFeeId: item.EXPFEESETUPID,
|
||||
customerType: item.CUSTOMERTYPE,
|
||||
deliveryType: item.DELIVERYTYPE,
|
||||
startTime: item.STARTTIME,
|
||||
endTime: item.ENDTIME,
|
||||
timeZone: item.TIMEZONE,
|
||||
fee: item.FEES,
|
||||
effectiveDate: item.EFFDATE,
|
||||
spid: item.SPID,
|
||||
createdBy: item.CREATEDBY || null,
|
||||
dateCreated: item.DATECREATED || null
|
||||
}));
|
||||
}
|
||||
|
||||
createExpeditedFee(spid: number, data: ExpeditedFee): Observable<any> {
|
||||
|
||||
const expeditedFee = {
|
||||
P_SPID: spid,
|
||||
P_CUSTOMERTYPE: data.customerType,
|
||||
P_DELIVERYTYPE: data.deliveryType,
|
||||
P_TIMEZONE: data.timeZone,
|
||||
P_STARTTIME: +data.startTime,
|
||||
P_ENDTIME: +data.endTime,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_FEES: data.fee,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateEfFee`, expeditedFee);
|
||||
}
|
||||
|
||||
updateExpeditedFee(id: number, data: ExpeditedFee): Observable<any> {
|
||||
|
||||
const expeditedFee = {
|
||||
P_EFFEESETUPID: id,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_FEES: data.fee,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}//UpdateEfFee`, expeditedFee);
|
||||
}
|
||||
|
||||
// deleteExpeditedFee(id: string): Observable<void> {
|
||||
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
|
||||
// }
|
||||
|
||||
}
|
||||
42
src/app/core/services/home.service.ts
Normal file
42
src/app/core/services/home.service.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HomeService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService) { }
|
||||
|
||||
getCarnetSummaryData(): Observable<any> {
|
||||
const userid = this.userService.getUser();
|
||||
return this.http.get(`${this.apiUrl}/${this.apiDb}/GetCarnetSummaryData/${userid}`);
|
||||
}
|
||||
|
||||
getCarnetDataByStatus(spid: number, carnetStatus: string): Observable<any> {
|
||||
const userid = this.userService.getUser();
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCarnetDetailsbyCarnetStatus/${spid}/${userid}/${carnetStatus}`).pipe(
|
||||
map(response => this.mapToCarnetData(response)));
|
||||
}
|
||||
|
||||
private mapToCarnetData(data: any[]): any[] {
|
||||
return data.map(item => ({
|
||||
applicationName: item.APPLICATIONNAME,
|
||||
holderName: item.HOLDERNAME,
|
||||
carnetNumber: item.CARNETNO,
|
||||
usSets: item.USSETS,
|
||||
foreignSets: item.FOREIGNSETS,
|
||||
transitSets: item.TRANSITSETS,
|
||||
carnetValue: item.CARNETVALUE,
|
||||
issueDate: item.ISSUEDATE || null,
|
||||
expiryDate: item.EXPDATE || null,
|
||||
orderType: item.ORDERTYPE,
|
||||
carnetStatus: item.CARNETSTATUS
|
||||
}));
|
||||
}
|
||||
}
|
||||
30
src/app/core/services/notification.service.ts
Normal file
30
src/app/core/services/notification.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NotificationService {
|
||||
constructor(private snackBar: MatSnackBar) { }
|
||||
|
||||
showSuccess(message: string): void {
|
||||
this.snackBar.open(message, 'Close', {
|
||||
duration: 3000,
|
||||
panelClass: ['success-snackbar']
|
||||
});
|
||||
}
|
||||
|
||||
showError(message: string): void {
|
||||
this.snackBar.open(message, 'Close', {
|
||||
duration: 3000,
|
||||
panelClass: ['error-snackbar']
|
||||
});
|
||||
}
|
||||
|
||||
showWarning(message: string): void {
|
||||
this.snackBar.open(message, 'Close', {
|
||||
duration: 3000,
|
||||
panelClass: ['error-snackbar']
|
||||
});
|
||||
}
|
||||
}
|
||||
69
src/app/core/services/security-deposit.service.ts
Normal file
69
src/app/core/services/security-deposit.service.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from './user.service';
|
||||
import { SecurityDeposit } from '../models/service-provider/security-deposit';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SecurityDepositService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService, private commonService: CommonService) { }
|
||||
|
||||
getSecurityDeposits(spid: number): Observable<SecurityDeposit[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetBondRates?P_SPID=${spid}&P_ACTIVE_INACTIVE=ACTIVE`).pipe(
|
||||
map(response => this.mapToSecurityDeposit(response)));
|
||||
}
|
||||
|
||||
private mapToSecurityDeposit(data: any[]): SecurityDeposit[] {
|
||||
return data.map(item => ({
|
||||
securityDepositId: item.BONDRATESETUPID,
|
||||
holderType: item.HOLDERTYPE,
|
||||
uscibMember: item.USCIBMEMBERFLAG,
|
||||
specialCommodity: item.SPCLCOMMODITY,
|
||||
specialCountry: item.SPCLCOUNTRY,
|
||||
rate: item.RATE,
|
||||
effectiveDate: item.EFFDATE,
|
||||
spid: item.SPID,
|
||||
createdBy: item.CREATEDBY || null,
|
||||
dateCreated: item.DATECREATED || null
|
||||
}));
|
||||
}
|
||||
|
||||
createSecurityDeposit(spid: number, data: SecurityDeposit): Observable<any> {
|
||||
|
||||
const securityDeposit = {
|
||||
P_SPID: spid,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_HOLDERTYPE: data.holderType,
|
||||
P_USCIBMEMBERFLAG: data.uscibMember ? 'Y' : 'N',
|
||||
P_SPCLCOMMODITY: data.specialCommodity,
|
||||
P_SPCLCOUNTRY: data.specialCountry,
|
||||
P_RATE: data.rate,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/CreateBondRate`, securityDeposit);
|
||||
}
|
||||
|
||||
updateSecurityDeposit(id: number, data: SecurityDeposit): Observable<any> {
|
||||
|
||||
const securityDeposit = {
|
||||
P_BONDRATESETUPID: id,
|
||||
P_EFFDATE: this.commonService.formatUSDate(data.effectiveDate),
|
||||
P_RATE: data.rate,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}/UpdateBondRate`, securityDeposit);
|
||||
}
|
||||
|
||||
// deleteSecurityDeposit(id: string): Observable<void> {
|
||||
// return this.http.delete<void>(`${this.apiUrl}/${this.apiDb}/InactivateSPContact/${id}`);
|
||||
// }
|
||||
}
|
||||
32
src/app/core/services/storage.service.ts
Normal file
32
src/app/core/services/storage.service.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService {
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
|
||||
constructor() { }
|
||||
setItem(key: string, value: string): void {
|
||||
sessionStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
return sessionStorage.getItem(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
}
|
||||
44
src/app/core/services/timeformat.service.ts
Normal file
44
src/app/core/services/timeformat.service.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
30
src/app/core/services/user-preference.service.ts
Normal file
30
src/app/core/services/user-preference.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CookieHelperService } from './cookie.service';
|
||||
import { DEFAULT_USER_PREFERENCES, UserPreferences } from '../models/user-preference';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserPreferencesService {
|
||||
private readonly COOKIE_KEY = 'user_preferences';
|
||||
|
||||
constructor(private cookieService: CookieHelperService) { }
|
||||
|
||||
getPreferences(): UserPreferences {
|
||||
const preferences = this.cookieService.get<UserPreferences>(this.COOKIE_KEY);
|
||||
return preferences || { ...DEFAULT_USER_PREFERENCES };
|
||||
}
|
||||
|
||||
savePreferences(preferences: UserPreferences): void {
|
||||
this.cookieService.set(this.COOKIE_KEY, preferences);
|
||||
}
|
||||
|
||||
getUserPrefenceByKey(keyName: string): any {
|
||||
const preferences = this.getPreferences();
|
||||
return preferences[keyName as keyof typeof preferences];
|
||||
}
|
||||
|
||||
resetToDefaults(): void {
|
||||
this.cookieService.remove(this.COOKIE_KEY);
|
||||
}
|
||||
}
|
||||
49
src/app/core/services/user.service.ts
Normal file
49
src/app/core/services/user.service.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StorageService } from './storage.service';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserService {
|
||||
|
||||
private readonly USER_EMAIL_KEY = 'CurrentUserEmail';
|
||||
private userLoggedInSubject = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private storageService: StorageService) { }
|
||||
|
||||
watchUser(): Observable<boolean> {
|
||||
return this.userLoggedInSubject.asObservable();
|
||||
}
|
||||
|
||||
setUser(email: string): void {
|
||||
if (!email) {
|
||||
console.error('Cannot set empty user email');
|
||||
return;
|
||||
}
|
||||
|
||||
this.storageService.setItem(this.USER_EMAIL_KEY, email);
|
||||
this.userLoggedInSubject.next(true);
|
||||
}
|
||||
|
||||
getUser(): string | null {
|
||||
const user = this.storageService.getItem(this.USER_EMAIL_KEY);
|
||||
if (!user) {
|
||||
this.userLoggedInSubject.next(false);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
clearUser(): void {
|
||||
this.storageService.removeItem(this.USER_EMAIL_KEY);
|
||||
this.userLoggedInSubject.next(false);
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return !!this.getUser();
|
||||
}
|
||||
|
||||
getSafeUser(): string {
|
||||
return this.getUser() || '';
|
||||
}
|
||||
}
|
||||
24
src/app/home/chart/chart.component.html
Normal file
24
src/app/home/chart/chart.component.html
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="dashboard-chart-container">
|
||||
|
||||
<div *ngIf="!chartData || chartData.length === 0" class="no-data">
|
||||
<mat-icon>pie_chart</mat-icon>
|
||||
<p>No carnet data available</p>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<div *ngFor="let config of chartConfigs; let i = index" class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h4 class="chart-title">{{ config.title }}</h4>
|
||||
<button mat-icon-button class="edit-icon" (click)="navigateToManageProvider(config.spid)"
|
||||
matTooltip="Edit Service Provider">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<canvas baseChart [data]="config.data" [options]="config.options" [type]="chartType"
|
||||
(chartClick)="chartClicked($event, i)" [plugins]="chartPlugins">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
91
src/app/home/chart/chart.component.scss
Normal file
91
src/app/home/chart/chart.component.scss
Normal file
@ -0,0 +1,91 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.dashboard-chart-container {
|
||||
padding: 24px 0px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 24px;
|
||||
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 250px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 8px;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-chart-container {
|
||||
padding: 16px;
|
||||
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
134
src/app/home/chart/chart.component.ts
Normal file
134
src/app/home/chart/chart.component.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { Router } from '@angular/router';
|
||||
import { CarnetStatus } from '../../core/models/carnet-status';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart',
|
||||
imports: [CommonModule, AngularMaterialModule, BaseChartDirective],
|
||||
templateUrl: './chart.component.html',
|
||||
styleUrl: './chart.component.scss'
|
||||
})
|
||||
export class ChartComponent {
|
||||
@Input() chartData: any[] = [];
|
||||
@Input() carnetStatuses: CarnetStatus[] = [];
|
||||
@Output() carnetStatusData = new EventEmitter<any>();
|
||||
|
||||
constructor(private router: Router) { }
|
||||
|
||||
navigateToManageProvider(spid: number): void {
|
||||
this.router.navigate(['/service-provider', spid]);
|
||||
}
|
||||
|
||||
public chartClicked(event: any, chartIndex: number): void {
|
||||
const active = event.active;
|
||||
if (active?.length) {
|
||||
const dataIndex = active[0].index;
|
||||
const chart = this.chartConfigs[chartIndex];
|
||||
|
||||
const spid = chart.spid as number;
|
||||
const carnetStatus = this.carnetStatuses.find(t => t.name === chart.data?.labels?.[dataIndex]);
|
||||
|
||||
if (carnetStatus !== undefined) {
|
||||
this.carnetStatusData.emit({
|
||||
spid: spid,
|
||||
carnetStatus: carnetStatus.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public chartOptions: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: '#333',
|
||||
font: {
|
||||
size: 14
|
||||
},
|
||||
padding: 20
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.dataset.label || '';
|
||||
const value = context.raw as number || 0;
|
||||
const total = (context.dataset.data as number[]).reduce((a: number, b: number) => a + b, 0);
|
||||
const percentage = Math.round((value / total) * 100);
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
public chartType: ChartType = 'pie';
|
||||
public chartPlugins = [];
|
||||
|
||||
public chartConfigs: {
|
||||
title: string,
|
||||
spid: number,
|
||||
data: ChartData<'pie'>;
|
||||
colors: string[];
|
||||
options: ChartConfiguration['options'];
|
||||
}[] = [];
|
||||
|
||||
ngOnChanges() {
|
||||
this.processChartData();
|
||||
}
|
||||
|
||||
private processChartData(): void {
|
||||
this.chartConfigs = this.chartData.map(provider => {
|
||||
const statusColors = provider.CARNETSTATUS.map((status: string) => {
|
||||
const carnetStatus = this.carnetStatuses.find(t => t.value === status);
|
||||
return carnetStatus ? carnetStatus.color : '#9E9E9E';
|
||||
});
|
||||
|
||||
const labels = provider.CARNETSTATUS.map((status: string) => {
|
||||
const carnetStatus = this.carnetStatuses.find(t => t.value === status);
|
||||
return carnetStatus!.name;
|
||||
});
|
||||
|
||||
return {
|
||||
title: provider.Service_Provider_Name,
|
||||
spid: provider.SPID,
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: provider.Carnet_Count,
|
||||
backgroundColor: statusColors,
|
||||
hoverBackgroundColor: statusColors.map((c: any) => this.adjustBrightness(c, -20)),
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
colors: statusColors,
|
||||
options: this.chartOptions
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private adjustBrightness(color: string, percent: number): string {
|
||||
// Helper function to adjust color brightness
|
||||
const num = parseInt(color.replace('#', ''), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = (num >> 16) + amt;
|
||||
const G = (num >> 8 & 0x00FF) + amt;
|
||||
const B = (num & 0x0000FF) + amt;
|
||||
|
||||
return '#' + (
|
||||
0x1000000 +
|
||||
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
||||
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
||||
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
||||
).toString(16).slice(1);
|
||||
}
|
||||
}
|
||||
95
src/app/home/home.component.html
Normal file
95
src/app/home/home.component.html
Normal file
@ -0,0 +1,95 @@
|
||||
<div class="dashboard-container">
|
||||
<div class="content">
|
||||
<div class="quick-actions">
|
||||
<button mat-raised-button color="accent" routerLink="/add-service-provider">
|
||||
<mat-icon>add</mat-icon> Add New Service Provider
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Chart Section -->
|
||||
<div class="chart-section">
|
||||
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses" (carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
|
||||
</div>
|
||||
|
||||
<!-- Carnet Details Section -->
|
||||
<div class="carnet-details-section">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8" *ngIf="showTable">
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="applicationName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Application Name</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.applicationName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="holderName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Name</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.holderName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="carnetNumber">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Number</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.carnetNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="usSets">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>US Sets</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.usSets }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="foreignSets">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Foregin Sets</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.foreignSets }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="transitSets">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Transit Sets</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.transitSets }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="carnetValue">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Value</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.carnetValue }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="issueDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Issue Date</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.issueDate |
|
||||
date:'mediumDate':'UTC' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="expiryDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Expiry Date</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.expiryDate |
|
||||
date:'mediumDate':'UTC' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="orderType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Order Type</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.orderType }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="carnetStatus">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Status</th>
|
||||
<td mat-cell *matCellDef="let item">{{ getCarnetStatusLabel(item.carnetStatus) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No data available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
63
src/app/home/home.component.scss
Normal file
63
src/app/home/home.component.scss
Normal file
@ -0,0 +1,63 @@
|
||||
.dashboard-container {
|
||||
padding: 24px;
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
button {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/app/home/home.component.ts
Normal file
113
src/app/home/home.component.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { HomeService } from '../core/services/home.service';
|
||||
import { AngularMaterialModule } from '../shared/module/angular-material.module';
|
||||
import { ChartComponent } from './chart/chart.component';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NotificationService } from '../core/services/notification.service';
|
||||
import { ApiErrorHandlerService } from '../core/services/api-error-handler.service';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { UserPreferences } from '../core/models/user-preference';
|
||||
import { UserPreferencesService } from '../core/services/user-preference.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CustomPaginator } from '../shared/custom-paginator';
|
||||
import { CommonService } from '../core/services/common.service';
|
||||
import { CarnetStatus } from '../core/models/carnet-status';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [AngularMaterialModule, ChartComponent, RouterLink, CommonModule],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class HomeComponent {
|
||||
carnetData: any[] = [];
|
||||
isLoading = false;
|
||||
showTable = false;
|
||||
userPreferences: UserPreferences;
|
||||
carnetStatuses: CarnetStatus[] = [];
|
||||
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
||||
@ViewChild(MatPaginator, { static: false })
|
||||
set paginator(value: MatPaginator) {
|
||||
this.dataSource.paginator = value;
|
||||
}
|
||||
|
||||
@ViewChild(MatSort, { static: false })
|
||||
set sort(value: MatSort) {
|
||||
this.dataSource.sort = value;
|
||||
}
|
||||
|
||||
displayedColumns: string[] = ['applicationName', 'holderName', 'carnetNumber', 'usSets', 'foreignSets', 'transitSets', 'carnetValue', 'issueDate', 'expiryDate', 'orderType', 'carnetStatus'];
|
||||
|
||||
|
||||
constructor(
|
||||
private homeService: HomeService,
|
||||
private errorHandler: ApiErrorHandlerService,
|
||||
private notificationService: NotificationService,
|
||||
userPrefenceService: UserPreferencesService,
|
||||
private commonService: CommonService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCarnetStatuses();
|
||||
this.loadCarnetData();
|
||||
}
|
||||
|
||||
loadCarnetStatuses(): void {
|
||||
this.commonService.getCarnetStatuses().subscribe({
|
||||
next: (carnetStatuses) => {
|
||||
this.carnetStatuses = carnetStatuses;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading carnet status:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCarnetData(): void {
|
||||
this.isLoading = true;
|
||||
this.homeService.getCarnetSummaryData().subscribe({
|
||||
next: (data) => {
|
||||
this.carnetData = data;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading carnet data:', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCarnetStatusClick(event: any): void {
|
||||
this.isLoading = true;
|
||||
this.showTable = false;
|
||||
this.homeService.getCarnetDataByStatus(event.spid, event.carnetStatus).subscribe({
|
||||
next: (carnetDetails) => {
|
||||
this.isLoading = false;
|
||||
this.showTable = true;
|
||||
this.dataSource.data = carnetDetails;
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load carnet data for the selected status');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading carnet data for the selected status:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exportData() {
|
||||
}
|
||||
|
||||
getCarnetStatusLabel(value: string): string {
|
||||
const carnetStatus = this.carnetStatuses.find(t => t.value === value);
|
||||
return carnetStatus ? carnetStatus.name : value;
|
||||
}
|
||||
}
|
||||
57
src/app/login/login.component.html
Normal file
57
src/app/login/login.component.html
Normal file
@ -0,0 +1,57 @@
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="logo-container">
|
||||
<img src="images/logo.jpeg" alt="USCIB Logo" class="logo">
|
||||
</div>
|
||||
|
||||
<h2 class="welcome-title">Welcome to USCIB Carnet Portal!</h2>
|
||||
<h3 class="subtitle">ATA Carnet: Your Passport for Duty-Free Global Trade</h3>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="login-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput formControlName="username" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="loginForm.get('username')?.errors?.['required']">
|
||||
Username is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="password" formControlName="password" required>
|
||||
<mat-icon matSuffix>lock</mat-icon>
|
||||
<mat-error *ngIf="loginForm.get('password')?.errors?.['required']">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="forgot-password">
|
||||
<a href="#" class="forgot-link">Forgot your password?</a>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="!loginForm.valid">
|
||||
Sign In
|
||||
</button>
|
||||
|
||||
<mat-error class="error-message" *ngIf="errorMessage !== ''">
|
||||
{{errorMessage}}
|
||||
</mat-error>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>ATA Carnet</h3>
|
||||
<p>
|
||||
Also known as the "Merchandise Passport," is an international customs document that simplifies temporary
|
||||
exports to over 79 countries and territories.
|
||||
It allows businesses to explore new markets, showcase products at trade shows, and attend global conferences
|
||||
without paying duties or taxes.
|
||||
</p>
|
||||
<p>
|
||||
It simplifies customs procedures for the temporary movement of goods and allows goods to
|
||||
enter Customs territories of the ATA Carnet system free of customs duties and taxes for up to
|
||||
one year.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
111
src/app/login/login.component.scss
Normal file
111
src/app/login/login.component.scss
Normal file
@ -0,0 +1,111 @@
|
||||
@import 'colors';
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
min-height: 85vh;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.login-card {
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
|
||||
.logo {
|
||||
max-width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
text-align: center;
|
||||
color: $primary-color;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
text-align: right;
|
||||
margin-top: -1rem;
|
||||
|
||||
.forgot-link {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-section {
|
||||
flex: 1;
|
||||
padding: 4rem;
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.login-container {
|
||||
flex-direction: column;
|
||||
|
||||
.login-card {
|
||||
max-width: 100%;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 2rem;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/app/login/login.component.ts
Normal file
63
src/app/login/login.component.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../core/services/auth.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AngularMaterialModule } from '../shared/module/angular-material.module';
|
||||
import { UserService } from '../core/services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.scss'
|
||||
})
|
||||
export class LoginComponent {
|
||||
loginForm: FormGroup;
|
||||
isLoading = false;
|
||||
errorMessage = '';
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private userService: UserService,
|
||||
private router: Router
|
||||
) {
|
||||
this.loginForm = this.fb.group({
|
||||
username: ['', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.userService.isLoggedIn()) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.loginForm.valid) {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
const { username, password } = this.loginForm.value;
|
||||
|
||||
this.authService.login(username, password).subscribe({
|
||||
next: (response) => {
|
||||
this.isLoading = false;
|
||||
if (response?.msg) {
|
||||
this.userService.setUser(username);
|
||||
this.router.navigate(['/home']);
|
||||
} else {
|
||||
this.errorMessage = response?.error ?? response?.msg;
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = 'Invalid username or password';
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
<div class="service-provider-container">
|
||||
<mat-stepper orientation="vertical" [linear]="true" (selectionChange)="onStepChange($event)"
|
||||
[selectedIndex]="currentStep">
|
||||
|
||||
<!-- Basic Details Step -->
|
||||
<mat-step [completed]="basicDetailsCompleted">
|
||||
<ng-template matStepLabel>Basic Details</ng-template>
|
||||
|
||||
<app-basic-details [isEditMode]="isEditMode" (spidCreated)="onBasicDetailsSaved($event)">
|
||||
</app-basic-details>
|
||||
</mat-step>
|
||||
|
||||
<!-- Contacts Step -->
|
||||
<mat-step [completed]="contactsCompleted" [editable]="!!serviceProviderId && basicDetailsCompleted">
|
||||
<ng-template matStepLabel>Contacts</ng-template>
|
||||
|
||||
<app-contacts *ngIf="serviceProviderId" [spid]="serviceProviderId" (hasContacts)="onContactsSaved($event)"
|
||||
[userPreferences]="userPreferences">
|
||||
</app-contacts>
|
||||
</mat-step>
|
||||
|
||||
<!-- Carnet Sequence Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && contactsCompleted" [completed]="carnetSequenceCompleted">
|
||||
<ng-template matStepLabel>Carnet Sequence</ng-template>
|
||||
|
||||
<app-carnet-sequence *ngIf="serviceProviderId" [spid]="serviceProviderId"
|
||||
(hasCarnetSequence)="onCarnetSequenceSaved($event)"
|
||||
[userPreferences]="userPreferences"></app-carnet-sequence>
|
||||
</mat-step>
|
||||
|
||||
<!-- Fees & Commission Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && carnetSequenceCompleted"
|
||||
[completed]="feeCommissionCompleted && carnetSequenceCompleted">
|
||||
<ng-template matStepLabel>Carnet Fee & Commission Setup</ng-template>
|
||||
|
||||
<app-carnet-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
|
||||
(hasFeeCommissions)="onFeeCommissionSaved($event)" [userPreferences]="userPreferences"></app-carnet-fee>
|
||||
</mat-step>
|
||||
|
||||
<!-- Basic Fee Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && feeCommissionCompleted"
|
||||
[completed]="basicFeeCompleted && feeCommissionCompleted">
|
||||
<ng-template matStepLabel>Basic Fee Setup</ng-template>
|
||||
|
||||
<app-basic-fee *ngIf="serviceProviderId" [spid]="serviceProviderId" (hasBasicFees)="onBasicFeeSaved($event)"
|
||||
[userPreferences]="userPreferences"></app-basic-fee>
|
||||
</mat-step>
|
||||
|
||||
<!-- Counterfoil Fee Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && basicFeeCompleted" [completed]="counterfoilFeeCompleted">
|
||||
<ng-template matStepLabel>Counterfoil Setup</ng-template>
|
||||
|
||||
<app-counterfoil-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
|
||||
(hasCounterFoilFee)="onCounterfoilFeeSaved($event)"
|
||||
[userPreferences]="userPreferences"></app-counterfoil-fee>
|
||||
</mat-step>
|
||||
|
||||
<!-- Continuation Sheet Fee Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && counterfoilFeeCompleted"
|
||||
[completed]="continuationSheetFeeCompleted">
|
||||
<ng-template matStepLabel>Continuation Sheet Fee Setup</ng-template>
|
||||
|
||||
<app-continuation-sheet-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
|
||||
(hasContinuationSheetFee)="onContinuationSheetFeeSaved($event)"
|
||||
[userPreferences]="userPreferences"></app-continuation-sheet-fee>
|
||||
</mat-step>
|
||||
|
||||
<!-- Expedited Fee Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && continuationSheetFeeCompleted" [completed]="expeditedFeeCompleted">
|
||||
<ng-template matStepLabel>Expedited Fee Setup</ng-template>
|
||||
|
||||
<app-expedited-fee *ngIf="serviceProviderId" [spid]="serviceProviderId"
|
||||
(hasExpeditedFees)="onExpeditedFeeSaved($event)"
|
||||
[userPreferences]="userPreferences"></app-expedited-fee>
|
||||
</mat-step>
|
||||
|
||||
<!-- Security Deposit Step -->
|
||||
<mat-step [editable]="!!serviceProviderId && expeditedFeeCompleted" [completed]="securityDepositCompleted">
|
||||
<ng-template matStepLabel>Security Deposit</ng-template>
|
||||
|
||||
<app-security-deposit *ngIf="serviceProviderId" [spid]="serviceProviderId"
|
||||
(hasSecurityDeposits)="onSecurityDepositSaved($event)"
|
||||
[userPreferences]="userPreferences"></app-security-deposit>
|
||||
</mat-step>
|
||||
|
||||
</mat-stepper>
|
||||
</div>
|
||||
@ -0,0 +1,25 @@
|
||||
@import 'colors';
|
||||
|
||||
.service-provider-container {
|
||||
max-width: 1200px;
|
||||
margin: 24px auto;
|
||||
padding: 16px;
|
||||
|
||||
mat-stepper {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.service-provider-container {
|
||||
padding: 8px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { BasicDetailsComponent } from '../basic-details/basic-details.component';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContactsComponent } from '../contacts/contacts.component';
|
||||
import { CarnetSequenceComponent } from '../carnet-sequence/carnet-sequence.component';
|
||||
import { CarnetFeeComponent } from '../carnet-fee/carnet-fee.component';
|
||||
import { BasicFeeComponent } from '../basic-fee/basic-fee.component';
|
||||
import { CounterfoilFeeComponent } from '../counterfoil-fee/counterfoil-fee.component';
|
||||
import { ExpeditedFeeComponent } from '../expedited-fee/expedited-fee.component';
|
||||
import { SecurityDepositComponent } from '../security-deposit/security-deposit.component';
|
||||
import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component";
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-service-provider',
|
||||
imports: [BasicDetailsComponent, AngularMaterialModule, CommonModule, ContactsComponent, CarnetSequenceComponent,
|
||||
CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, ContinuationSheetFeeComponent],
|
||||
templateUrl: './add-service-provider.component.html',
|
||||
styleUrl: './add-service-provider.component.scss'
|
||||
})
|
||||
export class AddServiceProviderComponent {
|
||||
isEditMode = false;
|
||||
serviceProviderId: number | null = null;
|
||||
currentStep: number = 0;
|
||||
isLoading: boolean = false;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
basicDetailsCompleted: boolean = false;
|
||||
contactsCompleted: boolean = false;
|
||||
carnetSequenceCompleted: boolean = false;
|
||||
feeCommissionCompleted: boolean = false;
|
||||
basicFeeCompleted: boolean = false;
|
||||
counterfoilFeeCompleted: boolean = false;
|
||||
continuationSheetFeeCompleted: boolean = false;
|
||||
expeditedFeeCompleted: boolean = false;
|
||||
securityDepositCompleted: boolean = false;
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
onBasicDetailsSaved(event: string): void {
|
||||
this.serviceProviderId = +event;
|
||||
this.basicDetailsCompleted = true;
|
||||
}
|
||||
|
||||
onContactsSaved(event: boolean): void {
|
||||
this.contactsCompleted = event;
|
||||
}
|
||||
|
||||
onCarnetSequenceSaved(event: boolean): void {
|
||||
this.carnetSequenceCompleted = event;
|
||||
}
|
||||
|
||||
onFeeCommissionSaved(event: boolean): void {
|
||||
this.feeCommissionCompleted = event;
|
||||
}
|
||||
|
||||
onBasicFeeSaved(event: boolean): void {
|
||||
this.basicFeeCompleted = event;
|
||||
}
|
||||
|
||||
onCounterfoilFeeSaved(event: boolean): void {
|
||||
this.counterfoilFeeCompleted = event;
|
||||
}
|
||||
|
||||
onContinuationSheetFeeSaved(event: boolean): void {
|
||||
this.continuationSheetFeeCompleted = event;
|
||||
}
|
||||
|
||||
onExpeditedFeeSaved(event: boolean): void {
|
||||
this.expeditedFeeCompleted = event;
|
||||
}
|
||||
|
||||
onSecurityDepositSaved(event: boolean): void {
|
||||
this.securityDepositCompleted = event;
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,186 @@
|
||||
<div class="basic-details-container">
|
||||
<mat-card class="details-card mat-elevation-z4">
|
||||
<mat-card-content>
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="basicDetailsForm" class="details-form" *ngIf="!isLoading"
|
||||
(ngSubmit)="saveBasicDetails()">
|
||||
<!-- Company Information Row -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="company-name">
|
||||
<mat-label>Company Name</mat-label>
|
||||
<input matInput formControlName="companyName" required>
|
||||
<mat-icon matSuffix>business</mat-icon>
|
||||
<mat-error *ngIf="f['companyName'].errors?.['required']">
|
||||
Company name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['companyName'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="lookup-code">
|
||||
<mat-label>Lookup Code</mat-label>
|
||||
<input matInput formControlName="lookupCode">
|
||||
<mat-error *ngIf="f['lookupCode'].errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- Address Information Row -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address-line-1">
|
||||
<mat-label>Address Line 1</mat-label>
|
||||
<input matInput formControlName="address1" required>
|
||||
<mat-error *ngIf="f['address1'].errors?.['required']">
|
||||
Address is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['address1'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address-line-2">
|
||||
<mat-label>Address Line 2 (Optional)</mat-label>
|
||||
<input matInput formControlName="address2">
|
||||
<mat-error *ngIf="f['address2'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Location Information Row -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="city">
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" required>
|
||||
<mat-error *ngIf="f['city'].errors?.['required']">
|
||||
City is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['city'].errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country</mat-label>
|
||||
<mat-select formControlName="country" required
|
||||
(selectionChange)="onCountryChange($event.value)">
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['country'].errors?.['required']">
|
||||
Country is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="state">
|
||||
<mat-label>State/Province</mat-label>
|
||||
<mat-select formControlName="state" required>
|
||||
<mat-option *ngFor="let state of states" [value]="state.value">
|
||||
{{ state.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['state'].errors?.['required']">
|
||||
State is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="zip">
|
||||
<mat-label>ZIP/Postal Code</mat-label>
|
||||
<input matInput formControlName="zip" required>
|
||||
<mat-error *ngIf="f['zip'].errors?.['required']">
|
||||
ZIP/Postal code is required
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="f['country']?.value === 'US' && f['zip']?.touched && f['zip']?.errors?.['invalidUSZip']">
|
||||
Please enter a valid 5-digit US ZIP code
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="f['country']?.value === 'CA' && f['zip']?.touched && f['zip']?.errors?.['invalidCanadaPostal']">
|
||||
Please enter a valid postal code (e.g., A1B2C3)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Region Information Row -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="issuing-region">
|
||||
<mat-label>Issuing Region</mat-label>
|
||||
<mat-select formControlName="issuingRegion" required>
|
||||
<mat-option *ngFor="let region of regions" [value]="region.region">
|
||||
{{ region.regionname }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['issuingRegion'].errors?.['required']">
|
||||
Issuing region is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="replacement-region">
|
||||
<mat-label>Replacement Region</mat-label>
|
||||
<mat-select formControlName="replacementRegion" required>
|
||||
<mat-option *ngFor="let region of regions" [value]="region.region">
|
||||
{{ region.regionname }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['replacementRegion'].errors?.['required']">
|
||||
Replacement region is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="cargo-surety">
|
||||
<mat-label>Cargo Surety</mat-label>
|
||||
<mat-select formControlName="cargoSurety" required>
|
||||
<mat-option *ngFor="let cargoSurety of cargoSuretys" [value]="cargoSurety.value">
|
||||
{{ cargoSurety.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['cargoSurety'].errors?.['required']">
|
||||
Cargo Surety is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="cargo-policy">
|
||||
<mat-label>Cargo Policy</mat-label>
|
||||
<mat-select formControlName="cargoPolicyNo" required>
|
||||
<mat-option *ngFor="let cargoPolicy of cargoPolicies" [value]="cargoPolicy.value">
|
||||
{{ cargoPolicy.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['cargoPolicyNo'].errors?.['required']">
|
||||
Cargo Policy is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="bond-surety">
|
||||
<mat-label>Bond Surety</mat-label>
|
||||
<mat-select formControlName="bondSurety" required>
|
||||
<mat-option *ngFor="let bondSurety of bondSuretys" [value]="bondSurety.value">
|
||||
{{ bondSurety.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['bondSurety'].errors?.['required']">
|
||||
Bond Surety is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="basicDetailsForm.invalid">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -0,0 +1,125 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.basic-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
|
||||
.details-card {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.details-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
// Specific field sizing
|
||||
.company-name {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.lookup-code {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.address-line-1,
|
||||
.address-line-2 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.city {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.country {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.state {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.zip {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.issuing-region,
|
||||
.replacement-region {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.cargo-surety,
|
||||
.cargo-policy,
|
||||
.bond-surety {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
|
||||
mat-icon[matSuffix] {
|
||||
color: $icon-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 960px) {
|
||||
.basic-details-container {
|
||||
.details-card {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.company-name,
|
||||
.address-line-1,
|
||||
.address-line-2,
|
||||
.lookup-code,
|
||||
.city,
|
||||
.country,
|
||||
.state,
|
||||
.zip,
|
||||
.issuing-region,
|
||||
.replacement-region,
|
||||
.cargo-surety,
|
||||
.cargo-policy,
|
||||
.bond-surety {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,288 @@
|
||||
import { Component, Input, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { BasicDetail } from '../../core/models/service-provider/basic-detail';
|
||||
import { Country } from '../../core/models/country';
|
||||
import { Region } from '../../core/models/region';
|
||||
import { State } from '../../core/models/state';
|
||||
import { CommonService } from '../../core/services/common.service';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { ZipCodeValidator } from '../../shared/validators/zipcode-validator';
|
||||
import { BasicDetailService } from '../../core/services/basic-detail.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { BondSurety } from '../../core/models/bond-surety';
|
||||
import { CargoSurety } from '../../core/models/cargo-surety';
|
||||
import { CargoPolicy } from '../../core/models/cargo-policy';
|
||||
|
||||
@Component({
|
||||
selector: 'app-basic-details',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
|
||||
templateUrl: './basic-details.component.html',
|
||||
styleUrls: ['./basic-details.component.scss']
|
||||
})
|
||||
export class BasicDetailsComponent implements OnInit, OnDestroy {
|
||||
@Input() isEditMode = false;
|
||||
@Input() spid: number = 0;
|
||||
@Output() spidCreated = new EventEmitter<string>();
|
||||
@Output() serviceProviderName = new EventEmitter<string>();
|
||||
|
||||
basicDetailsForm: FormGroup;
|
||||
countries: Country[] = [];
|
||||
regions: Region[] = [];
|
||||
states: State[] = [];
|
||||
bondSuretys: BondSurety[] = [];
|
||||
cargoSuretys: CargoSurety[] = [];
|
||||
cargoPolicies: CargoPolicy[] = [];
|
||||
|
||||
isLoading = true;
|
||||
countriesHasStates = ['US', 'CA', 'MX'];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private commonService: CommonService,
|
||||
private basicDetailService: BasicDetailService,
|
||||
private notificationService: NotificationService,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.basicDetailsForm = this.createForm();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadLookupData();
|
||||
// this.spidCreated.emit(this.spid?.toString());
|
||||
// Patch edit form data
|
||||
if (this.spid > 0) {
|
||||
this.basicDetailService.getBasicDetailsById(this.spid).subscribe({
|
||||
next: (basicDetail: BasicDetail) => {
|
||||
if (basicDetail?.spid > 0) {
|
||||
this.patchFormData(basicDetail);
|
||||
this.serviceProviderName.emit(basicDetail.companyName);
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading basic details:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
createForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
companyName: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
lookupCode: ['', [Validators.maxLength(20)]],
|
||||
address1: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
address2: ['', [Validators.maxLength(100)]],
|
||||
city: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
country: ['', Validators.required],
|
||||
state: ['', Validators.required],
|
||||
zip: ['', [Validators.required, ZipCodeValidator('country')]],
|
||||
issuingRegion: ['', Validators.required],
|
||||
replacementRegion: ['', Validators.required],
|
||||
cargoSurety: ['', Validators.required],
|
||||
cargoPolicyNo: ['', Validators.required],
|
||||
bondSurety: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
loadLookupData(): void {
|
||||
this.commonService.getCountries(this.spid)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
this.countries = countries;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.loadRegions();
|
||||
this.loadCargoSuretys();
|
||||
this.loadCargoPolicies();
|
||||
this.loadBondSuretys();
|
||||
}
|
||||
|
||||
loadRegions(): void {
|
||||
this.commonService.getRegions()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (regions) => {
|
||||
this.regions = regions;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load regions', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadStates(country: string): void {
|
||||
this.isLoading = true;
|
||||
country = this.countriesHasStates.includes(country) ? country : 'FN';
|
||||
this.commonService.getStates(country, this.spid)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (states) => {
|
||||
this.states = states;
|
||||
const stateControl = this.basicDetailsForm.get('state');
|
||||
if (this.countriesHasStates.includes(country)) {
|
||||
stateControl?.enable();
|
||||
} else {
|
||||
stateControl?.disable();
|
||||
stateControl?.setValue('FN');
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load states', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadBondSuretys(): void {
|
||||
this.commonService.getBondSuretys()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (bondSuretys) => {
|
||||
this.bondSuretys = bondSuretys;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load bond suretys', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCargoSuretys(): void {
|
||||
this.commonService.getCargoSuretys()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (cargoSuretys) => {
|
||||
this.cargoSuretys = cargoSuretys;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load cargo suretys', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCargoPolicies(): void {
|
||||
this.commonService.getCargoPolicies()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (cargoPolicies) => {
|
||||
this.cargoPolicies = cargoPolicies;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load cargo policies', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patchFormData(data: BasicDetail): void {
|
||||
this.basicDetailsForm.patchValue({
|
||||
companyName: data.companyName,
|
||||
lookupCode: data.lookupCode,
|
||||
address1: data.address1,
|
||||
address2: data.address2,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
state: data.state,
|
||||
zip: data.zip,
|
||||
issuingRegion: data.issuingRegion,
|
||||
replacementRegion: data.replacementRegion,
|
||||
cargoSurety: data.cargoSurety,
|
||||
cargoPolicyNo: data.cargoPolicyNo,
|
||||
bondSurety: data.bondSurety
|
||||
});
|
||||
|
||||
if (data.country) {
|
||||
this.loadStates(data.country);
|
||||
}
|
||||
|
||||
if (this.isEditMode) {
|
||||
this.basicDetailsForm.get('issuingRegion')?.disable();
|
||||
this.basicDetailsForm.get('replacementRegion')?.disable();
|
||||
this.basicDetailsForm.get('companyName')?.disable();
|
||||
this.basicDetailsForm.get('cargoSurety')?.disable();
|
||||
this.basicDetailsForm.get('bondSurety')?.disable();
|
||||
}
|
||||
}
|
||||
|
||||
onCountryChange(country: string): void {
|
||||
this.basicDetailsForm.get('state')?.reset();
|
||||
|
||||
if (country) {
|
||||
this.loadStates(country);
|
||||
}
|
||||
|
||||
this.basicDetailsForm.get('zip')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
// Convenience getter for easy access to form fields
|
||||
get f() {
|
||||
return this.basicDetailsForm.controls;
|
||||
}
|
||||
|
||||
saveBasicDetails(): void {
|
||||
if (this.basicDetailsForm.invalid) {
|
||||
this.basicDetailsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const basicDetailData: BasicDetail = this.basicDetailsForm.value;
|
||||
|
||||
// states
|
||||
basicDetailData.state = this.basicDetailsForm.get('state')?.value;
|
||||
|
||||
// non editable fields values
|
||||
if (this.isEditMode) {
|
||||
basicDetailData.issuingRegion = this.basicDetailsForm.get('issuingRegion')?.value;
|
||||
basicDetailData.replacementRegion = this.basicDetailsForm.get('replacementRegion')?.value;
|
||||
basicDetailData.companyName = this.basicDetailsForm.get('companyName')?.value;
|
||||
basicDetailData.cargoSurety = this.basicDetailsForm.get('cargoSurety')?.value;
|
||||
basicDetailData.bondSurety = this.basicDetailsForm.get('bondSurety')?.value;
|
||||
}
|
||||
|
||||
const saveObservable = this.isEditMode && this.spid > 0
|
||||
? this.basicDetailService.updateBasicDetails(this.spid, basicDetailData)
|
||||
: this.basicDetailService.createBasicDetails(basicDetailData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Basic details ${this.isEditMode ? 'updated' : 'added'} successfully`);
|
||||
|
||||
if (!this.isEditMode) {
|
||||
this.spidCreated.emit(basicData.SPID);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditMode ? 'update' : 'add'} basic details`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving contact:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
146
src/app/service-provider/basic-fee/basic-fee.component.html
Normal file
146
src/app/service-provider/basic-fee/basic-fee.component.html
Normal file
@ -0,0 +1,146 @@
|
||||
<div class="basic-fee-container">
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Start Carnet Value Column -->
|
||||
<ng-container matColumnDef="startCarnetValue">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Start Carnet Value</th>
|
||||
<td mat-cell *matCellDef="let fee">{{ fee.startCarnetValue | number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- End Carnet Value Column -->
|
||||
<ng-container matColumnDef="endCarnetValue">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>End Carnet Value</th>
|
||||
<td mat-cell *matCellDef="let fee">
|
||||
{{ fee.endCarnetValue ? (fee.endCarnetValue | number) : 'Above' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Fees Column -->
|
||||
<ng-container matColumnDef="fees">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Fees</th>
|
||||
<td mat-cell *matCellDef="let fee">{{ fee.fees | currency }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Effective Date Column -->
|
||||
<ng-container matColumnDef="effectiveDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th>
|
||||
<td mat-cell *matCellDef="let fee">{{ fee.effectiveDate | date:'mediumDate':'UTC' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let fee">
|
||||
<button mat-icon-button color="primary" (click)="editFee(fee)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No basic fees available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Fee Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="feeForm" (ngSubmit)="saveFee()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Basic Fee' : 'Add New Basic Fee' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Start Carnet Value</mat-label>
|
||||
<input matInput type="number" formControlName="startCarnetValue" required min="0" step="1">
|
||||
<mat-error *ngIf="feeForm.get('startCarnetValue')?.errors?.['required']">
|
||||
Start value is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="feeForm.get('startCarnetValue')?.errors?.['min']">
|
||||
Value must be positive
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>End Carnet Value (leave blank for no upper limit)</mat-label>
|
||||
<input matInput type="number" formControlName="endCarnetValue" min="0" step="1">
|
||||
<mat-error *ngIf="feeForm.get('endCarnetValue')?.errors?.['min']">
|
||||
Value must be positive
|
||||
</mat-error>
|
||||
<mat-error *ngIf="feeForm.get('endCarnetValue')?.errors?.['invalidRange']">
|
||||
Must be greater than start value
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fees</mat-label>
|
||||
<span class="dollar-prefix" matPrefix>$ </span>
|
||||
<input matInput type="number" formControlName="fees" required min="0" step="1">
|
||||
<mat-error *ngIf="feeForm.get('fees')?.errors?.['required']">
|
||||
Fees are required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="feeForm.get('fees')?.errors?.['min']">
|
||||
Fees must be positive
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Effective Date</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="feeForm.get('effectiveDate')?.errors?.['required']">
|
||||
Effective date is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{readOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(readOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="feeForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
156
src/app/service-provider/basic-fee/basic-fee.component.scss
Normal file
156
src/app/service-provider/basic-fee/basic-fee.component.scss
Normal file
@ -0,0 +1,156 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.basic-fee-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin-top: -16px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dollar-prefix {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.basic-fee-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
228
src/app/service-provider/basic-fee/basic-fee.component.ts
Normal file
228
src/app/service-provider/basic-fee/basic-fee.component.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { BasicFeeService } from '../../core/services/basic-fee.service';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { BasicFee } from '../../core/models/service-provider/basic-fee';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-basic-fee',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule, FormsModule],
|
||||
templateUrl: './basic-fee.component.html',
|
||||
styleUrl: './basic-fee.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class BasicFeeComponent {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['startCarnetValue', 'endCarnetValue', 'fees', 'effectiveDate', 'actions'];
|
||||
dataSource = new MatTableDataSource<BasicFee>();
|
||||
feeForm: FormGroup;
|
||||
isEditing = false;
|
||||
currentFeeId: number | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
|
||||
readOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null
|
||||
};
|
||||
|
||||
@Input() isEditMode = false;
|
||||
@Input() userPreferences!: UserPreferences;
|
||||
@Input() spid: number = 0;
|
||||
@Output() hasBasicFees = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private basicFeeService: BasicFeeService,
|
||||
private notificationService: NotificationService,
|
||||
private dialog: MatDialog,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.feeForm = this.fb.group({
|
||||
startCarnetValue: [0, [Validators.required, Validators.min(0)]],
|
||||
endCarnetValue: [null, [Validators.min(0)]],
|
||||
fees: [0, [Validators.required, Validators.min(0)]],
|
||||
effectiveDate: ['', Validators.required]
|
||||
}, { validators: this.validateRange });
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadBasicFees();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
loadBasicFees(): void {
|
||||
this.isLoading = true;
|
||||
this.basicFeeService.getBasicFees(this.spid).subscribe({
|
||||
next: (fees) => {
|
||||
this.dataSource.data = fees;
|
||||
this.hasBasicFees.emit(fees.length > 0);
|
||||
this.isLoading = false;
|
||||
|
||||
if (this.dataSource.data.length == 0) {
|
||||
this.initializeDefaultFees();
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic fees');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading basic fees:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeDefaultFees(): void {
|
||||
this.isLoading = true;
|
||||
const defaultFees: BasicFee[] = [
|
||||
{ basicFeeId: 0, startCarnetValue: 1, endCarnetValue: 9999, fees: 255, effectiveDate: new Date() },
|
||||
{ basicFeeId: 0, startCarnetValue: 10000, endCarnetValue: 49999, fees: 300, effectiveDate: new Date() },
|
||||
{ basicFeeId: 0, startCarnetValue: 50000, endCarnetValue: 149999, fees: 365, effectiveDate: new Date() },
|
||||
{ basicFeeId: 0, startCarnetValue: 150000, endCarnetValue: 399999, fees: 425, effectiveDate: new Date() },
|
||||
{ basicFeeId: 0, startCarnetValue: 400000, endCarnetValue: 999999, fees: 480, effectiveDate: new Date() },
|
||||
{ basicFeeId: 0, startCarnetValue: 1000000, endCarnetValue: null, fees: 545, effectiveDate: new Date() }
|
||||
];
|
||||
|
||||
// Create an array of observables for each fee creation
|
||||
const creationObservables = defaultFees.map(fee =>
|
||||
this.basicFeeService.createBasicFee(this.spid, fee)
|
||||
);
|
||||
|
||||
// Execute all creations in parallel and wait for all to complete
|
||||
forkJoin(creationObservables).subscribe({
|
||||
next: () => {
|
||||
this.loadBasicFees(); // Refresh the list after all creations are done
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading = false;
|
||||
console.error('Error initializing default fees:', error);
|
||||
// Even if some failed, try to load what was created
|
||||
this.loadBasicFees();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateRange(formGroup: FormGroup): { [key: string]: any } | null {
|
||||
const start = formGroup.get('startCarnetValue')?.value;
|
||||
const end = formGroup.get('endCarnetValue')?.value;
|
||||
|
||||
if (end !== null && start !== null && end <= start) {
|
||||
return { invalidRange: true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// addNewFee(): void {
|
||||
// this.showForm = true;
|
||||
// this.isEditing = false;
|
||||
// this.currentFeeId = null;
|
||||
// this.feeForm.reset({
|
||||
// startCarnetValue: 0,
|
||||
// endCarnetValue: null,
|
||||
// fees: 0
|
||||
// });
|
||||
// }
|
||||
|
||||
editFee(fee: BasicFee): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentFeeId = fee.basicFeeId;
|
||||
this.feeForm.patchValue({
|
||||
startCarnetValue: fee.startCarnetValue,
|
||||
endCarnetValue: fee.endCarnetValue,
|
||||
fees: fee.fees,
|
||||
effectiveDate: fee.effectiveDate
|
||||
});
|
||||
|
||||
this.readOnlyFields.lastChangedDate = fee.dateCreated;
|
||||
this.readOnlyFields.lastChangedBy = fee.createdBy;
|
||||
|
||||
this.feeForm.get('startCarnetValue')?.disable();
|
||||
this.feeForm.get('endCarnetValue')?.disable();
|
||||
}
|
||||
|
||||
saveFee(): void {
|
||||
if (this.feeForm.invalid) {
|
||||
this.feeForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const feeData: BasicFee = {
|
||||
basicFeeId: this.currentFeeId || 0,
|
||||
startCarnetValue: this.feeForm.value.startCarnetValue,
|
||||
endCarnetValue: this.feeForm.value.endCarnetValue,
|
||||
fees: this.feeForm.value.fees,
|
||||
effectiveDate: this.feeForm.value.effectiveDate,
|
||||
spid: this.spid
|
||||
};
|
||||
|
||||
const saveObservable = this.isEditing && this.currentFeeId
|
||||
? this.basicFeeService.updateBasicFee(this.currentFeeId, feeData)
|
||||
: this.basicFeeService.createBasicFee(this.spid, feeData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Basic fee ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadBasicFees();
|
||||
this.cancelEdit();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} basic fee`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving basic fee:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteFee(feeId: number): void {
|
||||
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
// width: '350px',
|
||||
// data: {
|
||||
// title: 'Confirm Delete',
|
||||
// message: 'Are you sure you want to delete this basic fee?',
|
||||
// confirmText: 'Delete',
|
||||
// cancelText: 'Cancel'
|
||||
// }
|
||||
// });
|
||||
|
||||
// dialogRef.afterClosed().subscribe(result => {
|
||||
// if (result) {
|
||||
// this.basicFeeService.deleteBasicFee(feeId).subscribe({
|
||||
// next: () => {
|
||||
// this.notificationService.showSuccess('Basic fee deleted successfully');
|
||||
// this.loadBasicFees();
|
||||
// },
|
||||
// error: (error) => {
|
||||
// this.notificationService.showError('Failed to delete basic fee');
|
||||
// console.error('Error deleting basic fee:', error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentFeeId = null;
|
||||
this.feeForm.reset();
|
||||
}
|
||||
}
|
||||
133
src/app/service-provider/carnet-fee/carnet-fee.component.html
Normal file
133
src/app/service-provider/carnet-fee/carnet-fee.component.html
Normal file
@ -0,0 +1,133 @@
|
||||
<div class="fee-commission-container">
|
||||
<div class="actions-bar">
|
||||
<button mat-raised-button color="primary" *ngIf="!isEditMode" (click)="addNewFeeCommission()">
|
||||
<mat-icon>add</mat-icon> Add New Fee & Commission
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Fee Type Column -->
|
||||
<ng-container matColumnDef="feeType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Fee Type</th>
|
||||
<td mat-cell *matCellDef="let item">{{ getFeeTypeLabel(item.feeType) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Commission Rate Column -->
|
||||
<ng-container matColumnDef="commissionRate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Commission Rate</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.commissionRate }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Effective Date Column -->
|
||||
<ng-container matColumnDef="effectiveDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.effectiveDate | date:'mediumDate':'UTC' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button color="primary" (click)="editFeeCommission(item)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<!-- <button mat-icon-button color="warn" (click)="deleteFeeCommission(item.feeCommissionId)"
|
||||
matTooltip="Delete" *ngIf="!isEditMode">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button> -->
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No fee & commission records available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Fee & Commission Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="feeCommissionForm" (ngSubmit)="saveFeeCommission()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Fee & Commission' : 'Add New Fee & Commission' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fee Type</mat-label>
|
||||
<mat-select formControlName="feeType" required>
|
||||
<mat-option *ngFor="let type of feeTypes" [value]="type.id">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="feeCommissionForm.get('feeType')?.errors?.['required']">
|
||||
Fee type is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Commission Rate</mat-label>
|
||||
<input matInput type="number" formControlName="commissionRate">
|
||||
<mat-error *ngIf="feeCommissionForm.get('commissionRate')?.errors?.['required']">
|
||||
Commission rate is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Effective Date</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="feeCommissionForm.get('effectiveDate')?.errors?.['required']">
|
||||
Effective date is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{readOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(readOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="feeCommissionForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
166
src/app/service-provider/carnet-fee/carnet-fee.component.scss
Normal file
166
src/app/service-provider/carnet-fee/carnet-fee.component.scss
Normal file
@ -0,0 +1,166 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.fee-commission-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin-top: -16px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.fee-commission-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
src/app/service-provider/carnet-fee/carnet-fee.component.ts
Normal file
209
src/app/service-provider/carnet-fee/carnet-fee.component.ts
Normal file
@ -0,0 +1,209 @@
|
||||
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { CommonService } from '../../core/services/common.service';
|
||||
import { CarnetFee } from '../../core/models/service-provider/carnet-fee';
|
||||
import { FeeType } from '../../core/models/fee-type';
|
||||
import { CarnetFeeService } from '../../core/services/carnet-fee.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-carnet-fee',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
|
||||
templateUrl: './carnet-fee.component.html',
|
||||
styleUrl: './carnet-fee.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class CarnetFeeComponent implements OnInit {
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['feeType', 'commissionRate', 'effectiveDate', 'actions'];
|
||||
dataSource = new MatTableDataSource<CarnetFee>();
|
||||
feeCommissionForm: FormGroup;
|
||||
isEditing = false;
|
||||
currentFeeCommissionId: number | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
feeTypes: FeeType[] = [];
|
||||
|
||||
readOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null
|
||||
};
|
||||
|
||||
@Input() isEditMode = false;
|
||||
@Input() userPreferences!: UserPreferences;
|
||||
@Input() spid: number = 0;
|
||||
@Output() hasFeeCommissions = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private feeCommissionService: CarnetFeeService,
|
||||
private commonService: CommonService,
|
||||
private notificationService: NotificationService,
|
||||
private dialog: MatDialog,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.feeCommissionForm = this.fb.group({
|
||||
feeType: ['', Validators.required],
|
||||
commissionRate: [0, [Validators.required]],
|
||||
effectiveDate: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadFeeCommissions();
|
||||
this.loadFeeTypes();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
loadFeeCommissions(): void {
|
||||
this.isLoading = true;
|
||||
this.feeCommissionService.getFeeCommissions(this.spid).subscribe({
|
||||
next: (fees) => {
|
||||
this.dataSource.data = fees;
|
||||
this.hasFeeCommissions.emit(fees.length > 0);
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load fee & commission data');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading fee & commission data:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadFeeTypes(): void {
|
||||
this.commonService.getFeeTypes().subscribe({
|
||||
next: (types) => {
|
||||
this.feeTypes = types;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading fee types:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// applyFilter(event: Event): void {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
// this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
|
||||
// if (this.dataSource.paginator) {
|
||||
// this.dataSource.paginator.firstPage();
|
||||
// }
|
||||
// }
|
||||
|
||||
addNewFeeCommission(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentFeeCommissionId = null;
|
||||
this.feeCommissionForm.reset();
|
||||
|
||||
this.feeCommissionForm.get('feeType')?.enable();
|
||||
}
|
||||
|
||||
editFeeCommission(feeCommission: CarnetFee): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentFeeCommissionId = feeCommission.feeCommissionId;
|
||||
this.feeCommissionForm.patchValue({
|
||||
feeType: feeCommission.feeType,
|
||||
commissionRate: feeCommission.commissionRate,
|
||||
effectiveDate: feeCommission.effectiveDate
|
||||
});
|
||||
|
||||
this.readOnlyFields.lastChangedDate = feeCommission.dateCreated;
|
||||
this.readOnlyFields.lastChangedBy = feeCommission.createdBy;
|
||||
|
||||
this.feeCommissionForm.get('feeType')?.disable();
|
||||
}
|
||||
|
||||
saveFeeCommission(): void {
|
||||
if (this.feeCommissionForm.invalid) {
|
||||
this.feeCommissionForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.feeCommissionForm.value;
|
||||
const feeCommissionData: CarnetFee = {
|
||||
feeCommissionId: this.currentFeeCommissionId || 0,
|
||||
feeType: formData.feeType,
|
||||
commissionRate: formData.commissionRate,
|
||||
effectiveDate: formData.effectiveDate,
|
||||
spid: this.spid
|
||||
};
|
||||
|
||||
const saveObservable = this.isEditing && this.currentFeeCommissionId
|
||||
? this.feeCommissionService.updateFeeCommission(this.currentFeeCommissionId, feeCommissionData)
|
||||
: this.feeCommissionService.createFeeCommission(this.spid, feeCommissionData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Fee & commission ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadFeeCommissions();
|
||||
this.cancelEdit();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} fee & commission`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving fee & commission:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteFeeCommission(feeCommissionId: number): void {
|
||||
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
// width: '350px',
|
||||
// data: {
|
||||
// title: 'Confirm Delete',
|
||||
// message: 'Are you sure you want to delete this fee & commission record?',
|
||||
// confirmText: 'Delete',
|
||||
// cancelText: 'Cancel'
|
||||
// }
|
||||
// });
|
||||
|
||||
// dialogRef.afterClosed().subscribe(result => {
|
||||
// if (result) {
|
||||
// this.feeCommissionService.deleteFeeCommission(feeCommissionId).subscribe({
|
||||
// next: () => {
|
||||
// this.notificationService.showSuccess('Fee & commission record deleted successfully');
|
||||
// this.loadFeeCommissions();
|
||||
// },
|
||||
// error: (error) => {
|
||||
// this.notificationService.showError('Failed to delete fee & commission record');
|
||||
// console.error('Error deleting fee & commission record:', error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentFeeCommissionId = null;
|
||||
this.feeCommissionForm.reset();
|
||||
}
|
||||
|
||||
getFeeTypeLabel(value: string): string {
|
||||
const type = this.feeTypes.find(t => t.id === value);
|
||||
return type ? type.name : value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
<div class="carnet-sequence-container">
|
||||
<div class="actions-bar">
|
||||
<button mat-raised-button color="primary" *ngIf="!isEditMode" (click)="addNewSequence()">
|
||||
<mat-icon>add</mat-icon> Add New Sequence
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Carnet Type Column -->
|
||||
<ng-container matColumnDef="carnetType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Type</th>
|
||||
<td mat-cell *matCellDef="let sequence">{{ getCarnetTypeLabel(sequence.carnetType) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Region Column -->
|
||||
<ng-container matColumnDef="region">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Region</th>
|
||||
<td mat-cell *matCellDef="let sequence">{{ getRegionLabel(sequence.region) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Start Number Column -->
|
||||
<ng-container matColumnDef="startNumber">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Start Number</th>
|
||||
<td mat-cell *matCellDef="let sequence">{{ sequence.startNumber | number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- End Number Column -->
|
||||
<ng-container matColumnDef="endNumber">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>End Number</th>
|
||||
<td mat-cell *matCellDef="let sequence">{{ sequence.endNumber | number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Last Number Column -->
|
||||
<ng-container matColumnDef="lastNumber">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Number</th>
|
||||
<td mat-cell *matCellDef="let sequence">{{ sequence.lastNumber | number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<!-- <ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let sequence">
|
||||
<button mat-icon-button color="primary" (click)="editSequence(sequence)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteSequence(sequence.id)" matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container> -->
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No sequences available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Sequence Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="sequenceForm" (ngSubmit)="saveSequence()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Sequence' : 'Add New Sequence' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-label>Carnet Type</mat-label>
|
||||
<mat-radio-group formControlName="carnetType" required class="horizontal-group">
|
||||
<mat-radio-button *ngFor="let type of carnetTypes" [value]="type.value" class="radio-button">
|
||||
{{ type.label }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-error *ngIf="sequenceForm.get('carnetType')?.errors?.['required']">
|
||||
Carnet type is required
|
||||
</mat-error>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Region</mat-label>
|
||||
<mat-select formControlName="region" required>
|
||||
<mat-option *ngFor="let region of regions" [value]="region.id">
|
||||
{{ region.regionname }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="sequenceForm.get('region')?.errors?.['required']">
|
||||
Region is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Start Number</mat-label>
|
||||
<input matInput formControlName="startNumber" type="number" required>
|
||||
<mat-error *ngIf="sequenceForm.get('startNumber')?.errors?.['required']">
|
||||
Start number is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="sequenceForm.get('startNumber')?.errors?.['pattern']">
|
||||
Must be a valid number
|
||||
</mat-error>
|
||||
<mat-error *ngIf="sequenceForm.get('startNumber')?.errors?.['min']">
|
||||
Must be greater than 0
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>End Number</mat-label>
|
||||
<input matInput formControlName="endNumber" type="number" required>
|
||||
<mat-error *ngIf="sequenceForm.get('endNumber')?.errors?.['required']">
|
||||
End number is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="sequenceForm.get('endNumber')?.errors?.['pattern']">
|
||||
Must be a valid number
|
||||
</mat-error>
|
||||
<mat-error *ngIf="sequenceForm.get('endNumber')?.errors?.['min']">
|
||||
Must be greater than 0
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-error *ngIf="sequenceForm?.errors?.['invalidRange']" class="form-error">
|
||||
End number must be greater than start number
|
||||
</mat-error>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="sequenceForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,159 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.carnet-sequence-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-column-startNumber,
|
||||
.mat-column-endNumber,
|
||||
.mat-column-lastNumber {
|
||||
text-align: right;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 8px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin-top: -16px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
::ng-deep .mat-radio-button.mat-accent .mat-radio-outer-circle {
|
||||
border-color: #3f51b5;
|
||||
}
|
||||
|
||||
::ng-deep .mat-radio-button.mat-accent .mat-radio-inner-circle {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.carnet-sequence-container {
|
||||
padding: 16px;
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { CarnetSequence } from '../../core/models/service-provider/carnet-sequence';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { CommonService } from '../../core/services/common.service';
|
||||
import { Region } from '../../core/models/region';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { CarnetSequenceService } from '../../core/services/carnet-sequence.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-carnet-sequence',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './carnet-sequence.component.html',
|
||||
styleUrl: './carnet-sequence.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class CarnetSequenceComponent implements OnInit {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['carnetType', 'region', 'startNumber', 'endNumber', 'lastNumber'];
|
||||
dataSource = new MatTableDataSource<CarnetSequence>();
|
||||
sequenceForm: FormGroup;
|
||||
isEditing = false;
|
||||
currentSequenceId: string | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
|
||||
carnetTypes = [
|
||||
{ label: 'Original', value: 'ORIGINAL' },
|
||||
{ label: 'Replacement', value: 'REPLACE' }
|
||||
];
|
||||
|
||||
@Input() spid: number = 0;
|
||||
@Input() userPreferences!: UserPreferences;
|
||||
@Input() isEditMode = false;
|
||||
@Output() hasCarnetSequence = new EventEmitter<boolean>();
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
sequences: CarnetSequence[] = [];
|
||||
regions: Region[] = [];
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private carnetSequenceService: CarnetSequenceService,
|
||||
private notificationService: NotificationService,
|
||||
private commonService: CommonService,
|
||||
private dialog: MatDialog,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.sequenceForm = this.fb.group({
|
||||
carnetType: ['ORIGINAL', Validators.required],
|
||||
region: ['', Validators.required],
|
||||
startNumber: ['', [
|
||||
Validators.required,
|
||||
Validators.pattern('^[0-9]*$'),
|
||||
Validators.min(1)
|
||||
]],
|
||||
endNumber: ['', [
|
||||
Validators.required,
|
||||
Validators.pattern('^[0-9]*$'),
|
||||
Validators.min(1)
|
||||
]]
|
||||
}, { validator: this.validateNumberRange });
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadRegions();
|
||||
this.loadSequences();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
private validateNumberRange(group: FormGroup): { [key: string]: any } | null {
|
||||
const start = +group.get('startNumber')?.value;
|
||||
const end = +group.get('endNumber')?.value;
|
||||
return start && end && start >= end ? { invalidRange: true } : null;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
loadRegions(): void {
|
||||
this.commonService.getRegions()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (regions) => {
|
||||
this.regions = regions;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load regions', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private loadSequences(): void {
|
||||
if (!this.spid) return;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.carnetSequenceService.getCarnetSequenceById(this.spid).subscribe({
|
||||
next: (carnetSequences: CarnetSequence[]) => {
|
||||
// this.sequences = carnetSequences;
|
||||
this.isLoading = false;
|
||||
this.dataSource.data = carnetSequences;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load sequences');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading sequences:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addSequence(): void {
|
||||
if (this.sequenceForm.invalid) {
|
||||
this.sequenceForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const sequenceData = {
|
||||
...this.sequenceForm.value,
|
||||
spid: this.spid,
|
||||
lastNumber: this.sequenceForm.value.startNumber
|
||||
};
|
||||
|
||||
this.carnetSequenceService.createCarnetSequence(sequenceData).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Sequence added successfully');
|
||||
this.loadSequences();
|
||||
this.hasCarnetSequence.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to add sequence');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error adding sequence:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// applyFilter(event: Event): void {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
// this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
|
||||
// if (this.dataSource.paginator) {
|
||||
// this.dataSource.paginator.firstPage();
|
||||
// }
|
||||
// }
|
||||
|
||||
addNewSequence(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentSequenceId = null;
|
||||
this.sequenceForm.reset({ carnetType: 'ORIGINAL' });
|
||||
}
|
||||
|
||||
saveSequence(): void {
|
||||
if (this.sequenceForm.invalid) {
|
||||
this.sequenceForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const sequenceData: CarnetSequence = {
|
||||
...this.sequenceForm.value,
|
||||
spid: this.spid
|
||||
};
|
||||
|
||||
this.carnetSequenceService.createCarnetSequence(sequenceData)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Sequence added successfully');
|
||||
this.loadSequences();
|
||||
this.cancelEdit();
|
||||
this.hasCarnetSequence.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to add sequence`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error adding sequence:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteSequence(sequenceId: string): void {
|
||||
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
// width: '350px',
|
||||
// data: {
|
||||
// title: 'Confirm Delete',
|
||||
// message: 'Are you sure you want to delete this sequence?',
|
||||
// confirmText: 'Delete',
|
||||
// cancelText: 'Cancel'
|
||||
// }
|
||||
// });
|
||||
|
||||
// dialogRef.afterClosed().subscribe(result => {
|
||||
// if (result) {
|
||||
// this.carnetSequenceService.deleteSequence(sequenceId).subscribe({
|
||||
// next: () => {
|
||||
// this.notificationService.showSuccess('Sequence deleted successfully');
|
||||
// this.loadSequences();
|
||||
// this.saved.emit(true);
|
||||
// },
|
||||
// error: (error) => {
|
||||
// this.notificationService.showError('Failed to delete sequence');
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentSequenceId = null;
|
||||
this.sequenceForm.reset({ carnetType: 'ORIGINAL' });
|
||||
}
|
||||
|
||||
getCarnetTypeLabel(type: string): string {
|
||||
return this.carnetTypes.find(t => t.value === type)?.label || type;
|
||||
}
|
||||
|
||||
getRegionLabel(type: number): string {
|
||||
return this.regions.find(t => t.id === type)?.regionname || type.toString();
|
||||
}
|
||||
}
|
||||
253
src/app/service-provider/contacts/contacts.component.html
Normal file
253
src/app/service-provider/contacts/contacts.component.html
Normal file
@ -0,0 +1,253 @@
|
||||
<div class="contacts-container">
|
||||
<div class="actions-bar">
|
||||
<!-- <mat-form-field appearance="outline">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input matInput (keyup)="applyFilter($event)" placeholder="Search contacts...">
|
||||
</mat-form-field> -->
|
||||
<button mat-raised-button color="primary" (click)="addNewContact()">
|
||||
<mat-icon>add</mat-icon> Add New Contact
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- First Name Column -->
|
||||
<ng-container matColumnDef="firstName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.firstName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Last Name Column -->
|
||||
<ng-container matColumnDef="lastName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.lastName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Title Column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Title</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.title }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Phone Column -->
|
||||
<ng-container matColumnDef="phone">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Phone</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.phone | phone }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Email Column -->
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.email }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Default Contact Column -->
|
||||
<ng-container matColumnDef="defaultContact">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Default</th>
|
||||
<td mat-cell *matCellDef="let contact">
|
||||
<mat-icon [color]="contact.defaultContact ? 'primary' : ''">
|
||||
{{ contact.defaultContact ? 'star' : 'star_border' }}
|
||||
</mat-icon>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let contact">
|
||||
<button mat-icon-button color="primary" (click)="editContact(contact)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="!contact.defaultContact || !contact.isInactive" (click)="
|
||||
deleteContact(contact.spContactId)" [hidden]="contact.defaultContact || contact.isInactive"
|
||||
matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<!-- <button mat-icon-button (click)="setDefaultContact(contact.contactId)"
|
||||
[color]="contact.defaultContact ? 'primary' : ''" matTooltip="Set as default">
|
||||
<mat-icon>star</mat-icon>
|
||||
</button> -->
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No records available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="contactForm" (ngSubmit)="saveContact()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Contact' : 'Add New Contact' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('firstName')?.errors?.['required']">
|
||||
First name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('firstName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="small-field">
|
||||
<mat-label>Middle Initial</mat-label>
|
||||
<input matInput formControlName="middleInitial" maxlength="1">
|
||||
<mat-error *ngIf="contactForm.get('middleInitial')?.errors?.['maxlength']">
|
||||
Only 1 character allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('lastName')?.errors?.['required']">
|
||||
Last name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('lastName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Title</mat-label>
|
||||
<input matInput formControlName="title" required>
|
||||
<mat-icon matSuffix>work</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['required']">
|
||||
Title is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('phone')?.errors?.['required']">
|
||||
Phone is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('phone')?.errors?.['pattern']">
|
||||
Please enter a valid phone number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Mobile</mat-label>
|
||||
<input matInput formControlName="mobile">
|
||||
<mat-icon matSuffix>smartphone</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['required']">
|
||||
Mobile is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['pattern']">
|
||||
Please enter a valid mobile number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax</mat-label>
|
||||
<input matInput formControlName="fax">
|
||||
<mat-icon matSuffix>fax</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('fax')?.errors?.['required']">
|
||||
Fax is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('fax')?.errors?.['pattern']">
|
||||
Please enter a valid fax number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" required>
|
||||
<mat-icon matSuffix>email</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['required']">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['email']">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!--
|
||||
<div class="form-row">
|
||||
<mat-checkbox formControlName="defaultContact">Default Contact</mat-checkbox>
|
||||
</div> -->
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{contactReadOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inactive status -->
|
||||
<div class="readonly-field">
|
||||
<label>Inactive Status </label>
|
||||
<div class="readonly-value">
|
||||
{{contactReadOnlyFields.isInactive === true ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(contactReadOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inactivated Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Inactivated Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(contactReadOnlyFields.inactivatedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="contactForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
171
src/app/service-provider/contacts/contacts.component.scss
Normal file
171
src/app/service-provider/contacts/contacts.component.scss
Normal file
@ -0,0 +1,171 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.contacts-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-column-defaultContact {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.small-field {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.contacts-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
|
||||
.small-field {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
209
src/app/service-provider/contacts/contacts.component.ts
Normal file
209
src/app/service-provider/contacts/contacts.component.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { Contact } from '../../core/models/service-provider/contact';
|
||||
import { PhonePipe } from '../../shared/pipes/phone.pipe';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { ContactService } from '../../core/services/contact.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contacts',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, PhonePipe, CommonModule],
|
||||
templateUrl: './contacts.component.html',
|
||||
styleUrl: './contacts.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class ContactsComponent implements OnInit {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'email', 'defaultContact', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
contactForm: FormGroup;
|
||||
isEditing = false;
|
||||
currentContactId: number | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
|
||||
contactReadOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null,
|
||||
isInactive: null,
|
||||
inactivatedDate: null
|
||||
};
|
||||
|
||||
@Input() spid: number = 0;
|
||||
@Input() userPreferences: UserPreferences = {};
|
||||
@Output() hasContacts = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private contactService: ContactService,
|
||||
private notificationService: NotificationService,
|
||||
private dialog: MatDialog,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.contactForm = this.fb.group({
|
||||
firstName: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
lastName: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
middleInitial: ['', [Validators.maxLength(1)]],
|
||||
title: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
phone: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
|
||||
mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
|
||||
fax: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
|
||||
email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]],
|
||||
defaultContact: [false]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadContacts();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
loadContacts(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
this.contactService.getContactsById(this.spid).subscribe({
|
||||
next: (contacts: Contact[]) => {
|
||||
this.dataSource.data = contacts;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading contacts:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// applyFilter(event: Event): void {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
// this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
|
||||
// if (this.dataSource.paginator) {
|
||||
// this.dataSource.paginator.firstPage();
|
||||
// }
|
||||
// }
|
||||
|
||||
addNewContact(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentContactId = null;
|
||||
this.contactForm.reset();
|
||||
// this.contactForm.patchValue({ defaultContact: false });
|
||||
}
|
||||
|
||||
editContact(contact: Contact): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentContactId = contact.spContactId;
|
||||
this.contactForm.patchValue({
|
||||
firstName: contact.firstName,
|
||||
lastName: contact.lastName,
|
||||
middleInitial: contact.middleInitial,
|
||||
title: contact.title,
|
||||
phone: contact.phone,
|
||||
mobile: contact.mobile,
|
||||
fax: contact.fax,
|
||||
email: contact.email
|
||||
// defaultContact: contact.defaultContact
|
||||
});
|
||||
|
||||
this.contactReadOnlyFields.lastChangedDate = contact.lastUpdatedDate ?? contact.dateCreated;
|
||||
this.contactReadOnlyFields.lastChangedBy = contact.lastUpdatedBy ?? contact.createdBy;
|
||||
this.contactReadOnlyFields.isInactive = contact.isInactive;
|
||||
this.contactReadOnlyFields.inactivatedDate = contact.inactivatedDate;
|
||||
}
|
||||
|
||||
saveContact(): void {
|
||||
if (this.contactForm.invalid) {
|
||||
this.contactForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
// default the first contact
|
||||
const contactData: Contact = this.contactForm.value;
|
||||
contactData.defaultContact = this.dataSource?.data?.length === 0;
|
||||
|
||||
const saveObservable = this.isEditing && (this.currentContactId! > 0)
|
||||
? this.contactService.updateContact(this.currentContactId!, contactData)
|
||||
: this.contactService.createContact(this.spid, contactData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Contact ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadContacts();
|
||||
this.cancelEdit();
|
||||
this.hasContacts.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} contact`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving contact:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteContact(contactId: string): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Delete',
|
||||
message: 'Are you sure you want to delete this contact?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.contactService.deleteContact(contactId).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Contact deleted successfully');
|
||||
this.loadContacts();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete contact');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error deleting contact:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentContactId = null;
|
||||
this.contactForm.reset();
|
||||
}
|
||||
|
||||
// setDefaultContact(contactId: string): void {
|
||||
// this.contactService.setDefaultServiceProviderContact(this.spid, contactId).subscribe({
|
||||
// next: () => {
|
||||
// this.notificationService.showSuccess('Default contact updated successfully');
|
||||
// this.loadContacts();
|
||||
// },
|
||||
// error: (error) => {
|
||||
// this.notificationService.showError('Failed to set default contact');
|
||||
// console.error('Error setting default contact:', error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
<div class="continuation-sheet-container">
|
||||
<div class="actions-bar">
|
||||
<button mat-raised-button color="primary" (click)="addNewContinuationSheet()">
|
||||
<mat-icon>add</mat-icon> Add New Continuation Sheet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Customer Type Column -->
|
||||
<ng-container matColumnDef="customerType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Customer Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ getOptionLabel(customerTypes, item.customerType) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Carnet Type Column -->
|
||||
<ng-container matColumnDef="carnetType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ getOptionLabel(carnetTypes, item.carnetType) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Rate Column -->
|
||||
<ng-container matColumnDef="rate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Rate</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.rate | currency }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Effective Date Column -->
|
||||
<ng-container matColumnDef="effectiveDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.effectiveDate | date:'mediumDate':'UTC' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button color="primary" (click)="editContinuationSheet(item)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<!-- <button mat-icon-button color="warn" (click)="deleteContinuationSheet(item.id)" matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button> -->
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No continuation sheet setups available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Continuation Sheet Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="continuationSheetForm" (ngSubmit)="saveContinuationSheet()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Continuation Sheet' : 'Add New Continuation Sheet' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-label>Customer Type</mat-label>
|
||||
<mat-radio-group formControlName="customerType" required class="horizontal-group">
|
||||
<mat-radio-button *ngFor="let type of customerTypes" [value]="type.value" class="radio-button">
|
||||
{{ type.label }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-error *ngIf="continuationSheetForm.get('customerType')?.errors?.['required']">
|
||||
Customer type is required
|
||||
</mat-error>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-label>Carnet Type</mat-label>
|
||||
<mat-radio-group formControlName="carnetType" required class="horizontal-group">
|
||||
<mat-radio-button *ngFor="let type of carnetTypes" [value]="type.value">
|
||||
{{ type.label }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-error *ngIf="continuationSheetForm.get('carnetType')?.errors?.['required']" class="radio-button">
|
||||
Carnet type is required
|
||||
</mat-error>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Rate</mat-label>
|
||||
<input matInput formControlName="rate" type="number" step="1" required>
|
||||
<span class="dollar-prefix" matPrefix>$ </span>
|
||||
<mat-error *ngIf="continuationSheetForm.get('rate')?.errors?.['required']">
|
||||
Rate is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="continuationSheetForm.get('rate')?.errors?.['pattern']">
|
||||
Must be a valid dollar amount
|
||||
</mat-error>
|
||||
<mat-error *ngIf="continuationSheetForm.get('rate')?.errors?.['min']">
|
||||
Must be 0 or greater
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Effective Date</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="continuationSheetForm.get('effectiveDate')?.errors?.['required']">
|
||||
Effective date is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{readOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(readOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="continuationSheetForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,184 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.continuation-sheet-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin-top: -16px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
::ng-deep .mat-radio-button.mat-accent .mat-radio-outer-circle {
|
||||
border-color: #3f51b5;
|
||||
}
|
||||
|
||||
::ng-deep .mat-radio-button.mat-accent .mat-radio-inner-circle {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
|
||||
.dollar-prefix {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.continuation-sheet-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { of } from 'rxjs';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { ContinuationSheetFeeService } from '../../core/services/continuation-sheet-fee.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-continuation-sheet-fee',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './continuation-sheet-fee.component.html',
|
||||
styleUrl: './continuation-sheet-fee.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class ContinuationSheetFeeComponent implements OnInit {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['customerType', 'carnetType', 'rate', 'effectiveDate', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
continuationSheetForm: FormGroup;
|
||||
isEditing = false;
|
||||
currentContinuationSheetId: number | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
|
||||
readOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null
|
||||
};
|
||||
|
||||
// Dropdown options
|
||||
customerTypes = [
|
||||
{ label: 'Preparer', value: 'PREPARER' },
|
||||
{ label: 'Self Issuer', value: 'SELFISSUER' }
|
||||
];
|
||||
|
||||
carnetTypes = [
|
||||
{ label: 'Original', value: 'ORIGINAL' },
|
||||
{ label: 'Re-order', value: 'REORDER' },
|
||||
{ label: 'Replacement', value: 'REPLACE' }
|
||||
];
|
||||
|
||||
@Input() isEditMode = false;
|
||||
@Input() spid: number = 0;
|
||||
@Input() userPreferences!: UserPreferences;
|
||||
@Output() hasContinuationSheetFee = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private continuationSheetFeeService: ContinuationSheetFeeService,
|
||||
private notificationService: NotificationService,
|
||||
private dialog: MatDialog,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.continuationSheetForm = this.fb.group({
|
||||
customerType: ['PREPARER', Validators.required],
|
||||
carnetType: ['ORIGINAL', Validators.required],
|
||||
rate: ['', [
|
||||
Validators.required,
|
||||
Validators.pattern(/^\d+\.?\d{0,2}$/),
|
||||
Validators.min(0)
|
||||
]],
|
||||
effectiveDate: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadContinuationSheets();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
loadContinuationSheets(): void {
|
||||
if (!this.spid) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.continuationSheetFeeService.getContinuationSheets(this.spid).subscribe({
|
||||
next: (continuationSheets) => {
|
||||
this.dataSource.data = continuationSheets;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load continuation sheets');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
return of([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// applyFilter(event: Event): void {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
// this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
|
||||
// if (this.dataSource.paginator) {
|
||||
// this.dataSource.paginator.firstPage();
|
||||
// }
|
||||
// }
|
||||
|
||||
addNewContinuationSheet(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentContinuationSheetId = null;
|
||||
this.continuationSheetForm.reset({
|
||||
customerType: 'PREPARER',
|
||||
carnetType: 'ORIGINAL'
|
||||
});
|
||||
|
||||
this.continuationSheetForm.get('customerType')?.enable();
|
||||
this.continuationSheetForm.get('carnetType')?.enable();
|
||||
}
|
||||
|
||||
editContinuationSheet(continuationSheet: any): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentContinuationSheetId = continuationSheet.id;
|
||||
this.continuationSheetForm.patchValue({
|
||||
customerType: continuationSheet.customerType,
|
||||
carnetType: continuationSheet.carnetType,
|
||||
rate: continuationSheet.rate,
|
||||
effectiveDate: new Date(continuationSheet.effectiveDate)
|
||||
});
|
||||
|
||||
this.readOnlyFields.lastChangedDate = continuationSheet.dateCreated;
|
||||
this.readOnlyFields.lastChangedBy = continuationSheet.createdBy;
|
||||
|
||||
this.continuationSheetForm.get('customerType')?.disable();
|
||||
this.continuationSheetForm.get('carnetType')?.disable();
|
||||
}
|
||||
|
||||
saveContinuationSheet(): void {
|
||||
if (this.continuationSheetForm.invalid) {
|
||||
this.continuationSheetForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const continuationSheetData = {
|
||||
...this.continuationSheetForm.value,
|
||||
spid: this.spid
|
||||
};
|
||||
|
||||
const saveObservable = this.isEditing && this.currentContinuationSheetId
|
||||
? this.continuationSheetFeeService.updateContinuationSheet(this.currentContinuationSheetId, continuationSheetData)
|
||||
: this.continuationSheetFeeService.addContinuationSheet(this.spid, continuationSheetData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Continuation Sheet ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadContinuationSheets();
|
||||
this.cancelEdit();
|
||||
this.hasContinuationSheetFee.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} continuation sheet`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
return of(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteContinuationSheet(continuationSheetId: string): void {
|
||||
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
// width: '350px',
|
||||
// data: {
|
||||
// title: 'Confirm Delete',
|
||||
// message: 'Are you sure you want to delete this continuation sheet setup?',
|
||||
// confirmText: 'Delete',
|
||||
// cancelText: 'Cancel'
|
||||
// }
|
||||
// });
|
||||
|
||||
// dialogRef.afterClosed().subscribe(result => {
|
||||
// if (result) {
|
||||
// this.continuationSheetFeeService.deleteContinuationSheet(continuationSheetId).subscribe({
|
||||
// next: () => {
|
||||
// this.notificationService.showSuccess('Continuation sheet deleted successfully');
|
||||
// this.loadContinuationSheets();
|
||||
// },
|
||||
// error: (error) => {
|
||||
// this.notificationService.showError('Failed to delete continuation sheet');
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentContinuationSheetId = null;
|
||||
this.continuationSheetForm.reset({
|
||||
customerType: 'PREPARER',
|
||||
carnetType: 'ORIGINAL'
|
||||
});
|
||||
}
|
||||
|
||||
getOptionLabel(options: any[], value: string): string {
|
||||
return options.find(opt => opt.value === value)?.label || value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
<div class="counterfoil-container">
|
||||
<div class="actions-bar">
|
||||
<button mat-raised-button color="primary" (click)="addNewCounterfoil()">
|
||||
<mat-icon>add</mat-icon> Add New Counterfoil
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Customer Type Column -->
|
||||
<ng-container matColumnDef="customerType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Customer Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ getOptionLabel(customerTypes, item.customerType) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Carnet Type Column -->
|
||||
<ng-container matColumnDef="carnetType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ getOptionLabel(carnetTypes, item.carnetType) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Start Sets Column -->
|
||||
<ng-container matColumnDef="startSets">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Start Sets</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.startSets }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- End Sets Column -->
|
||||
<ng-container matColumnDef="endSets">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>End Sets</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.endSets }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Rate Column -->
|
||||
<ng-container matColumnDef="rate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Rate</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.rate | currency }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Effective Date Column -->
|
||||
<ng-container matColumnDef="effectiveDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Effective Date</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.effectiveDate | date:'mediumDate':'UTC' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button color="primary" (click)="editCounterfoil(item)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<!-- <button mat-icon-button color="warn" (click)="deleteCounterfoil(item.id)" matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button> -->
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No counterfoil setups available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Counterfoil Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="counterfoilForm" (ngSubmit)="saveCounterfoil()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Counterfoil Setup' : 'Add New Counterfoil Setup' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-label>Customer Type</mat-label>
|
||||
<mat-radio-group formControlName="customerType" required class="horizontal-group">
|
||||
<mat-radio-button *ngFor="let type of customerTypes" [value]="type.value" class="radio-button">
|
||||
{{ type.label }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-error *ngIf="counterfoilForm.get('customerType')?.errors?.['required']">
|
||||
Customer type is required
|
||||
</mat-error>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-label>Carnet Type</mat-label>
|
||||
<mat-radio-group formControlName="carnetType" required class="horizontal-group">
|
||||
<mat-radio-button *ngFor="let type of carnetTypes" [value]="type.value" class="radio-button">
|
||||
{{ type.label }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-error *ngIf="counterfoilForm.get('carnetType')?.errors?.['required']">
|
||||
Carnet type is required
|
||||
</mat-error>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Start Sets</mat-label>
|
||||
<input matInput formControlName="startSets" type="number" required>
|
||||
<mat-error *ngIf="counterfoilForm.get('startSets')?.errors?.['required']">
|
||||
Start sets is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="counterfoilForm.get('startSets')?.errors?.['pattern']">
|
||||
Must be a valid number
|
||||
</mat-error>
|
||||
<mat-error *ngIf="counterfoilForm.get('startSets')?.errors?.['min']">
|
||||
Must be greater than 0
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>End Sets</mat-label>
|
||||
<input matInput formControlName="endSets" type="number" required>
|
||||
<mat-error *ngIf="counterfoilForm.get('endSets')?.errors?.['required']">
|
||||
End sets is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="counterfoilForm.get('endSets')?.errors?.['pattern']">
|
||||
Must be a valid number
|
||||
</mat-error>
|
||||
<mat-error *ngIf="counterfoilForm.get('endSets')?.errors?.['min']">
|
||||
Must be greater than 0
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-error *ngIf="counterfoilForm?.errors?.['invalidRange']" class="form-error">
|
||||
End sets must be greater than start sets
|
||||
</mat-error>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Rate</mat-label>
|
||||
<input matInput formControlName="rate" type="number" step="1" required>
|
||||
<span class="dollar-prefix" matPrefix>$ </span>
|
||||
<mat-error *ngIf="counterfoilForm.get('rate')?.errors?.['required']">
|
||||
Rate is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="counterfoilForm.get('rate')?.errors?.['pattern']">
|
||||
Must be a valid dollar amount
|
||||
</mat-error>
|
||||
<mat-error *ngIf="counterfoilForm.get('rate')?.errors?.['min']">
|
||||
Must be 0 or greater
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Effective Date</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="effectiveDate" required>
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="counterfoilForm.get('effectiveDate')?.errors?.['required']">
|
||||
Effective date is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{readOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(readOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="counterfoilForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,184 @@
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
|
||||
.counterfoil-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin-top: -16px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
::ng-deep .mat-radio-button.mat-accent .mat-radio-outer-circle {
|
||||
border-color: #3f51b5;
|
||||
}
|
||||
|
||||
::ng-deep .mat-radio-button.mat-accent .mat-radio-inner-circle {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
|
||||
.dollar-prefix {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.counterfoil-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { NotificationService } from '../../core/services/notification.service';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CounterfoilFee } from '../../core/models/service-provider/counterfoil-fee';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { CounterfoilFeeService } from '../../core/services/counterfoil-fee.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-counterfoil-fee',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './counterfoil-fee.component.html',
|
||||
styleUrl: './counterfoil-fee.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class CounterfoilFeeComponent implements OnInit {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['customerType', 'carnetType', 'startSets', 'endSets', 'rate', 'effectiveDate', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
counterfoilForm: FormGroup;
|
||||
isEditing = false;
|
||||
currentCounterfoilId: number | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
|
||||
readOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null
|
||||
};
|
||||
|
||||
// Dropdown options
|
||||
customerTypes = [
|
||||
{ label: 'Preparer', value: 'PREPARER' },
|
||||
{ label: 'Self Issuer', value: 'SELFISSUER' }
|
||||
];
|
||||
|
||||
carnetTypes = [
|
||||
{ label: 'Original', value: 'ORIGINAL' },
|
||||
{ label: 'Re-order', value: 'REORDER' },
|
||||
{ label: 'Replacement', value: 'REPLACE' }
|
||||
];
|
||||
|
||||
@Input() isEditMode = false;
|
||||
@Input() spid: number = 0;
|
||||
@Input() userPreferences!: UserPreferences;
|
||||
@Output() hasCounterFoilFee = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private counterfoilFeeService: CounterfoilFeeService,
|
||||
private notificationService: NotificationService,
|
||||
private dialog: MatDialog,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.counterfoilForm = this.fb.group({
|
||||
customerType: ['PREPARER', Validators.required],
|
||||
carnetType: ['ORIGINAL', Validators.required],
|
||||
startSets: ['', [
|
||||
Validators.required,
|
||||
Validators.pattern('^[0-9]*$'),
|
||||
Validators.min(1)
|
||||
]],
|
||||
endSets: ['', [
|
||||
Validators.required,
|
||||
Validators.pattern('^[0-9]*$'),
|
||||
Validators.min(1)
|
||||
]],
|
||||
rate: ['', [
|
||||
Validators.required,
|
||||
Validators.pattern(/^\d+\.?\d{0,2}$/),
|
||||
Validators.min(0)
|
||||
]],
|
||||
effectiveDate: ['', Validators.required]
|
||||
}, { validator: this.validateSetsRange });
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCounterfoils();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
private validateSetsRange(group: FormGroup): { [key: string]: any } | null {
|
||||
const start = +group.get('startSets')?.value;
|
||||
const end = +group.get('endSets')?.value;
|
||||
return start && end && start >= end ? { invalidRange: true } : null;
|
||||
}
|
||||
|
||||
loadCounterfoils(): void {
|
||||
if (!this.spid) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.counterfoilFeeService.getCounterfoils(this.spid)
|
||||
.subscribe({
|
||||
next: (
|
||||
counterfoils: CounterfoilFee[]) => {
|
||||
this.dataSource.data = counterfoils;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load counterfoils');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading counterfoils:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// applyFilter(event: Event): void {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
// this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
|
||||
// if (this.dataSource.paginator) {
|
||||
// this.dataSource.paginator.firstPage();
|
||||
// }
|
||||
// }
|
||||
|
||||
addNewCounterfoil(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentCounterfoilId = null;
|
||||
this.counterfoilForm.reset({
|
||||
customerType: 'PREPARER',
|
||||
carnetType: 'ORIGINAL'
|
||||
});
|
||||
|
||||
this.counterfoilForm.get('customerType')?.enable();
|
||||
this.counterfoilForm.get('carnetType')?.enable();
|
||||
this.counterfoilForm.get('startSets')?.enable();
|
||||
this.counterfoilForm.get('endSets')?.enable();
|
||||
}
|
||||
|
||||
editCounterfoil(counterfoil: any): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentCounterfoilId = counterfoil.id;
|
||||
this.counterfoilForm.patchValue({
|
||||
customerType: counterfoil.customerType,
|
||||
carnetType: counterfoil.carnetType,
|
||||
startSets: counterfoil.startSets,
|
||||
endSets: counterfoil.endSets,
|
||||
rate: counterfoil.rate,
|
||||
effectiveDate: new Date(counterfoil.effectiveDate)
|
||||
});
|
||||
|
||||
this.readOnlyFields.lastChangedDate = counterfoil.dateCreated;
|
||||
this.readOnlyFields.lastChangedBy = counterfoil.createdBy;
|
||||
|
||||
this.counterfoilForm.get('customerType')?.disable();
|
||||
this.counterfoilForm.get('carnetType')?.disable();
|
||||
this.counterfoilForm.get('startSets')?.disable();
|
||||
this.counterfoilForm.get('endSets')?.disable();
|
||||
}
|
||||
|
||||
saveCounterfoil(): void {
|
||||
if (this.counterfoilForm.invalid) {
|
||||
this.counterfoilForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const counterfoilData = {
|
||||
...this.counterfoilForm.value,
|
||||
spid: this.spid
|
||||
};
|
||||
|
||||
const saveObservable = this.isEditing && this.currentCounterfoilId
|
||||
? this.counterfoilFeeService.updateCounterfoil(this.currentCounterfoilId, counterfoilData)
|
||||
: this.counterfoilFeeService.addCounterfoil(this.spid, counterfoilData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Counterfoil ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadCounterfoils();
|
||||
this.cancelEdit();
|
||||
this.hasCounterFoilFee.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} counterfoil`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving counterfoil:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteCounterfoil(counterfoilId: string): void {
|
||||
// const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
// width: '350px',
|
||||
// data: {
|
||||
// title: 'Confirm Delete',
|
||||
// message: 'Are you sure you want to delete this counterfoil setup?',
|
||||
// confirmText: 'Delete',
|
||||
// cancelText: 'Cancel'
|
||||
// }
|
||||
// });
|
||||
|
||||
// dialogRef.afterClosed().subscribe(result => {
|
||||
// if (result) {
|
||||
// this.counterfoilFeeService.deleteCounterfoil(counterfoilId).subscribe({
|
||||
// next: () => {
|
||||
// this.notificationService.showSuccess('Counterfoil deleted successfully');
|
||||
// this.loadCounterfoils();
|
||||
// },
|
||||
// error: (error) => {
|
||||
// this.notificationService.showError('Failed to delete counterfoil');
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentCounterfoilId = null;
|
||||
this.counterfoilForm.reset({
|
||||
customerType: 'PREPARER',
|
||||
carnetType: 'ORIGINAL'
|
||||
});
|
||||
}
|
||||
|
||||
getOptionLabel(options: any[], value: string): string {
|
||||
return options.find(opt => opt.value === value)?.label || value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
<h2 *ngIf="this.serviceProviderName" class="page-header">Manage {{this.serviceProviderName}}</h2>
|
||||
|
||||
<div class="service-provider-action-buttons">
|
||||
<button mat-button (click)="accordion().openAll()">Expand All</button>
|
||||
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
|
||||
</div>
|
||||
<mat-accordion class="service-provider-headers-align" multi>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Basic Details </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<app-basic-details [spid]="spid" [isEditMode]="isEditMode"
|
||||
(serviceProviderName)="onServiceProviderNameUpdate($event)"></app-basic-details>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Contacts </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-contacts [spid]="spid" [userPreferences]="userPreferences"></app-contacts>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Carnet Sequence </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-carnet-sequence [spid]="spid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences"></app-carnet-sequence>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Carnet Fee & Commission Setup </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-carnet-fee [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences"></app-carnet-fee>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Basic Fee Setup</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-basic-fee [spid]="spid" [isEditMode]="isEditMode" [userPreferences]="userPreferences"></app-basic-fee>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Counterfoil Setup</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-counterfoil-fee [spid]="spid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences"></app-counterfoil-fee>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Continuation Sheet Fee Setup </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-continuation-sheet-fee [spid]="spid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences"></app-continuation-sheet-fee>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Expedited Fee Setup</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-expedited-fee [spid]="spid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences"></app-expedited-fee>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Security Deposit </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-security-deposit [spid]="spid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences"></app-security-deposit>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
@ -0,0 +1,20 @@
|
||||
@import 'colors';
|
||||
|
||||
.page-header {
|
||||
margin: 0.5rem 0px;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-provider-action-buttons {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.service-provider-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-provider-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { afterNextRender, Component, viewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { MatAccordion } from '@angular/material/expansion';
|
||||
import { BasicDetailsComponent } from '../basic-details/basic-details.component';
|
||||
import { ContactsComponent } from '../contacts/contacts.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CarnetSequenceComponent } from '../carnet-sequence/carnet-sequence.component';
|
||||
import { CarnetFeeComponent } from "../carnet-fee/carnet-fee.component";
|
||||
import { BasicFeeComponent } from "../basic-fee/basic-fee.component";
|
||||
import { CounterfoilFeeComponent } from "../counterfoil-fee/counterfoil-fee.component";
|
||||
import { ContinuationSheetFeeComponent } from "../continuation-sheet-fee/continuation-sheet-fee.component";
|
||||
import { ExpeditedFeeComponent } from "../expedited-fee/expedited-fee.component";
|
||||
import { SecurityDepositComponent } from "../security-deposit/security-deposit.component";
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-service-provider',
|
||||
imports: [AngularMaterialModule, BasicDetailsComponent, ContactsComponent, CarnetSequenceComponent, CarnetFeeComponent, BasicFeeComponent, CounterfoilFeeComponent, ContinuationSheetFeeComponent, ExpeditedFeeComponent, SecurityDepositComponent, CommonModule],
|
||||
templateUrl: './edit-service-provider.component.html',
|
||||
styleUrl: './edit-service-provider.component.scss'
|
||||
})
|
||||
export class EditServiceProviderComponent {
|
||||
accordion = viewChild.required(MatAccordion);
|
||||
isEditMode = true;
|
||||
spid = 0;
|
||||
serviceProviderName: string | null = null;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
afterNextRender(() => {
|
||||
// Open all panels
|
||||
this.accordion().openAll();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('id');
|
||||
this.spid = idParam ? parseInt(idParam, 10) : 0;
|
||||
}
|
||||
|
||||
onServiceProviderNameUpdate(event: string): void {
|
||||
this.serviceProviderName = event;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user