all Data Export API changes from April 28 2026
Some checks are pending
Server Tests / Mocha – Unit & Utility Tests (push) Waiting to run
Some checks are pending
Server Tests / Mocha – Unit & Utility Tests (push) Waiting to run
This commit is contained in:
parent
df31b2080d
commit
9303274349
@ -52,6 +52,11 @@
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/leaflet/dist/images",
|
||||
"output": "/assets/images"
|
||||
},
|
||||
{
|
||||
"glob": "CHANGELOG.md",
|
||||
"input": "docs",
|
||||
"output": "/assets/docs"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
|
||||
8
Development/client/docs/CHANGELOG.md
Normal file
8
Development/client/docs/CHANGELOG.md
Normal file
@ -0,0 +1,8 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [revision 1603] - 2026-04-28
|
||||
|
||||
- Created the changelog file
|
||||
108
Development/client/package-lock.json
generated
108
Development/client/package-lock.json
generated
@ -40,8 +40,10 @@
|
||||
"geodesy": "^1.1.3",
|
||||
"intl": "^1.2.5",
|
||||
"leaflet": "^1.9.4",
|
||||
"marked": "^1.2.9",
|
||||
"ngrx-store-localstorage": "^9.0.0",
|
||||
"ngx-captcha": "^8.0.1",
|
||||
"ngx-markdown": "^9.1.1",
|
||||
"primeng-lts": "^9.2.8",
|
||||
"quill": "^1.3.7",
|
||||
"rbush": "^3.0.1",
|
||||
@ -2838,6 +2840,11 @@
|
||||
"@types/leaflet": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.7.4.tgz",
|
||||
"integrity": "sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw=="
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-6.0.0.tgz",
|
||||
@ -5032,8 +5039,7 @@
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
@ -6455,6 +6461,11 @@
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/emoji-toolkit": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-5.5.1.tgz",
|
||||
"integrity": "sha512-H8E6DNTsRLgy1FVWAiyuW4nqHka0rvUkXhmJPzL28gXo4pLKvuoEi6VhodJ1RfIZOZZ7Zmxo1sENYinyytl/ww=="
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
@ -10165,6 +10176,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.11.1.tgz",
|
||||
"integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
|
||||
"dependencies": {
|
||||
"commander": "^2.19.0"
|
||||
},
|
||||
"bin": {
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@ -10660,6 +10682,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.9.tgz",
|
||||
"integrity": "sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==",
|
||||
"bin": {
|
||||
"marked": "bin/marked"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.16.2"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -11268,6 +11301,26 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-markdown": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-9.1.1.tgz",
|
||||
"integrity": "sha512-dEuR1KBa/Ivb1HT+DvSW1p6wLSx79EZz8/WpgDxiEZfL1PADTQUziNLgmtwAEgBTDjsXFhZ/Af7Zq5J9dQf6KQ==",
|
||||
"dependencies": {
|
||||
"@types/marked": "^0.7.4",
|
||||
"emoji-toolkit": "^5.5.0",
|
||||
"katex": "^0.11.0",
|
||||
"marked": "^1.1.0",
|
||||
"prismjs": "^1.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^8.0.0 || ^9.0.0",
|
||||
"@angular/core": "^8.0.0 || ^9.0.0",
|
||||
"@angular/platform-browser": "^8.0.0 || ^9.0.0",
|
||||
"rxjs": "^6.0.0",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "^0.9.0 || ^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@ -13301,6 +13354,14 @@
|
||||
"zone.js": "^0.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
@ -21768,6 +21829,11 @@
|
||||
"@types/leaflet": "*"
|
||||
}
|
||||
},
|
||||
"@types/marked": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.7.4.tgz",
|
||||
"integrity": "sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw=="
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-6.0.0.tgz",
|
||||
@ -23550,8 +23616,7 @@
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"commondir": {
|
||||
"version": "1.0.1",
|
||||
@ -24713,6 +24778,11 @@
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"emoji-toolkit": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-5.5.1.tgz",
|
||||
"integrity": "sha512-H8E6DNTsRLgy1FVWAiyuW4nqHka0rvUkXhmJPzL28gXo4pLKvuoEi6VhodJ1RfIZOZZ7Zmxo1sENYinyytl/ww=="
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
@ -27599,6 +27669,14 @@
|
||||
"source-map-support": "^0.5.5"
|
||||
}
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.11.1.tgz",
|
||||
"integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
|
||||
"requires": {
|
||||
"commander": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@ -28002,6 +28080,11 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.9.tgz",
|
||||
"integrity": "sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw=="
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -28492,6 +28575,18 @@
|
||||
"xmldom": "^0.1.27"
|
||||
}
|
||||
},
|
||||
"ngx-markdown": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-9.1.1.tgz",
|
||||
"integrity": "sha512-dEuR1KBa/Ivb1HT+DvSW1p6wLSx79EZz8/WpgDxiEZfL1PADTQUziNLgmtwAEgBTDjsXFhZ/Af7Zq5J9dQf6KQ==",
|
||||
"requires": {
|
||||
"@types/marked": "^0.7.4",
|
||||
"emoji-toolkit": "^5.5.0",
|
||||
"katex": "^0.11.0",
|
||||
"marked": "^1.1.0",
|
||||
"prismjs": "^1.20.0"
|
||||
}
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@ -30193,6 +30288,11 @@
|
||||
"integrity": "sha512-0cxWuVEuruMFT5GovcnNSqyzn+f/qgCUdEXHEcJ297ySp5P1S5HB8IfsmHkCEpI8BDU+X6k3seP9FtzWeqxigw==",
|
||||
"requires": {}
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
|
||||
@ -58,8 +58,10 @@
|
||||
"geodesy": "^1.1.3",
|
||||
"intl": "^1.2.5",
|
||||
"leaflet": "^1.9.4",
|
||||
"marked": "^1.2.9",
|
||||
"ngrx-store-localstorage": "^9.0.0",
|
||||
"ngx-captcha": "^8.0.1",
|
||||
"ngx-markdown": "^9.1.1",
|
||||
"primeng-lts": "^9.2.8",
|
||||
"quill": "^1.3.7",
|
||||
"rbush": "^3.0.1",
|
||||
|
||||
@ -74,6 +74,11 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./tools/dlq-monitor/dlq-monitor.module').then(m => m.DlqMonitorModule),
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'dealers',
|
||||
loadChildren: () => import('./tools/dealers/dealers.module').then(m => m.DealersModule),
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'track',
|
||||
loadChildren: () => import('./track/track.module').then(m => m.TrackModule),
|
||||
@ -100,6 +105,11 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./settings/api-keys/api-keys.module').then(m => m.ApiKeysModule),
|
||||
runGuardsAndResolvers: 'always'
|
||||
},
|
||||
{
|
||||
path: 'changelog',
|
||||
loadChildren: () => import('./changelog/changelog.module').then(m => m.ChangelogModule),
|
||||
runGuardsAndResolvers: 'always'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -39,6 +39,7 @@ export class AppMenuComponent implements OnInit {
|
||||
const mItems: MenuItem[] = [
|
||||
{ id: 'dashboard', label: $localize`:@@dashboard:Dashboard`, icon: 'dashboard', routerLink: ['/home'] },
|
||||
{ id: 'customers', label: $localize`:@@customers:Customers`, icon: 'assignment_ind', routerLink: ['/customers'] },
|
||||
{ id: 'dealers', label: $localize`:@@dealers:Dealers`, icon: 'store', routerLink: ['/dealers'] },
|
||||
{ id: 'partners', label: $localize`:@@partnerMgnt:Partner Management`, icon: 'business', routerLink: ['/partners'] },
|
||||
{ label: $localize`:@@billing:Billing`, icon: 'monetization_on', routerLink: ['/billing'] },
|
||||
{
|
||||
@ -58,6 +59,7 @@ export class AppMenuComponent implements OnInit {
|
||||
{ id: 'subscription', label: $localize`:@@promoManagement:Promo Management`, icon: 'credit_card', routerLink: ['/settings/subscription'] }
|
||||
]
|
||||
},
|
||||
{ id: 'changelog', label: $localize`:@@changelog:Changelog`, icon: 'history', routerLink: ['/changelog'] },
|
||||
];
|
||||
this.model = mItems;
|
||||
}
|
||||
@ -219,7 +221,7 @@ export class AppMenuComponent implements OnInit {
|
||||
{ id: 'upload', label: $localize`:@@uploadJobData:Upload Job Data`, icon: 'cloud_upload', routerLink: ['/tools/upload'] },
|
||||
{ id: 'areaLib', label: $localize`:@@manageAreasLib:Manage Areas Library`, icon: 'folder_special', routerLink: ['/tools/areas'] },
|
||||
{ id: 'settings', label: $localize`:@@settings:Settings`, icon: 'settings', routerLink: ['/tools/settings'] },
|
||||
...( this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM])
|
||||
...( this.authSvc.hasRole([RoleIds.APP])
|
||||
? [{ id: 'api-keys', label: $localize`:@@apiKeys:API Keys`, icon: 'vpn_key', routerLink: ['/api-keys'] }]
|
||||
: [] )
|
||||
]
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AuthGuard } from '../domain/guards/auth.guard';
|
||||
import { ChangelogComponent } from './changelog.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ChangelogComponent,
|
||||
canActivate: [AuthGuard]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ChangelogRoutingModule { }
|
||||
@ -0,0 +1,20 @@
|
||||
<div class="ui-g">
|
||||
<div class="ui-g-12">
|
||||
<div class="card card-w-title">
|
||||
<h1 i18n="@@changelog">Changelog</h1>
|
||||
<div *ngIf="loading" style="text-align:center; padding: 2rem;">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
</div>
|
||||
<ng-container *ngIf="!loading">
|
||||
<p-panel
|
||||
*ngFor="let section of sections; let i = index"
|
||||
[header]="section.header"
|
||||
[toggleable]="true"
|
||||
[collapsed]="i !== 0"
|
||||
styleClass="changelog-panel">
|
||||
<div class="changelog-content" [innerHTML]="section.content"></div>
|
||||
</p-panel>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
62
Development/client/src/app/changelog/changelog.component.ts
Normal file
62
Development/client/src/app/changelog/changelog.component.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import * as marked from 'marked';
|
||||
const parseMarkdown: (src: string) => string = (marked as any).marked ?? (marked as any).default ?? (marked as any);
|
||||
|
||||
interface ChangelogSection {
|
||||
header: string;
|
||||
content: SafeHtml;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-changelog',
|
||||
templateUrl: './changelog.component.html'
|
||||
})
|
||||
export class ChangelogComponent implements OnInit {
|
||||
sections: ChangelogSection[] = [];
|
||||
loading = true;
|
||||
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
private readonly sanitizer: DomSanitizer
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.http.get('/assets/docs/CHANGELOG.md', { responseType: 'text' }).subscribe({
|
||||
next: (md) => {
|
||||
this.sections = this.parseSections(md);
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => { this.loading = false; }
|
||||
});
|
||||
}
|
||||
|
||||
private parseSections(md: string): ChangelogSection[] {
|
||||
const sections: ChangelogSection[] = [];
|
||||
let currentHeader = '';
|
||||
let currentBody = '';
|
||||
|
||||
for (const line of md.split('\n')) {
|
||||
if (line.startsWith('## ')) {
|
||||
if (currentHeader) {
|
||||
sections.push({
|
||||
header: currentHeader,
|
||||
content: this.sanitizer.bypassSecurityTrustHtml(parseMarkdown(currentBody.trim()))
|
||||
});
|
||||
}
|
||||
currentHeader = line.replace(/^##\s*/, '');
|
||||
currentBody = '';
|
||||
} else if (currentHeader) {
|
||||
currentBody += line + '\n';
|
||||
}
|
||||
}
|
||||
if (currentHeader) {
|
||||
sections.push({
|
||||
header: currentHeader,
|
||||
content: this.sanitizer.bypassSecurityTrustHtml(parseMarkdown(currentBody.trim()))
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
}
|
||||
20
Development/client/src/app/changelog/changelog.module.ts
Normal file
20
Development/client/src/app/changelog/changelog.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { ProgressSpinnerModule } from 'primeng/progressspinner';
|
||||
import { PanelModule } from 'primeng/panel';
|
||||
|
||||
import { AppSharedModule } from '../shared/app-shared.module';
|
||||
import { ChangelogRoutingModule } from './changelog-routing.module';
|
||||
import { ChangelogComponent } from './changelog.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AppSharedModule,
|
||||
HttpClientModule,
|
||||
ProgressSpinnerModule,
|
||||
PanelModule,
|
||||
ChangelogRoutingModule
|
||||
],
|
||||
declarations: [ChangelogComponent]
|
||||
})
|
||||
export class ChangelogModule { }
|
||||
@ -56,6 +56,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dealer Selection -->
|
||||
<div class="ui-g-12 ui-md-6 ui-lg-6 form-row">
|
||||
<span class="form-label-span">Dealer:</span>
|
||||
<p-dropdown id="dealer" name="dealer" formControlName="dealer"
|
||||
[options]="dealerOptions"
|
||||
[style]="{'min-width': '200px'}"
|
||||
placeholder="Select Dealer"
|
||||
[loading]="dealerLoading"
|
||||
[filter]="true"
|
||||
filterBy="label"
|
||||
appendTo="body">
|
||||
</p-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="ui-g-12 ui-md-6 ui-lg-6 form-row">
|
||||
<p-checkbox id="billable" name="billable" formControlName="billable" label="Billable"
|
||||
binary="true"></p-checkbox>
|
||||
|
||||
@ -6,6 +6,7 @@ import { Customer, Partner } from '../models/customer.model';
|
||||
import * as customerActions from '../actions/customer.actions';
|
||||
import { UserService } from '@app/domain/services/user.service';
|
||||
import { PartnerService } from '@app/partners/services/partner.service';
|
||||
import { Dealer, DealerService } from '@app/tools/dealers/dealer.service';
|
||||
import { BaseComp } from '@app/shared/base/base.component';
|
||||
import { GC, RoleIds, globals, Labels } from '@app/shared/global';
|
||||
import { AGNavSubscription, Trial } from '@app/domain/models/subscription.model';
|
||||
@ -40,6 +41,10 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
partnerLoading = false;
|
||||
partnerError: string | null = null;
|
||||
|
||||
// Dealer Selection Properties
|
||||
dealerOptions: SelectItem[] = [];
|
||||
dealerLoading = false;
|
||||
|
||||
private _customer: Customer;
|
||||
get customer(): Customer { return this._customer; }
|
||||
set customer(customer: Customer) {
|
||||
@ -51,7 +56,8 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
premium: this.selectedItem.premium,
|
||||
billable: this.selectedItem.billable,
|
||||
trials: this.selectedItem.membership?.trials,
|
||||
partner: this.selectedItem.partner || null
|
||||
partner: this.selectedItem.partner || null,
|
||||
dealer: this.selectedItem.dealer || null
|
||||
});
|
||||
|
||||
// Set partner selection based on customer.partner field, or null if not set
|
||||
@ -67,6 +73,7 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly userSvc: UserService,
|
||||
private readonly partnerSvc: PartnerService,
|
||||
private readonly dealerSvc: DealerService,
|
||||
private readonly fb: FormBuilder
|
||||
) {
|
||||
super();
|
||||
@ -83,7 +90,9 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
billable: [],
|
||||
trials: [],
|
||||
// Partner form control
|
||||
partner: [null]
|
||||
partner: [null],
|
||||
// Dealer form control
|
||||
dealer: [null]
|
||||
});
|
||||
this.lang = this.authSvc.locale;
|
||||
|
||||
@ -106,6 +115,7 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
}
|
||||
// Load partners from service
|
||||
this.loadPartners();
|
||||
this.loadDealers();
|
||||
}
|
||||
});
|
||||
|
||||
@ -171,7 +181,8 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
custObj = Object.assign(this.selectedItem, this.form.value.profile, this.form.value.account,
|
||||
{ premium: this.form.value.premium || false },
|
||||
{ billable: this.form.value.billable || false },
|
||||
{ partner: this.form.value.partner || null });
|
||||
{ partner: this.form.value.partner || null },
|
||||
{ dealer: this.form.value.dealer?._id || this.form.value.dealer || null });
|
||||
|
||||
this.membership
|
||||
? custObj = Object.assign(custObj, { membership: updateTrialMembship(this.membership) })
|
||||
@ -209,6 +220,30 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
|
||||
return DateUtils.dateToTS(date);
|
||||
}
|
||||
|
||||
// Dealer Methods
|
||||
private loadDealers(): void {
|
||||
this.dealerLoading = true;
|
||||
this.dealerSvc.getAll().subscribe({
|
||||
next: (dealers: Dealer[]) => {
|
||||
this.dealerOptions = [
|
||||
{ label: 'None', value: null },
|
||||
...dealers
|
||||
.sort((a, b) => a.companyName.localeCompare(b.companyName))
|
||||
.map(d => ({
|
||||
label: d.country ? `${d.companyName} (${d.country})` : d.companyName,
|
||||
value: d
|
||||
}))
|
||||
];
|
||||
if (this.customer?.dealer) {
|
||||
const match = this.dealerOptions.find(o => o.value && o.value._id === (this.customer.dealer as any)?._id);
|
||||
if (match) { this.form.patchValue({ dealer: match.value }); }
|
||||
}
|
||||
this.dealerLoading = false;
|
||||
},
|
||||
error: () => { this.dealerLoading = false; }
|
||||
});
|
||||
}
|
||||
|
||||
// Partner Methods
|
||||
private loadPartners(): void {
|
||||
this.partnerLoading = true;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { RoleIds } from '@app/shared/global';
|
||||
import { createNewUser, User } from '@app/accounts/models/user.model';
|
||||
import { IMembership } from '@app/auth/models/user.model';
|
||||
import { Dealer } from '@app/tools/dealers/dealer.service';
|
||||
|
||||
export interface Customer extends User {
|
||||
contact?: string;
|
||||
@ -10,6 +11,7 @@ export interface Customer extends User {
|
||||
totalJobs?: number;
|
||||
membership: IMembership,
|
||||
partner?: Partner;
|
||||
dealer?: Dealer;
|
||||
selfSignup?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { from, Observable, of } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
import { AppConfigService } from './app-config.service';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
/** Shape of every entry stored in Cache Storage. */
|
||||
export interface BrowserCacheEntry<T> {
|
||||
@ -37,10 +38,14 @@ export class BrowserCacheService {
|
||||
private readonly supported = typeof caches !== 'undefined';
|
||||
private readonly fallbackMaxAgeMs = 60_000;
|
||||
|
||||
constructor(private readonly appConfig: AppConfigService) {}
|
||||
constructor(
|
||||
private readonly appConfig: AppConfigService,
|
||||
private readonly authSvc: AuthService
|
||||
) {}
|
||||
|
||||
private ttlStorageKey(cacheName: string): string {
|
||||
return `browser-cache-ttl:${cacheName}`;
|
||||
const userId = this.authSvc.user?._id || 'anonymous';
|
||||
return `browser-cache-ttl:${userId}:${cacheName}`;
|
||||
}
|
||||
|
||||
private get defaultMaxAgeMs(): number {
|
||||
|
||||
@ -53,13 +53,15 @@
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: #A5D6A7;
|
||||
color: #2E7D32;
|
||||
background: #4CAF50;
|
||||
border: 1px solid #2E7D32;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-revoked {
|
||||
background: #ffcdd2;
|
||||
color: #b71c1c;
|
||||
background: #f44336;
|
||||
border: 1px solid #d32f2f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
|
||||
@ -10,7 +10,7 @@ const routes: Routes = [
|
||||
path: '',
|
||||
component: ApiKeyManagerComponent,
|
||||
data: {
|
||||
roles: [RoleIds.ADMIN, RoleIds.APP, RoleIds.APP_ADM]
|
||||
roles: [RoleIds.ADMIN, RoleIds.APP]
|
||||
},
|
||||
canActivate: [AuthGuard]
|
||||
}
|
||||
|
||||
@ -224,10 +224,19 @@
|
||||
|
||||
<ng-template #partnerSection let-formGroup="formGroup">
|
||||
<div class="ui-g ui-g-12">
|
||||
<div class="ui-g-12 sub-heading" i18n="@@partners" style="font-weight: bold;">Partners</div>
|
||||
<div class="ui-g-12" i18n="@@enterPartner" style="padding-top: 0;">Select a partner if you are using AgMission with our partner's systems.</div>
|
||||
<div class="ui-g-4 ui-sm-6">
|
||||
<ng-container [ngTemplateOutlet]="dropdown" [ngTemplateOutletContext]="{options: partners, filter: false, formControlName: partner, formGroup: formGroup}"></ng-container>
|
||||
<div class="ui-g-6 ui-sm-12">
|
||||
<div class="ui-g-12 sub-heading" i18n="@@partners" style="font-weight: bold;">Partners</div>
|
||||
<div class="ui-g-12" i18n="@@enterPartner" style="padding-top: 0;">Select a partner if you are using AgMission with our partner's systems.</div>
|
||||
<div class="ui-g-8 ui-sm-12">
|
||||
<ng-container [ngTemplateOutlet]="dropdown" [ngTemplateOutletContext]="{options: partners, filter: false, formControlName: partner, formGroup: formGroup}"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-g-6 ui-sm-12">
|
||||
<div class="ui-g-12 sub-heading" i18n="@@dealers" style="font-weight: bold;">Dealer</div>
|
||||
<div class="ui-g-12" i18n="@@enterDealer" style="padding-top: 0;">Select the dealer who sold or supports your AG-NAV system.</div>
|
||||
<div class="ui-g-8 ui-sm-12">
|
||||
<ng-container [ngTemplateOutlet]="dropdown" [ngTemplateOutletContext]="{options: dealers, filter: true, formControlName: dealer, formGroup: formGroup}"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@ -15,6 +15,7 @@ import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
import { UniqueUserValidator } from '@app/shared/user-unique.directive';
|
||||
import { CommonService } from '@app/domain/services/common.service';
|
||||
import { PartnerService } from '@app/partners/services/partner.service';
|
||||
import { DealerService } from '@app/tools/dealers/dealer.service';
|
||||
import { of } from 'rxjs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { GAService } from '@app/shared/ga.service';
|
||||
@ -58,6 +59,7 @@ export class SignupFormComponent extends BaseComp implements OnInit, OnDestroy,
|
||||
readonly billing = 'billing';
|
||||
readonly password = 'password';
|
||||
readonly partner = 'partner';
|
||||
readonly dealer = 'dealer';
|
||||
|
||||
@ViewChild("captchaElem") captchaElem: ReCaptcha2Component;
|
||||
@ViewChild("captchaContainer", { static: false }) captchaContainer: ElementRef;
|
||||
@ -93,6 +95,7 @@ export class SignupFormComponent extends BaseComp implements OnInit, OnDestroy,
|
||||
{ label: $localize`:@@3rdParty:3rd Party`, value: '3rd_party' }
|
||||
];
|
||||
partners: SelectItem[] = [];
|
||||
dealers: SelectItem[] = [];
|
||||
filteredPlaces: BoundLocation[];
|
||||
error: { code?: string, message: string } | null = null;
|
||||
|
||||
@ -182,6 +185,7 @@ export class SignupFormComponent extends BaseComp implements OnInit, OnDestroy,
|
||||
private readonly uniqueUserValidator: UniqueUserValidator,
|
||||
private readonly commonSvc: CommonService,
|
||||
private readonly partnerSvc: PartnerService,
|
||||
private readonly dealerSvc: DealerService,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly gaService: GAService
|
||||
) {
|
||||
@ -224,6 +228,7 @@ export class SignupFormComponent extends BaseComp implements OnInit, OnDestroy,
|
||||
refSources: this.fb.array([])
|
||||
}),
|
||||
[this.partner]: [''],
|
||||
[this.dealer]: [''],
|
||||
recaptcha: ['', Validators.required],
|
||||
lang: [this.authSvc.locale, Validators.required],
|
||||
[this.password]: ['', [Validators.required, Validators.minLength(8)]],
|
||||
@ -258,6 +263,19 @@ export class SignupFormComponent extends BaseComp implements OnInit, OnDestroy,
|
||||
this.lang = this.authSvc.locale;
|
||||
this.setupFormSubscriptions();
|
||||
}),
|
||||
switchMap(() => this.dealerSvc.getAll()),
|
||||
tap((dealers: any[]) => {
|
||||
this.dealers = [
|
||||
{ label: $localize`:@@none:None`, value: '' },
|
||||
...dealers
|
||||
.sort((a, b) => a.companyName.localeCompare(b.companyName))
|
||||
.map(d => ({
|
||||
label: d.country ? `${d.companyName} (${d.country})` : d.companyName,
|
||||
value: d._id
|
||||
}))
|
||||
];
|
||||
this.signupForm.patchValue({ [this.dealer]: '' });
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error('Error during signup loading:', err);
|
||||
this.error = handleSignupErr({ error: err, opt: { tag: signupCode.signupLoadingError } });
|
||||
|
||||
43
Development/client/src/app/tools/dealers/dealer.service.ts
Normal file
43
Development/client/src/app/tools/dealers/dealer.service.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface Dealer {
|
||||
_id?: string;
|
||||
companyName: string;
|
||||
country: string;
|
||||
contactName?: string;
|
||||
address?: string;
|
||||
phone?: string;
|
||||
cell?: string;
|
||||
fax?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
isCertifiedRepair?: boolean;
|
||||
notes?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DealerService {
|
||||
private readonly base = '/dealers';
|
||||
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
getAll(): Observable<Dealer[]> {
|
||||
return this.http.get<Dealer[]>(this.base);
|
||||
}
|
||||
|
||||
create(dealer: Dealer): Observable<Dealer> {
|
||||
return this.http.post<Dealer>(this.base, dealer);
|
||||
}
|
||||
|
||||
update(id: string, dealer: Dealer): Observable<Dealer> {
|
||||
return this.http.put<Dealer>(`${this.base}/${id}`, dealer);
|
||||
}
|
||||
|
||||
delete(id: string): Observable<{ ok: boolean }> {
|
||||
return this.http.delete<{ ok: boolean }>(`${this.base}/${id}`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthGuard } from '../../domain/guards/auth.guard';
|
||||
import { SettingsGuard } from '../../domain/guards/settings-guard.service';
|
||||
import { RoleIds } from '../../shared/global';
|
||||
import { DealersComponent } from './dealers.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DealersComponent,
|
||||
data: { roles: [RoleIds.ADMIN] },
|
||||
canActivate: [AuthGuard, SettingsGuard]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
providers: [AuthGuard]
|
||||
})
|
||||
export class DealersRoutingModule { }
|
||||
@ -0,0 +1,12 @@
|
||||
.dealer-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
color: #555;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0.625rem;
|
||||
}
|
||||
|
||||
.dealer-required {
|
||||
color: #f44336;
|
||||
}
|
||||
176
Development/client/src/app/tools/dealers/dealers.component.html
Normal file
176
Development/client/src/app/tools/dealers/dealers.component.html
Normal file
@ -0,0 +1,176 @@
|
||||
<div class="ui-g">
|
||||
<div class="ui-g-12">
|
||||
<div class="card card-w-title">
|
||||
<p-table #dt [value]="dealers" [columns]="cols" selectionMode="single"
|
||||
[(selection)]="selectedDealer" dataKey="_id"
|
||||
[paginator]="true" [rows]="15" [rowsPerPageOptions]="[15, 30, 50]"
|
||||
[alwaysShowPaginator]="true" [responsive]="true"
|
||||
[loading]="loading" stateStorage="session" stateKey="dealers-tbl">
|
||||
|
||||
<ng-template pTemplate="caption">
|
||||
<span class="table-caption-1" style="display:block; text-align:center;">Dealers</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header" let-columns>
|
||||
<tr>
|
||||
<th *ngFor="let col of columns" [pSortableColumn]="col.field" [style.width]="col.width">
|
||||
{{ col.header }}
|
||||
<p-sortIcon [field]="col.field"></p-sortIcon>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th *ngFor="let col of columns" class="ui-fluid">
|
||||
<ng-container *ngIf="col.filtered">
|
||||
<div class="input-with-icon" *ngIf="col.filterType === 'text'">
|
||||
<i class="ui-icon-search"></i>
|
||||
<input pInputText type="text"
|
||||
(input)="dt.filter($event.target.value, col.field, 'contains')"
|
||||
[value]="dt.filters[col.field]?.value || ''">
|
||||
</div>
|
||||
<p-dropdown *ngIf="col.filterType === 'dropdown'"
|
||||
[options]="col.field === 'country' ? countryOptions : certifiedOptions"
|
||||
(onChange)="dt.filter($event.value, col.field, 'equals')"
|
||||
placeholder="All"
|
||||
[style]="{'width':'100%'}"
|
||||
appendTo="body">
|
||||
</p-dropdown>
|
||||
</ng-container>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-row>
|
||||
<tr [pSelectableRow]="row">
|
||||
<td>
|
||||
<span class="ui-column-title">Company</span>
|
||||
{{ row.companyName }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="ui-column-title">Country</span>
|
||||
{{ row.country }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="ui-column-title">Contact</span>
|
||||
{{ row.contactName }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="ui-column-title">Phone</span>
|
||||
{{ row.phone }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="ui-column-title">Email</span>
|
||||
<a *ngIf="row.email" [href]="'mailto:' + row.email" style="color:inherit;">{{ row.email }}</a>
|
||||
</td>
|
||||
<td class="table-col-center">
|
||||
<span class="ui-column-title">Certified</span>
|
||||
<p-checkbox [ngModel]="row.isCertifiedRepair" [binary]="true" [disabled]="true"></p-checkbox>
|
||||
</td>
|
||||
<td>
|
||||
<span class="ui-column-title">Website</span>
|
||||
<a *ngIf="row.website" [href]="row.website" target="_blank" rel="noopener noreferrer">
|
||||
<i class="material-icons" style="font-size:1rem; vertical-align:middle;">open_in_new</i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center; color:#757575; padding:1.5rem;">
|
||||
<i class="material-icons" style="vertical-align:middle; margin-right:0.25rem;">store</i>
|
||||
No dealers found.
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="paginatorleft" let-state>
|
||||
{{ state.totalRecords | i18nPlural: totalItems }}
|
||||
</ng-template>
|
||||
|
||||
</p-table>
|
||||
|
||||
<div class="ui-widget-header ui-helper-clearfix toolbar">
|
||||
<button pButton type="button" icon="ui-icon-plus" label="New" (click)="openNew()"></button>
|
||||
<button pButton type="button" icon="ui-icon-edit" label="Edit"
|
||||
[disabled]="!selectedDealer" (click)="openEdit()"></button>
|
||||
<button pButton type="button" icon="ui-icon-delete" label="Delete"
|
||||
[disabled]="!selectedDealer" (click)="deleteDealer()"></button>
|
||||
<button pButton type="button" icon="ui-icon-refresh" label="Refresh"
|
||||
class="blue-btn" (click)="load()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create / Edit Dialog -->
|
||||
<p-dialog
|
||||
[header]="isEditing ? 'Edit Dealer' : 'New Dealer'"
|
||||
[(visible)]="showDialog"
|
||||
[modal]="true"
|
||||
[style]="{width:'560px'}"
|
||||
[closable]="true"
|
||||
[draggable]="false">
|
||||
|
||||
<div class="ui-g ui-g-fluid">
|
||||
|
||||
<div class="ui-g-12 ui-md-8">
|
||||
<label class="dealer-label">Company Name <span class="dealer-required">*</span></label>
|
||||
<input pInputText type="text" [(ngModel)]="form.companyName" style="width:100%;" placeholder="e.g. Aerotec">
|
||||
</div>
|
||||
<div class="ui-g-12 ui-md-4">
|
||||
<label class="dealer-label">Country <span class="dealer-required">*</span></label>
|
||||
<input pInputText type="text" [(ngModel)]="form.country" style="width:100%;" placeholder="e.g. Argentina">
|
||||
</div>
|
||||
|
||||
<div class="ui-g-12 ui-md-6">
|
||||
<label class="dealer-label">Contact Name</label>
|
||||
<input pInputText type="text" [(ngModel)]="form.contactName" style="width:100%;" placeholder="Full name">
|
||||
</div>
|
||||
<div class="ui-g-12 ui-md-6">
|
||||
<label class="dealer-label">Email</label>
|
||||
<input pInputText type="email" [(ngModel)]="form.email" style="width:100%;" placeholder="contact@example.com">
|
||||
</div>
|
||||
|
||||
<div class="ui-g-12">
|
||||
<label class="dealer-label">Address</label>
|
||||
<input pInputText type="text" [(ngModel)]="form.address" style="width:100%;" placeholder="Street, city, postal code">
|
||||
</div>
|
||||
|
||||
<div class="ui-g-12 ui-md-4">
|
||||
<label class="dealer-label">Phone</label>
|
||||
<input pInputText type="text" [(ngModel)]="form.phone" style="width:100%;" placeholder="+1 555 000 0000">
|
||||
</div>
|
||||
<div class="ui-g-12 ui-md-4">
|
||||
<label class="dealer-label">Cell</label>
|
||||
<input pInputText type="text" [(ngModel)]="form.cell" style="width:100%;">
|
||||
</div>
|
||||
<div class="ui-g-12 ui-md-4">
|
||||
<label class="dealer-label">Fax</label>
|
||||
<input pInputText type="text" [(ngModel)]="form.fax" style="width:100%;">
|
||||
</div>
|
||||
|
||||
<div class="ui-g-12 ui-md-9">
|
||||
<label class="dealer-label">Website</label>
|
||||
<input pInputText type="url" [(ngModel)]="form.website" style="width:100%;" placeholder="https://...">
|
||||
</div>
|
||||
<div class="ui-g-12 ui-md-3" style="display:flex; align-items:flex-end; padding-bottom:0.25rem;">
|
||||
<p-checkbox [(ngModel)]="form.isCertifiedRepair" [binary]="true"
|
||||
label="Certified Repair"></p-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="ui-g-12">
|
||||
<label class="dealer-label">Notes</label>
|
||||
<textarea pInputTextarea [(ngModel)]="form.notes" style="width:100%;" rows="2"
|
||||
placeholder="Additional information…"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p-footer>
|
||||
<button pButton type="button" icon="ui-icon-close" label="Cancel"
|
||||
(click)="showDialog = false" [disabled]="saving"></button>
|
||||
<button pButton type="button" icon="ui-icon-save" label="Save" class="green-btn"
|
||||
[disabled]="!form.companyName?.trim() || !form.country?.trim() || saving"
|
||||
(click)="saveDealer()"></button>
|
||||
</p-footer>
|
||||
</p-dialog>
|
||||
131
Development/client/src/app/tools/dealers/dealers.component.ts
Normal file
131
Development/client/src/app/tools/dealers/dealers.component.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BaseComp } from '@app/shared/base/base.component';
|
||||
import { Dealer, DealerService } from './dealer.service';
|
||||
|
||||
@Component({
|
||||
selector: 'agm-dealers',
|
||||
templateUrl: './dealers.component.html',
|
||||
styleUrls: ['./dealers.component.css']
|
||||
})
|
||||
export class DealersComponent extends BaseComp implements OnInit {
|
||||
|
||||
dealers: Dealer[] = [];
|
||||
selectedDealer: Dealer | null = null;
|
||||
loading = false;
|
||||
|
||||
showDialog = false;
|
||||
isEditing = false;
|
||||
saving = false;
|
||||
|
||||
form: Dealer = this.emptyForm();
|
||||
|
||||
cols = [
|
||||
{ field: 'companyName', header: 'Company', width: '22%', filtered: true, filterType: 'text' },
|
||||
{ field: 'country', header: 'Country', width: '14%', filtered: true, filterType: 'dropdown' },
|
||||
{ field: 'contactName', header: 'Contact', width: '16%', filtered: true, filterType: 'text' },
|
||||
{ field: 'phone', header: 'Phone', width: '13%', filtered: true, filterType: 'text' },
|
||||
{ field: 'email', header: 'Email', width: '16%', filtered: true, filterType: 'text' },
|
||||
{ field: 'isCertifiedRepair', header: 'Certified', width: '7%', filtered: true, filterType: 'dropdown' },
|
||||
{ field: 'website', header: 'Website', width: '12%', filtered: false },
|
||||
];
|
||||
|
||||
countryOptions: { label: string; value: string | null }[] = [];
|
||||
|
||||
certifiedOptions = [
|
||||
{ label: 'All', value: null },
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
totalItems = { '=0': 'No dealers', '=1': '1 dealer', 'other': '# dealers' };
|
||||
|
||||
constructor(private readonly dealerSvc: DealerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.dealerSvc.getAll().subscribe({
|
||||
next: (data) => {
|
||||
this.dealers = data;
|
||||
this.countryOptions = [
|
||||
{ label: 'All', value: null },
|
||||
...Array.from(new Set(data.map(d => d.country))).sort()
|
||||
.map(c => ({ label: c, value: c }))
|
||||
];
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.msgSvc.addFailedMsg('Failed to load dealers: ' + (err?.error?.error?.message || err.message));
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openNew(): void {
|
||||
this.form = this.emptyForm();
|
||||
this.isEditing = false;
|
||||
this.showDialog = true;
|
||||
}
|
||||
|
||||
openEdit(): void {
|
||||
if (!this.selectedDealer) { return; }
|
||||
this.form = { ...this.selectedDealer };
|
||||
this.isEditing = true;
|
||||
this.showDialog = true;
|
||||
}
|
||||
|
||||
saveDealer(): void {
|
||||
if (!this.form.companyName?.trim() || !this.form.country?.trim()) { return; }
|
||||
this.saving = true;
|
||||
|
||||
const action$ = this.isEditing
|
||||
? this.dealerSvc.update(this.form._id, this.form)
|
||||
: this.dealerSvc.create(this.form);
|
||||
|
||||
action$.subscribe({
|
||||
next: () => {
|
||||
this.msgSvc.addSuccessMsg(this.isEditing ? 'Dealer updated.' : 'Dealer created.');
|
||||
this.showDialog = false;
|
||||
this.selectedDealer = null;
|
||||
this.saving = false;
|
||||
this.load();
|
||||
},
|
||||
error: (err) => {
|
||||
this.msgSvc.addFailedMsg('Save failed: ' + (err?.error?.error?.message || err.message));
|
||||
this.saving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDealer(): void {
|
||||
if (!this.selectedDealer) { return; }
|
||||
this.confirmSvc.confirm({
|
||||
message: `Delete dealer "${this.selectedDealer.companyName}"?`,
|
||||
header: 'Confirm Delete',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
accept: () => {
|
||||
this.dealerSvc.delete(this.selectedDealer._id).subscribe({
|
||||
next: () => {
|
||||
this.msgSvc.addSuccessMsg('Dealer deleted.');
|
||||
this.selectedDealer = null;
|
||||
this.load();
|
||||
},
|
||||
error: (err) => this.msgSvc.addFailedMsg('Delete failed: ' + (err?.error?.error?.message || err.message))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private emptyForm(): Dealer {
|
||||
return {
|
||||
companyName: '', country: '', contactName: '', address: '',
|
||||
phone: '', cell: '', fax: '', email: '', website: '',
|
||||
isCertifiedRepair: false, notes: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
23
Development/client/src/app/tools/dealers/dealers.module.ts
Normal file
23
Development/client/src/app/tools/dealers/dealers.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { DialogModule } from 'primeng-lts/dialog';
|
||||
import { ConfirmDialogModule } from 'primeng-lts/confirmdialog';
|
||||
import { TableModule } from 'primeng-lts/table';
|
||||
import { CheckboxModule } from 'primeng-lts/checkbox';
|
||||
|
||||
import { AppSharedModule } from '../../shared/app-shared.module';
|
||||
import { DealersRoutingModule } from './dealers-routing.module';
|
||||
import { DealersComponent } from './dealers.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AppSharedModule,
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
TableModule,
|
||||
CheckboxModule,
|
||||
DealersRoutingModule
|
||||
],
|
||||
declarations: [DealersComponent]
|
||||
})
|
||||
export class DealersModule { }
|
||||
@ -112,7 +112,8 @@ async function getCustomer_get(req, res) {
|
||||
const view = req.query.view;
|
||||
let query = Customer.findOne({ _id: ObjectId(cId) }, null, { lean: true })
|
||||
.populate({ path: 'Country', select: 'code name -_id' })
|
||||
.populate({ path: 'partner', select: 'name description' });
|
||||
.populate({ path: 'partner', select: 'name description' })
|
||||
.populate({ path: 'dealer', select: 'companyName country contactName phone email' });
|
||||
|
||||
if (view !== 'edit') {
|
||||
query = query.select('-password');
|
||||
|
||||
46
Development/server/controllers/dealer.js
Normal file
46
Development/server/controllers/dealer.js
Normal file
@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const Dealer = require('../model/dealer'),
|
||||
{ AppParamError } = require('../helpers/app_error'),
|
||||
assert = require('assert');
|
||||
|
||||
async function getDealers_get(req, res) {
|
||||
const dealers = await Dealer.find().sort({ country: 1, companyName: 1 }).lean();
|
||||
res.json(dealers);
|
||||
}
|
||||
|
||||
async function createDealer_post(req, res) {
|
||||
const body = req.body;
|
||||
assert(body && body.companyName && body.country, AppParamError.create());
|
||||
|
||||
delete body._id;
|
||||
const dealer = new Dealer(body);
|
||||
const saved = await dealer.save();
|
||||
res.json(saved);
|
||||
}
|
||||
|
||||
async function updateDealer_put(req, res) {
|
||||
const { id } = req.params;
|
||||
const body = req.body;
|
||||
assert(id, AppParamError.create());
|
||||
|
||||
const updated = await Dealer.findByIdAndUpdate(id, body, { new: true, runValidators: true });
|
||||
if (!updated) AppParamError.throw();
|
||||
res.json(updated);
|
||||
}
|
||||
|
||||
async function deleteDealer_delete(req, res) {
|
||||
const { id } = req.params;
|
||||
assert(id, AppParamError.create());
|
||||
|
||||
const deleted = await Dealer.findByIdAndDelete(id);
|
||||
if (!deleted) AppParamError.throw();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDealers_get,
|
||||
createDealer_post,
|
||||
updateDealer_put,
|
||||
deleteDealer_delete
|
||||
};
|
||||
@ -487,6 +487,9 @@ async function signup_post(req, res) {
|
||||
if (typeof input.partner === 'string' && input.partner?.trim().length !== 0) {
|
||||
customerData.partner = input.partner.trim();
|
||||
}
|
||||
if (typeof input.dealer === 'string' && input.dealer?.trim().length !== 0) {
|
||||
customerData.dealer = input.dealer.trim();
|
||||
}
|
||||
const newCustomer = new CustomerModel(customerData);
|
||||
|
||||
const emailData = {
|
||||
|
||||
@ -27,6 +27,7 @@ function isSecuredRoute(routePath, method) {
|
||||
{ path: '/resetPassword', method: 'ALL' },
|
||||
{ path: '/signup', method: 'ALL' },
|
||||
{ path: '/api/partners', method: 'GET', exact: true }, // Allow unauthenticated GET /api/partners only (not subroutes)
|
||||
{ path: '/api/dealers', method: 'GET', exact: true }, // Allow unauthenticated GET /api/dealers only (used on signup page)
|
||||
{ path: '/exists', method: 'POST' },
|
||||
{ path: '/countries', method: 'GET' },
|
||||
{ path: '/testAuth', method: 'ALL' },
|
||||
|
||||
32
Development/server/model/dealer.js
Normal file
32
Development/server/model/dealer.js
Normal file
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const mongoose = require('mongoose'),
|
||||
Schema = mongoose.Schema;
|
||||
|
||||
const dealerSchema = new Schema({
|
||||
companyName: { type: String, required: true, trim: true },
|
||||
country: { type: String, required: true, trim: true },
|
||||
contactName: { type: String, trim: true, default: '' },
|
||||
address: { type: String, trim: true, default: '' },
|
||||
phone: { type: String, trim: true, default: '' },
|
||||
cell: { type: String, trim: true, default: '' },
|
||||
fax: { type: String, trim: true, default: '' },
|
||||
email: { type: String, trim: true, lowercase: true, default: '' },
|
||||
website: { type: String, trim: true, default: '' },
|
||||
isCertifiedRepair: { type: Boolean, default: false },
|
||||
notes: { type: String, trim: true, default: '' },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now }
|
||||
}, { strictQuery: false });
|
||||
|
||||
dealerSchema.pre('save', function (next) {
|
||||
this.updatedAt = new Date();
|
||||
next();
|
||||
});
|
||||
|
||||
dealerSchema.pre('findOneAndUpdate', function (next) {
|
||||
this.set({ updatedAt: new Date() });
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Dealer', dealerSchema);
|
||||
@ -46,6 +46,9 @@ const schema = new Schema({
|
||||
// Reference to the Partner collection (optional) - used for customers with partner integrations
|
||||
partner: { type: Schema.Types.ObjectId, ref: 'User', required: false },
|
||||
|
||||
// Reference to the Dealer collection (optional) - the dealer that sold/supports this customer
|
||||
dealer: { type: Schema.Types.ObjectId, ref: 'Dealer', required: false },
|
||||
|
||||
loggedInAt: { type: Date },
|
||||
|
||||
markedDelete: { type: Boolean, default: false },
|
||||
|
||||
14
Development/server/routes/dealer.js
Normal file
14
Development/server/routes/dealer.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function (app) {
|
||||
const router = require('express').Router(),
|
||||
{ authAllowAdmin } = require('../middlewares/validate'),
|
||||
dealerCtl = require('../controllers/dealer');
|
||||
|
||||
router.get('/', dealerCtl.getDealers_get);
|
||||
router.post('/', authAllowAdmin(), dealerCtl.createDealer_post);
|
||||
router.put('/:id', authAllowAdmin(), dealerCtl.updateDealer_put);
|
||||
router.delete('/:id', authAllowAdmin(), dealerCtl.deleteDealer_delete);
|
||||
|
||||
app.use('/api/dealers', router);
|
||||
};
|
||||
@ -29,6 +29,7 @@ module.exports = function (app) {
|
||||
require('./costing_items')(app);
|
||||
require('./log_payment')(app);
|
||||
require('./partner')(app);
|
||||
require('./dealer')(app);
|
||||
require('./health')(app);
|
||||
// Data Export public API (X-API-Key auth) and key management (JWT auth)
|
||||
require('./api_pub')(app);
|
||||
|
||||
@ -53,6 +53,7 @@ module.exports = function (app) {
|
||||
taxId: Joi.string().allow('').optional(),
|
||||
lang: Joi.string().default(DEFAULT_LANG).optional(),
|
||||
partner: Joi.objectId().allow('').allow(null).optional(),
|
||||
dealer: Joi.objectId().allow('').allow(null).optional(),
|
||||
emailToken: Joi.string().optional(),
|
||||
token: Joi.string().optional(),
|
||||
})
|
||||
|
||||
303
Development/server/scripts/seedDealers.js
Normal file
303
Development/server/scripts/seedDealers.js
Normal file
@ -0,0 +1,303 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dealer Seed Script
|
||||
*
|
||||
* Inserts the initial AG-NAV world-wide dealer network into the database.
|
||||
* Safe to re-run: upserts on (companyName + country) so no duplicates are created.
|
||||
*
|
||||
* Usage (from repo root):
|
||||
* set -a && source server/environment.env && set +a && DEBUG=agm:seed-dealers node server/scripts/seedDealers.js
|
||||
*
|
||||
* Dry-run (no writes):
|
||||
* set -a && source server/environment.env && set +a && DEBUG=agm:seed-dealers node server/scripts/seedDealers.js --dry-run
|
||||
*/
|
||||
|
||||
const debug = require('debug')('agm:seed-dealers');
|
||||
const { DBConnection } = require('../helpers/db/connect.js');
|
||||
const Dealer = require('../model/dealer.js');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const isDryRun = args.includes('--dry-run');
|
||||
|
||||
if (isDryRun) {
|
||||
debug('Running in DRY-RUN mode — no changes will be made');
|
||||
}
|
||||
|
||||
const DEALERS = [
|
||||
{
|
||||
companyName: 'Aerotec',
|
||||
country: 'Argentina',
|
||||
contactName: 'Diego M. Cardama Mendoza',
|
||||
address: 'Aerodromo Mario Cardama (5577) Comandante Torres 100 – Rivadavia Buenos Aires; Aerodromo Aeroclub Lujan – Beschtedt S/N Hangar 1',
|
||||
phone: '+54 (263) 444 3212',
|
||||
cell: '+54 (9) 261 569 2744',
|
||||
email: 'diego@aerotec-argentina.com.ar',
|
||||
website: 'http://aerotec.com.ar/',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'APAC Heli Solutions',
|
||||
country: 'Australia',
|
||||
contactName: 'Scott Simpson',
|
||||
address: 'Kurrajong NSW 2758, Australia',
|
||||
phone: '+61 (0) 418 484 515',
|
||||
email: 'ssimpson@apachelisolutions.com',
|
||||
website: 'https://www.apachelisolutions.com/',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Trabajo Aereo Agricola T.A.A',
|
||||
country: 'Bolivia',
|
||||
contactName: 'Verly Valdez Ruiz',
|
||||
address: 'Lagunillas # 301 esq. Villamontes, Braniff Santa Cruz, Bolivia',
|
||||
phone: '591 3 352 6578',
|
||||
cell: '591 716 48864',
|
||||
email: 'verlyvaldez@hotmail.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'DGPS & CIA',
|
||||
country: 'Brazil',
|
||||
contactName: 'Miguel Paim',
|
||||
address: 'Rua dos Hangares No. 453 Bairro: Parque Industrial – Aeroporto, Primavera do Leste – MT, CEP: 78850-000',
|
||||
phone: '+55 (66) 3497-3400',
|
||||
cell: '+55 (66) 9986-1198',
|
||||
email: 'miguelpaim@dgpsecia.com.br',
|
||||
website: 'http://www.dgpsecia.com.br',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
{
|
||||
companyName: 'Dinnarc Tecnologia Agricola',
|
||||
country: 'Brazil',
|
||||
contactName: 'Augusto e Ramon',
|
||||
address: 'Avenida Adolino Bedin Nº 875, CEP: 78.894-132 Jardim das Américas, Sorriso, MT, Brazil',
|
||||
phone: '+55 (66) 99981-8300',
|
||||
cell: '+55 (66) 99200-7447',
|
||||
email: 'contato@dinnarc.com.br',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'ABA Manutencao de Aeronaves',
|
||||
country: 'Brazil',
|
||||
contactName: 'Ruddiger Alves Da Silva',
|
||||
address: 'Rua da Prainha, 3320 Cond. Sitio de Voo ABA Lotes 05 e 06, Barreirinhas, Barreiras',
|
||||
phone: '+55 (66) 99987-0727',
|
||||
email: 'ruddigger@abamanutencao.com.br',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Galindo e Galindo Comercio e Servicos Eletronicos Ltda',
|
||||
country: 'Brazil',
|
||||
contactName: 'Francisco Galindo',
|
||||
address: 'Rua D, 20 - Balneario Recreativa de Campo, CEP: 14.073-808, Ribeirao Preto - SP',
|
||||
phone: '+55 (16) 3629-3317',
|
||||
cell: '+55 (16) 99137-1517',
|
||||
email: 'fgalindo@galindodgps.com.br',
|
||||
website: 'http://www.galindodgps.com.br',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Aeroglobo Aeronaves',
|
||||
country: 'Brazil',
|
||||
address: 'Rua José Dal Farra, 654, Jardim Dona Carolina, Botucatu – São Paulo, 18.602.020',
|
||||
phone: '+55 14 3814-3450',
|
||||
email: 'contato@aeroglobo.com.br',
|
||||
website: 'https://www.aeroglobo.com.br',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Provincial Airways',
|
||||
country: 'Canada',
|
||||
contactName: 'James',
|
||||
address: 'Box 2170, Hwy 301N, Moose Jaw, SK, S6H 7T2',
|
||||
phone: '306 692 7335',
|
||||
fax: '306 693 5288',
|
||||
cell: '306 693 0877',
|
||||
email: 'james@provincialairways.net',
|
||||
website: 'http://www.provincialairways.net',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Carlos Ilabaca Vacarezza',
|
||||
country: 'Chile',
|
||||
contactName: 'Carlos Ilabaca Vacarezza',
|
||||
address: 'La Serena, Chile',
|
||||
phone: '56 097 849 0244',
|
||||
email: 'cpilabaca@gmail.com',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
{
|
||||
companyName: 'Jarly Camacho Torres',
|
||||
country: 'Colombia',
|
||||
contactName: 'Jarly Camacho Torres',
|
||||
address: 'Calle 6, casa # 12-52 Barrio Pescadito, Santa Marta, Colombia',
|
||||
phone: '(57) 431-6187',
|
||||
cell: '(57) 321-525-6367',
|
||||
fax: '(57) 317-525-4385',
|
||||
email: 'jcamachot87@yahoo.es',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
{
|
||||
companyName: 'Mario Berrones Corp',
|
||||
country: 'Ecuador',
|
||||
contactName: 'Mario Berrones',
|
||||
address: 'Eloy Alfaro 126 y Tarqui, Canton Yaguachi, Guayas, Ecuador',
|
||||
phone: '011-593-93910438',
|
||||
email: 'mabescorp@hotmail.com',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
{
|
||||
companyName: 'Aero Agricola Paraguaya SA',
|
||||
country: 'Paraguay',
|
||||
address: 'Ruta 1 KM 286, Aeropuerto Aero Agricola Paraguaya, General Delgado, Itapua 6860, Paraguay',
|
||||
phone: '+595 985-220286',
|
||||
email: 'administracion@aeroagricolaparaguaya.com.py',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Davao Aerowurkz Corporation',
|
||||
country: 'Philippines',
|
||||
address: 'BTC Hangar, Old Airport, Sasa, Davao City, 8000, Philippines',
|
||||
phone: '+63 (082) 234-8843',
|
||||
email: 'aerowurks_aviation@yahoo.com.ph',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Business Development Services',
|
||||
country: 'Poland',
|
||||
contactName: 'Lukasz Kempys',
|
||||
address: 'UL. USTRONIE 31, NOWY TARG, 34-400, Poland',
|
||||
phone: '+48-694-473-616',
|
||||
email: 'lukaszkempys@gmail.com',
|
||||
website: 'http://www.SkyFun.pl',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
{
|
||||
companyName: 'Steve Viviers Aviation',
|
||||
country: 'South Africa',
|
||||
contactName: 'Steve Viviers',
|
||||
address: 'P.O. Box 1952, Kroonstad, 9500, South Africa',
|
||||
phone: '+27-836378504',
|
||||
fax: '+27-56-212-3436',
|
||||
cell: '+27-82-800-1508',
|
||||
email: 'viviersaviation@act.co.za',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
{
|
||||
companyName: 'Lane Aviation',
|
||||
country: 'United States',
|
||||
contactName: 'Dona Jorden',
|
||||
address: '3205 FM 2218 Rd, Rosenberg, TX 77471, USA',
|
||||
phone: '(281) 342-5451',
|
||||
fax: '(281) 232-5401',
|
||||
email: 'dona@laneav.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Frost Flying Inc.',
|
||||
country: 'United States',
|
||||
contactName: 'Garret Frost',
|
||||
address: '3393 Hwy. 121 West, Marianna, AR 72360, USA',
|
||||
phone: '(870) 295-6218',
|
||||
fax: '(870) 295-6237',
|
||||
email: 'frostparts@hotmail.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Crosslands International, LLC',
|
||||
country: 'United States',
|
||||
contactName: 'John M. Mishler',
|
||||
address: '17921 S US Hwy 377, Cresson, TX 76035, USA',
|
||||
phone: '(817) 478-9933',
|
||||
email: 'john@crosslandsinternational.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Summit Helicopters, Inc.',
|
||||
country: 'United States',
|
||||
contactName: 'Jeff Partain',
|
||||
address: 'Box 909, 525 McCelland Street, Salem, VA 24153, USA',
|
||||
phone: '(540) 375-8909',
|
||||
email: 'jeff.partain@summithelicopters.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Thomas Helicopters',
|
||||
country: 'United States',
|
||||
contactName: 'Rod Thomas',
|
||||
address: '1553 South 1800 East, Gooding, ID 83330, USA',
|
||||
phone: '(208) 934-8298',
|
||||
fax: '(208) 934-5934',
|
||||
email: 'rodheli@aol.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Collective Aviation',
|
||||
country: 'United States',
|
||||
contactName: 'Kristopher Petter',
|
||||
address: '2874 Henry Wallace Rd., Orient, Iowa 50858, USA',
|
||||
phone: '(206) 484-8749',
|
||||
email: 'kris@collectiveaviationservices.com',
|
||||
isCertifiedRepair: false,
|
||||
},
|
||||
{
|
||||
companyName: 'Servicio Aeroagricola De Flores S.R.L.',
|
||||
country: 'Uruguay',
|
||||
contactName: 'Julio Placeres / Juan Perez',
|
||||
address: 'Manuel Irazabal 530, Trinidad, Flores, Uruguay',
|
||||
phone: '+598 4364-4686',
|
||||
fax: '+598 4364-4686',
|
||||
email: 'juliopla@adinet.com.uy',
|
||||
isCertifiedRepair: true,
|
||||
},
|
||||
];
|
||||
|
||||
async function seedDealers() {
|
||||
debug(`Seeding ${DEALERS.length} dealers (dry-run: ${isDryRun})`);
|
||||
|
||||
let inserted = 0, skipped = 0;
|
||||
|
||||
for (const dealer of DEALERS) {
|
||||
const filter = { companyName: dealer.companyName, country: dealer.country };
|
||||
|
||||
if (isDryRun) {
|
||||
const existing = await Dealer.findOne(filter).lean();
|
||||
if (existing) {
|
||||
debug(`[DRY-RUN] Would skip — ${dealer.companyName} (${dealer.country}) already exists`);
|
||||
skipped++;
|
||||
} else {
|
||||
debug(`[DRY-RUN] Would insert — ${dealer.companyName} (${dealer.country})`);
|
||||
inserted++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await Dealer.findOne(filter).lean();
|
||||
if (existing) {
|
||||
debug(`[skipped] ${dealer.companyName} (${dealer.country})`);
|
||||
skipped++;
|
||||
} else {
|
||||
await Dealer.create(dealer);
|
||||
debug(`[inserted] ${dealer.companyName} (${dealer.country})`);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Done. Inserted: ${inserted}, Skipped: ${skipped}`);
|
||||
}
|
||||
|
||||
const workerDB = new DBConnection('Dealer Seed Script');
|
||||
|
||||
workerDB.initialize({
|
||||
setupExitHandlers: false,
|
||||
onReady: async () => {
|
||||
try {
|
||||
await seedDealers();
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
debug('Seed failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user