Compare commits

..

No commits in common. "feature/advanced-reports" and "master" have entirely different histories.

1681 changed files with 7856 additions and 121676 deletions

View File

@ -12,7 +12,7 @@ on:
push:
branches:
- master
jobs:
push-to-svn:
runs-on: self-hosted
@ -44,7 +44,7 @@ jobs:
--no-auth-cache \
--non-interactive \
--trust-server-cert \
"${{ secrets.SVN_REPO_URL }}/branches/advanced-reports" svn-branch
"${{ secrets.SVN_REPO_URL }}/branches/data-export-api-copy" svn-branch
- name: Sync files to SVN working copy
run: |
@ -74,4 +74,4 @@ jobs:
--no-auth-cache \
--non-interactive \
--trust-server-cert \
-m "Gitea CI sync from commit ${{ github.sha }} [ci skip]"
-m "Gitea CI sync from commit ${{ github.sha }} [ci skip]"

6
Development/client/.env Normal file
View File

@ -0,0 +1,6 @@
CURRENT_FILE_PATH=src/locale/messages.xlf
TRANSLATED_FILE_PATH_ES=src/locale/messages.es.xlf
TRANSLATED_FILE_PATH_PT=src/locale/messages.pt.xlf
GOOGLE_LOCATION=global
GOOGLE_PROJECT_ID=predictive-fx-392018
GOOGLE_APPLICATION_CREDENTIALS=google-cloud.json

View File

@ -52,21 +52,6 @@
"glob": "**/*",
"input": "node_modules/leaflet/dist/images",
"output": "/assets/images"
},
{
"glob": "CHANGELOG.md",
"input": "docs",
"output": "/assets/docs"
},
{
"glob": "**/*",
"input": "docs/releases",
"output": "/assets/docs/releases"
},
{
"glob": "**/*",
"input": "../server/public/images",
"output": "/images"
}
],
"styles": [

View File

@ -4,19 +4,12 @@
"license": "COMMERCIAL",
"angular-cli": {},
"scripts": {
"generate-release-manifest": "node scripts/generate-release-manifest.js",
"ng": "ng",
"prestart-cert": "npm run generate-release-manifest",
"start-cert": "ng serve --ssl true --sslKey ~/ssl/server.key --sslCert ~/ssl/server.crt --proxy-config proxy.config.json --host 0.0.0.0 --disableHostCheck",
"prestart": "npm run generate-release-manifest",
"start": "CHOKIDAR_USEPOLLING=true ng serve --ssl true --proxy-config proxy.config.json --host 0.0.0.0 --disableHostCheck",
"prestart-es": "npm run generate-release-manifest",
"start-es": "ng serve --ssl true --proxy-config proxy.config.json --host 0.0.0.0 --disableHostCheck --configuration=es",
"prestart-pt": "npm run generate-release-manifest",
"start-pt": "ng serve --ssl true --proxy-config proxy.config.json --host 0.0.0.0 --disableHostCheck --configuration=pt",
"prebuild": "npm run generate-release-manifest",
"build": "ng build",
"prebuild-prep": "npm run generate-release-manifest",
"build-prep": "ng build --aot --localize=false",
"test": "ng test",
"lint": "ng lint",
@ -29,9 +22,7 @@
"sync-i18n": "npm run build-prep && npm run i18n-extract && npm run i18n-merge",
"sync-i18n-w": "npm run build-prep && npm run i18n-extract-w && npm run i18n-merge-w",
"pre-translate": "npx translation start && npm run sync-i18n && npx translation translate && npx translation cleanup",
"prebuild-prod": "npm run generate-release-manifest",
"build-prod": "ng build --prod --localize && cp -R dist/en/* dist/ && rm -R dist/en",
"prebuild-prod-window": "npm run generate-release-manifest",
"build-prod-window": "ng build --prod --localize && xcopy /E /Y dist\\en\\* dist\\ && rmdir /S /Q dist\\en"
},
"private": true,
@ -67,13 +58,8 @@
"geodesy": "^1.1.3",
"intl": "^1.2.5",
"leaflet": "^1.9.4",
"leaflet-river": "^1.0.1",
"marked": "^1.2.9",
"mermaid": "^8.14.0",
"ngrx-store-localstorage": "^9.0.0",
"ngx-captcha": "^8.0.1",
"ngx-markdown": "^9.1.1",
"polygon-clipping": "^0.15.7",
"primeng-lts": "^9.2.8",
"quill": "^1.3.7",
"rbush": "^3.0.1",
@ -124,4 +110,4 @@
},
"websocket-driver": "0.7.3"
}
}
}

View File

@ -11,12 +11,6 @@
"changeOrigin": false,
"logLevel": "debug"
},
"/images/*": {
"target": "https://127.0.0.1:4100",
"secure": false,
"changeOrigin": false,
"logLevel": "debug"
},
"/es/uploads/*": {
"target": "https://127.0.0.1:4100",
"pathRewrite": {

View File

@ -22,8 +22,6 @@
<input pInputText type="text" (input)="dt.filter($event.target.value, col.field, col.filterMatchMode)"
[value]="dt.filters[col.field]?.value">
</div>
<p-dropdown *ngIf="col.field === ACTIVE" [options]="activeOpts" [style]="{'width':'100%'}" [ngModel]="dt.filters[col.field]?.value" (onChange)="dt.filter($event.value, col.field, 'equals')"></p-dropdown>
<p-dropdown *ngIf="col.field === KIND" [options]="kindOpts" [style]="{'width':'100%'}" [ngModel]="dt.filters[col.field]?.value" (onChange)="dt.filter($event.value, col.field, 'equals')"></p-dropdown>
<span *ngSwitchDefault></span>
</th>
</tr>

View File

@ -7,7 +7,7 @@ import { User } from '../models/user.model';
import * as fromUsers from '../reducers';
import * as userActions from '../actions/account.actions';
import { RoleIds, Roles, globals, OperationalStatus, Labels } from '@app/shared/global';
import { RoleIds, globals, OperationalStatus, Labels } from '@app/shared/global';
import { BaseComp } from '@app/shared/base/base.component';
import { Utils } from '@app/shared/utils';
@ -21,15 +21,6 @@ export class AccountListComponent extends BaseComp implements OnInit, OnDestroy
readonly resolveFieldData = Utils.resolveFieldData;
readonly KIND = 'kind';
readonly ACTIVE = OperationalStatus.ACTIVE;
activeOpts = [
{ label: globals.all, value: null },
{ label: globals.active, value: true },
{ label: globals.notActive, value: false },
];
kindOpts = [
{ label: globals.all, value: null },
...Object.entries(Roles).map(([value, label]) => ({ label: label as string, value }))
];
accounts: Array<User>;
isLoading: boolean;
currAcc: User;

View File

@ -69,16 +69,6 @@ const routes: Routes = [
loadChildren: () => import('./tools/tools.module').then(m => m.ToolsModule),
runGuardsAndResolvers: 'always',
},
{
path: 'dlq',
loadChildren: () => import('./tools/dlq-monitor/dlq-monitor.module').then(m => m.DlqMonitorModule),
runGuardsAndResolvers: 'always',
},
{
path: 'dealers',
loadChildren: () => import('./dealers/dealers.module').then(m => m.DealersModule),
runGuardsAndResolvers: 'always',
},
{
path: 'track',
loadChildren: () => import('./track/track.module').then(m => m.TrackModule),
@ -100,21 +90,6 @@ const routes: Routes = [
loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule),
runGuardsAndResolvers: 'always'
},
{
path: 'api-keys',
loadChildren: () => import('./settings/api-keys/api-keys.module').then(m => m.ApiKeysModule),
runGuardsAndResolvers: 'always'
},
{
path: 'release-notes',
loadChildren: () => import('./release-notes/release-notes.module').then(m => m.ReleaseNotesModule),
runGuardsAndResolvers: 'always'
},
{
path: 'changelog',
redirectTo: 'release-notes',
pathMatch: 'full'
},
],
},
{

View File

@ -27,16 +27,7 @@
<app-topbar></app-topbar>
<!-- Mobile-only left-edge tab to open/close the navigation panel -->
<button id="mobile-menu-tab" (click)="onMenuButtonClick($event)" aria-label="Toggle navigation">
<i class="material-icons">chevron_right</i>
</button>
<div class="layout-menu" [ngClass]="{'layout-menu-dark':darkMenu}" (click)="onMenuClick($event)">
<div class="menu-user-info" *ngIf="user$ | async as user">
<app-inline-profile [user]="user" [expiryWarning]="expiryWarning$ | async"
(navigateToSubscription)="onNavigateToManageSubscription()"></app-inline-profile>
</div>
<app-menu></app-menu>
</div>

View File

@ -405,7 +405,7 @@ export class AppMainComponent implements AfterViewInit, OnDestroy, OnInit, After
get showFooter() {
return (
!(['/editMap', '/areas', '/track', '/home']
!(['/editMap', '/areas', '/track']
.filter(it => {
const re = new RegExp(it.toLowerCase(), 'i');
return this.router.url.toLocaleLowerCase().match(re);

View File

@ -12,7 +12,7 @@ import { SubKeys } from './profile/common';
selector: 'app-menu',
template: `
<ul class="ultima-menu ultima-main-menu clearfix">
<li app-menuitem *ngFor="let item of model; let i = index;" [item]="item" [index]="i" [root]="true" [class]="item.badgeClass"></li>
<li app-menuitem *ngFor="let item of model; let i = index;" [item]="item" [index]="i" [root]="true"></li>
</ul>
`
})
@ -39,18 +39,8 @@ 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'] },
{
id: 'tools',
label: $localize`:@@tools:Tools`, icon: 'extension',
routerLink: ['/tools'],
items: [
{ id: 'api-keys', label: $localize`:@@apiKeys:API Keys`, icon: 'vpn_key', routerLink: ['/api-keys'] },
{ id: 'dlq-monitor', label: $localize`:@@dlqMonitor:DLQ Monitor`, icon: 'bug_report', routerLink: ['/dlq'] }
]
},
{
id: 'settings',
label: $localize`:@@settings:Settings`, icon: 'settings',
@ -59,7 +49,6 @@ export class AppMenuComponent implements OnInit {
{ id: 'subscription', label: $localize`:@@promoManagement:Promo Management`, icon: 'credit_card', routerLink: ['/settings/subscription'] }
]
},
{ id: 'release-notes', label: $localize`:@@releaseNotes:Release Notes`, icon: 'history', routerLink: ['/release-notes'] },
];
this.model = mItems;
}
@ -76,9 +65,13 @@ export class AppMenuComponent implements OnInit {
{
id: 'Help',
label: $localize`:@@help:Help`, icon: 'help_outline',
items: this.buildHelpMenuItems(),
badge: '1'
} as any
items: [{
label: $localize`:@@trainingVideos:Training Videos`,
icon: 'video_library',
url: 'https://www.youtube.com/watch?v=QjGZan5QdAo&list=PLSMll_kIgHA3eamxiSH0Dgl95v60okMcV',
target: '_blank'
}]
}
];
this.model = mItems;
}
@ -120,28 +113,14 @@ export class AppMenuComponent implements OnInit {
{
id: 'Help',
label: $localize`:@@help:Help`, icon: 'help_outline',
items: this.buildHelpMenuItems(),
badge: '1'
} as any
)
}
private buildHelpMenuItems(): MenuItem[] {
return [
{
id: 'release-notes',
label: $localize`:@@releaseNotes:Release Notes`,
icon: 'history',
routerLink: ['/release-notes'],
badge: '1'
} as any,
{
label: $localize`:@@trainingVideos:Training Videos`,
icon: 'video_library',
url: 'https://www.youtube.com/watch?v=QjGZan5QdAo&list=PLSMll_kIgHA3eamxiSH0Dgl95v60okMcV',
target: '_blank'
items: [{
label: $localize`:@@trainingVideos:Training Videos`,
icon: 'video_library',
url: 'https://www.youtube.com/watch?v=QjGZan5QdAo&list=PLSMll_kIgHA3eamxiSH0Dgl95v60okMcV',
target: '_blank'
}]
}
];
)
}
private addOnlyTrackingItems(mItems: MenuItem[]) {
@ -230,10 +209,7 @@ export class AppMenuComponent implements OnInit {
items: [
{ 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])
? [{ id: 'api-keys', label: $localize`:@@apiKeys:API Keys`, icon: 'vpn_key', routerLink: ['/api-keys'] }]
: [] )
{ id: 'settings', label: $localize`:@@settings:Settings`, icon: 'settings', routerLink: ['/tools/settings'] }
]
}
);

View File

@ -15,22 +15,20 @@ const DUMP_PKEY = '-1';
template: `
<ng-container>
<a [attr.href]="item.url" (click)="itemClick($event)" *ngIf="!item.routerLink || item.items"
(mouseenter)="onMouseEnter()" class="ripplelink"
[ngClass]="{'active-menuitem-routerlink': selected, 'has-notify': item.badge && !active }"
(mouseenter)="onMouseEnter()" class="ripplelink"
[ngClass]="{'active-menuitem-routerlink': selected }"
[attr.target]="item.target" [attr.tabindex]="0">
<i *ngIf="item.icon" class="material-icons">{{item.icon}}</i>
<span class="menu-label-wrap">
<span>{{item.label}}</span>
<span class="menu-notify-counter" *ngIf="item.badge && !active">{{item.badge}}</span>
</span>
<span>{{item.label}}</span>
<span class="menuitem-badge" *ngIf="item.badge">{{item.badge}}</span>
<i class="material-icons submenu-icon" *ngIf="item.items">keyboard_arrow_down</i>
</a>
<a (click)="itemClick($event)" (mouseenter)="onMouseEnter()" *ngIf="item.routerLink && !item.items"
[routerLink]="item.routerLink" routerLinkActive="active-menuitem-routerlink" class="ripplelink"
<a (click)="itemClick($event)" (mouseenter)="onMouseEnter()" *ngIf="item.routerLink && !item.items"
[routerLink]="item.routerLink" routerLinkActive="active-menuitem-routerlink" class="ripplelink"
[routerLinkActiveOptions]="{exact: true}" [attr.target]="item.target" [attr.tabindex]="0">
<i *ngIf="item.icon" class="material-icons">{{item.icon}}</i>
<span>{{item.label}}</span>
<span class="menu-notify-counter menu-notify-inline" *ngIf="item.badge">{{item.badge}}</span>
<span class="menuitem-badge" *ngIf="item.badge">{{item.badge}}</span>
<i class="material-icons submenu-icon" *ngIf="item.items">keyboard_arrow_down</i>
</a>
<div class="layout-menu-tooltip">

View File

@ -16,7 +16,6 @@ import { ConfirmationService, MessageService } from 'primeng/api';
import { ToastModule } from 'primeng/toast';
import { ScrollPanelModule } from 'primeng/scrollpanel';
import { CheckboxModule } from 'primeng/checkbox';
import { TooltipModule } from 'primeng/tooltip';
import { AppComponent } from './app.component';
import { AppMainComponent } from './app.main.component';
@ -25,9 +24,9 @@ import { AppMenuitemComponent } from './app.menuitem.component';
import { AppTopbarComponent } from './app.topbar.component';
import { AppInlineProfileComponent } from './app.profile.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { ReportComponent } from './report.component';
import { PageNotFoundComponent } from './page-not-found.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { StoreModule, ActionsSubject } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
@ -88,8 +87,7 @@ export function translationsFactory(locale: string) {
imports: [
BrowserModule, BrowserAnimationsModule, HttpClientModule, GlobalModule,
InputTextModule, ButtonModule, MenuModule, ProgressSpinnerModule, ScrollPanelModule,
MessagesModule, ToastModule, ConfirmDialogModule, DialogModule, DropdownModule, CheckboxModule, TooltipModule, AppSharedModule,
DashboardModule,
MessagesModule, ToastModule, ConfirmDialogModule, DialogModule, DropdownModule, CheckboxModule, AppSharedModule,
// The store that defines our app state
StoreModule.forRoot(reducers, {
metaReducers,
@ -110,6 +108,7 @@ export function translationsFactory(locale: string) {
MapBaseComp,
MapEditBaseComp,
PageNotFoundComponent,
DashboardComponent,
AppComponent,
AppMainComponent,
AppMenuComponent,

View File

@ -8,18 +8,15 @@
.account-summary-info .account-username {
margin-right: 0.5em;
text-align:center;
}
.account-summary-info .account-type {
margin-right: 0.5em;
font-style: italic;
opacity: 0.85;
text-align:center;
}
.account-summary-info .account-contact {
color: #ffd700;
opacity: 0.9;
text-align:center;
}

View File

@ -4,7 +4,6 @@ import { Client } from "../models/client.model";
export const FETCH = '[CLIENTS] Fetch clients';
export class Fetch implements Action {
type: typeof FETCH = FETCH;
constructor(readonly payload?: { filters?: string; useCache?: boolean }) { }
}
export const FETCH_SUCCESS = '[CLIENTS] Fetch clients success';

View File

@ -0,0 +1,6 @@
/*
To disable deselect or reselect an item on a table row
Ref:https://stackoverflow.com/questions/48675497/how-to-disable-the-option-to-deselect-a-row-on-turbotable-component*/
tr.ui-state-highlight {
pointer-events: none;
}

View File

@ -0,0 +1,42 @@
<div class="ui-g">
<div class="ui-g-12">
<div class="card">
<p-table #dt [value]="clients" [columns]="cols" selectionMode="single" (onRowSelect)="onRowSelect($event)" [paginator]="true" [rows]="15" [pageLinks]="5" [rowsPerPageOptions]="[15,30,50]" [alwaysShowPaginator]="false" [(selection)]="currClient" dataKey="_id" [resetPageOnSort]="false" stateStorage="session" stateKey="cltb-ops" [responsive]="true">
<ng-template pTemplate="caption">
<span class="table-caption-1" i18n="@@clientList">Client List</span>
</ng-template>
<ng-template pTemplate="header" let-columns>
<tr>
<th *ngFor="let col of columns" [pSortableColumn]="col.field" [width]="col.width">{{col.header}}<p-sortIcon [field]="col.field"></p-sortIcon>
</th>
</tr>
<tr>
<th *ngFor="let col of columns" [ngSwitch]="col.filtered" class="ui-fluid">
<div class="input-with-icon" *ngSwitchCase="true">
<i class="ui-icon-search"></i>
<input pInputText type="text" (input)="dt.filter($event.target.value, col.field, col.filterMatchMode)" [value]="dt.filters[col.field]?.value">
</div>
<span *ngSwitchDefault></span>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-client let-rowData let-columns>
<tr [pSelectableRow]="client">
<td *ngFor="let col of cols">
<span class="ui-column-title">{{col.header}}</span>
{{resolveFieldData(rowData, col.field)}}
</td>
</tr>
</ng-template>
</p-table>
<div class="ui-widget-header ui-helper-clearfix toolbar">
<button type="button" *ngIf="canWrite" pButton icon="ui-icon-plus" (click)="newClient()" i18n-label="@@new" label="New"></button>
<button type="button" pButton icon="ui-icon-edit" [disabled]="!canEdit" (click)="editClient()" i18n-label="@@detail" label="Detail"></button>
<button type="button" *ngIf="canWrite" [disabled]="!canEdit" pButton icon="ui-icon-trash" (click)="deleteClient()" i18n-label="@@delete" label="Delete"></button>
<div class="float-right">
<button type="button" [disabled]="!canEdit" pButton icon="ui-icon-view-list" (click)="toJobList()" i18n-label="@@viewJobs" label="View Jobs"></button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,111 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Table } from 'primeng/table';
import { Client } from '../models/client.model';
import * as fromClients from '../reducers';
import * as clientActions from '../actions/client.actions';
import { RoleIds, globals } from '../../shared/global';
import { JobService } from '../../domain/services/job.service';
import { Utils } from 'src/app/shared/utils';
import { BaseComp } from 'src/app/shared/base/base.component';
@Component({
selector: 'agm-client-list',
templateUrl: './client-list.component.html',
styleUrls: ['./client-list.component.css']
})
export class ClientListComponent extends BaseComp implements OnInit, OnDestroy {
resolveFieldData = Utils.resolveFieldData;
clients: Array<Client>;
currClient: Client;
@ViewChild('dt') dt: Table;
cols: any[];
loading$ = this.store.select(fromClients.isLoading);
get canWrite(): boolean {
return this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM, RoleIds.OFFICER]);
}
constructor(
private readonly route: ActivatedRoute,
private readonly jobService: JobService,
) {
super();
}
ngOnInit() {
this.cols = [
{ field: 'name', header: globals.name, filtered: true, filterMatchMode: 'contains' },
{ field: 'username', header: globals.userName, filtered: true, filterMatchMode: 'contains' },
{ field: 'address', header: globals.address },
{ field: 'contact', header: globals.contact, filtered: true },
{ field: 'phone', header: globals.phone + ' ' + $localize`:@@Num:N°`, width: '15%', filtered: true },
{ field: 'email', header: globals.email, filtered: true, filterMatchMode: 'contains' }
];
// These reference entity services and logic should apply to logged in logic later
this.sub$ = this.store.select(fromClients.getAllClients).subscribe(clients => this.clients = clients);
this.sub$.add(this.store.select(fromClients.getSelectedClient).subscribe((client) => {
this.currClient = client;
}));
this.store.dispatch(new clientActions.Fetch());
}
onRowSelect(event) {
this.store.dispatch(new clientActions.Select(event.data));
}
get canEdit() {
return (this.currClient && this.currClient._id !== '0');
}
newClient() {
this.router.navigate(['client', '0'], { relativeTo: this.route });
}
editClient() {
this.router.navigate(['client', this.currClient._id], { relativeTo: this.route });
}
deleteClient() {
this.jobService.countByClient(this.currClient._id).subscribe((count) => {
return this.confirmDelete(count);
});
}
private confirmDelete(count: number) {
let msg = globals.confirmDeleteThing.replace('#thing#', globals.client);
if (count > 0) {
let ref = count === 1 ? $localize`:@@relatedJobSingular:There is #jobs# related job` : $localize`:@@relatedJobPlural:There are #jobs# related jobs`;
ref = ref.replace('#jobs#', String(count));
msg = ref + '. ' + msg;
}
this.confirmSvc.confirm({
message: msg,
accept: () => {
this.store.dispatch(new clientActions.Delete(this.currClient));
this.currClient = null;
}
});
}
toJobList() {
this.router.navigate(['/jobs']);
}
ngOnDestroy() {
super.ngOnDestroy();
}
}

View File

@ -13,7 +13,6 @@ import { DropdownModule } from 'primeng/dropdown';
import { TableModule } from 'primeng/table';
import { ToastModule } from 'primeng/toast';
import { AccordionModule } from 'primeng/accordion';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
@ -29,7 +28,7 @@ import { AppSharedModule } from '../shared/app-shared.module';
@NgModule({
imports: [
CommonModule, TableModule, PaginatorModule, DialogModule, ConfirmDialogModule, ToastModule, MessagesModule, InputTextModule,
CheckboxModule, ToolbarModule, ButtonModule, DropdownModule, AccordionModule, AppSharedModule,
CheckboxModule, ToolbarModule, ButtonModule, DropdownModule, AppSharedModule,
StoreModule.forFeature(fromClients.FEATURE_KEY, fromClients.reducer),
EffectsModule.forFeature([ClientEffects]),
ClientsRoutingModule

View File

@ -10,7 +10,6 @@ import { ClientService } from '@app/domain/services/client.service';
import { AuthService } from '@app/domain/services/auth.service';
import { AppMessageService } from '@app/shared/app-message.service';
import { globals } from '@app/shared/global';
import { ClientCacheService } from '@app/domain/services/client-cache.service';
@Injectable()
export class ClientEffects {
@ -18,20 +17,15 @@ export class ClientEffects {
private readonly actions$: Actions,
private readonly clientSvc: ClientService,
private readonly authSvc: AuthService,
private readonly msgSvc: AppMessageService,
private readonly clientCache: ClientCacheService
private readonly msgSvc: AppMessageService
) {
}
@Effect()
loadClients$: Observable<Action> = this.actions$.pipe(
ofType<clientActions.Fetch>(clientActions.FETCH),
switchMap(({ payload }) =>
this.clientSvc.loadClients({
byPuid: this.authSvc.user.parent,
useCache: payload?.useCache,
...(payload?.filters ? { filters: payload.filters } : {})
}).pipe(
switchMap(() =>
this.clientSvc.loadClients({ byPuid: this.authSvc.user.parent }).pipe(
map(clients => new clientActions.FetchSuccess(clients)),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.load).replace('#thing#', globals.clients));
@ -46,10 +40,7 @@ export class ClientEffects {
ofType<clientActions.Create>(clientActions.CREATE),
switchMap(({ payload }) =>
this.clientSvc.saveClient(payload).pipe(
map((client) => {
this.clientCache.invalidate();
return new clientActions.CreateSuccess(client);
}),
map((client) => new clientActions.CreateSuccess(client)),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.create).replace('#thing#', globals.client));
return of(new clientActions.CreateFailed())
@ -63,9 +54,7 @@ export class ClientEffects {
ofType<clientActions.Update>(clientActions.UPDATE),
switchMap(({ payload }) =>
this.clientSvc.saveClient(payload).pipe(
map(() => {
return new clientActions.UpdateSuccess(payload);
}),
map(() => new clientActions.UpdateSuccess(payload)),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.save).replace('#thing#', globals.client));
return of(new clientActions.UpdateFailed());
@ -79,10 +68,7 @@ export class ClientEffects {
ofType<clientActions.Delete>(clientActions.DELETE),
switchMap(({ payload }) =>
this.clientSvc.deleteClient(payload).pipe(
map(() => {
this.clientCache.invalidate();
return new clientActions.DeleteSuccess(payload);
}),
map(() => new clientActions.DeleteSuccess(payload)),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.delete).replace('#thing#', globals.client));
return of(new clientActions.UpdateFailed())

View File

@ -4,7 +4,6 @@ import { Customer } from "../models/customer.model";
export const FETCH = '[CUSTOMERS] Fetch customers';
export class Fetch implements Action {
type: typeof FETCH = FETCH;
constructor(readonly payload?: { filters?: string; useCache?: boolean }) {}
}
export const FETCH_SUCCESS = '[CUSTOMERS] Fetch customers success';

View File

@ -56,20 +56,6 @@
</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>
@ -104,12 +90,6 @@
required="true" i18n-title="@@accessAccount" title="Access Account" showActive="true">
</agm-account-editor>
</div>
<!-- API Key Manager (existing customers only) -->
<div class="ui-g-12" *ngIf="!isNew">
<agm-api-key-manager [ownerId]="customer._id" [toggleable]="true" [collapsed]="true"></agm-api-key-manager>
</div>
<div class="ui-g-12 toolbar padtop1 ui-fluid">
<button pButton [disabled]="form.invalid || partnerLoading" type="button" style="width:auto"
[icon]="isNew ? 'ui-icon-plus' : 'ui-icon-save'" [label]="isNew ? globals.create : globals.save"

View File

@ -6,7 +6,6 @@ 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/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';
@ -41,10 +40,6 @@ 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) {
@ -56,8 +51,7 @@ 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,
dealer: this.selectedItem.dealer || null
partner: this.selectedItem.partner || null
});
// Set partner selection based on customer.partner field, or null if not set
@ -73,7 +67,6 @@ 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();
@ -90,9 +83,7 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
billable: [],
trials: [],
// Partner form control
partner: [null],
// Dealer form control
dealer: [null]
partner: [null]
});
this.lang = this.authSvc.locale;
@ -115,7 +106,6 @@ export class CustomerEditComponent extends BaseComp implements OnInit {
}
// Load partners from service
this.loadPartners();
this.loadDealers();
}
});
@ -181,8 +171,7 @@ 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 },
{ dealer: this.form.value.dealer?._id || this.form.value.dealer || null });
{ partner: this.form.value.partner || null });
this.membership
? custObj = Object.assign(custObj, { membership: updateTrialMembship(this.membership) })
@ -220,32 +209,6 @@ 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.code
? (d.country ? `${d.code} - ${d.companyName} (${d.country})` : `${d.code} - ${d.companyName}`)
: (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;

View File

@ -1,25 +1,15 @@
<div class="ui-g">
<div class="ui-g-12">
<div class="card">
<p-accordion styleClass="agm-accordion" [style]="{'display':'block', 'margin-bottom':'0.75rem'}">
<p-accordionTab i18n-header="@@searchCustomers" header="Search Customers" [transitionOptions]="'250ms'" [selected]="searchAccordionOpen"
(selectedChange)="searchAccordionOpen = $event; onAccordionToggle($event)">
<agm-dynamic-filter [filterDefinitions]="customerFilterDefinitions" [locale]="locale" stateKey="customers-list-filters" (filtersSubmit)="onFiltersSubmit($event)"></agm-dynamic-filter>
</p-accordionTab>
</p-accordion>
<p-table #dt [value]="customers" [columns]="cols" selectionMode="single" (onRowSelect)="onRowSelect($event)" [paginator]="true" [rows]="15" [pageLinks]="5" [rowsPerPageOptions]="[10, 15, 30]" [alwaysShowPaginator]="true" [(selection)]="curCust" dataKey="_id" [resetPageOnSort]="false" stateStorage="session" stateKey="ctb-ops" [responsive]="true">
<ng-template pTemplate="caption">
<div class="ui-g ui-g-nopad cache-ttl-caption">
<div class="ui-g-6 cc-field-label cache-ttl-caption-title">
<span class="table-caption-1" style="display:block; text-align:left;" i18n="@@customerList">Customer List</span>
<div class="ui-g ui-g-nopad">
<div class="ui-g-6 cc-field-label">
<span class="table-caption-1" i18n="@@customerList">Customer List</span>
</div>
<div class="ui-g-6 cc-field-label cache-ttl-caption-controls">
<input pInputText type="number" min="0" step="1" placeholder="Cache TTL" [(ngModel)]="cacheTtlSeconds"
(blur)="updateCacheTtl()" style="width: 3.5rem; margin-right: 8px;">
<span class="cache-ttl-help" tabindex="0">
<span class="cache-ttl-help-icon">?</span>
<span class="cache-ttl-help-text">Controls how long results stay cached after you return to this page. Value is in seconds.</span>
</span>
<div class="ui-g-6 cc-field-label">
<label style="margin-right: 8px;">Self Signup Accounts {{ isSelfSignup ? 'On' : 'Off' }}</label>
<p-inputSwitch [(ngModel)]="isSelfSignup" (onChange)="onToggle($event)"></p-inputSwitch>
</div>
</div>
</ng-template>
@ -40,10 +30,6 @@
<p-dropdown *ngIf="col.field === PARTNER_NAME" [options]="partners" [style]="{'width':'100%'}" [ngModel]="dt.filters[col.field]?.value" (onChange)="dt.filter($event.value, col.field, 'equals')"></p-dropdown>
<div class="input-with-icon" *ngIf="col.field === 'contact'">
<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>
<span *ngSwitchDefault></span>
</th>
</tr>

View File

@ -10,9 +10,6 @@ import * as customerActions from '../actions/customer.actions';
import { globals, OperationalStatus } from '@app/shared/global';
import { BaseComp } from '@app/shared/base/base.component';
import { CustomerCacheService } from '@app/domain/services/customer-cache.service';
import { ListReturnCacheService } from '@app/domain/services/list-return-cache.service';
import { FilterDefinition, FilterChangeEvent } from '@app/shared/dynamic-filter/dynamic-filter.component';
@Component({
selector: 'agm-customer-list',
@ -35,20 +32,13 @@ export class CustomerListComponent extends BaseComp implements OnInit, OnDestroy
partners: SelectItem[];
cols: any[];
totalItems;
searchAccordionOpen = sessionStorage.getItem('customers-list-accordion') === 'true';
private lastFiltersQuery: Record<string, any> | undefined;
private useCacheOnReturn = false;
cacheTtlSeconds: number;
customerFilterDefinitions: FilterDefinition[];
isSelfSignup = false;
constructor(
private readonly route: ActivatedRoute,
private readonly customerCache: CustomerCacheService,
private readonly listReturnCache: ListReturnCacheService,
) {
super();
this.cacheTtlSeconds = Math.round(this.customerCache.getTtlMs() / 1000);
this.totalItems = { '=0': '', '=1': '1 ' + $localize`:@@customer:customer`.toLocaleLowerCase(), 'other': $localize`:@@total#Customers:Total: # customers` };
this.statuses = [
@ -66,21 +56,12 @@ export class CustomerListComponent extends BaseComp implements OnInit, OnDestroy
{ field: this.ACTIVE, header: globals.active, width: '9%' },
{ field: this.PARTNER_NAME, header: globals.partner, width: '9%' }
];
this.customerFilterDefinitions = [
{ key: 'name', label: globals.name, dataType: 'text' },
{ key: 'username', label: globals.userName, dataType: 'text' },
{ key: 'email', label: globals.email, dataType: 'text' },
{ key: 'contact', label: globals.contact, dataType: 'text' },
{ key: 'createdAt', label: globals.from, dataType: 'date-preset' },
{ key: 'selfSignup', label: 'Self Signup', dataType: 'select', options: [
{ label: 'True', value: true },
{ label: 'False', value: false },
]},
];
}
ngOnInit() {
const saved = localStorage.getItem('isSelfSignup');
this.isSelfSignup = saved === 'true';
this.sub$ = this.store.select(fromCustomers.getAllCustomers).subscribe(customers => {
this.setCustomersAndPartners(customers);
});
@ -89,23 +70,12 @@ export class CustomerListComponent extends BaseComp implements OnInit, OnDestroy
this.curCust = cust;
}));
this.useCacheOnReturn = this.listReturnCache.startVisit('customers');
const savedFilters = sessionStorage.getItem('customers-list-last-filters');
if (savedFilters) {
try {
this.lastFiltersQuery = JSON.parse(savedFilters);
} catch (_err) {
this.lastFiltersQuery = undefined;
}
}
this.store.dispatch(savedFilters
? new customerActions.Fetch({ filters: savedFilters, useCache: this.useCacheOnReturn })
: new customerActions.Fetch({ useCache: this.useCacheOnReturn })
);
this.store.dispatch(new customerActions.Fetch());
}
private setCustomersAndPartners(customers: Customer[]) {
this.customers = customers.map(c => ({
const filtered = this.isSelfSignup ? customers.filter(c => c.selfSignup) : customers;
this.customers = filtered.map(c => ({
...c,
partnerName: c.partner?.name || null
}));
@ -119,32 +89,18 @@ export class CustomerListComponent extends BaseComp implements OnInit, OnDestroy
];
}
onToggle(event: any): void {
this.isSelfSignup = event.checked;
localStorage.setItem('isSelfSignup', String(this.isSelfSignup));
this.store.select(fromCustomers.getAllCustomers).subscribe(customers => {
this.setCustomersAndPartners(customers);
});
}
onRowSelect(event) {
this.store.dispatch(new customerActions.Select(event.data));
}
onAccordionToggle(expanded: boolean) {
sessionStorage.setItem('customers-list-accordion', String(expanded));
}
updateCacheTtl(): void {
const ttlMs = this.customerCache.setTtlMs(Number(this.cacheTtlSeconds || 0) * 1000);
this.cacheTtlSeconds = Math.round(ttlMs / 1000);
}
onFiltersSubmit(event: FilterChangeEvent) {
const q = { ...event.query };
const filtersStr = JSON.stringify(q);
const prevFilters = sessionStorage.getItem('customers-list-last-filters');
if (filtersStr !== prevFilters) {
this.customerCache.invalidate();
this.useCacheOnReturn = false;
}
this.lastFiltersQuery = q;
sessionStorage.setItem('customers-list-last-filters', filtersStr);
this.store.dispatch(new customerActions.Fetch({ filters: filtersStr, useCache: this.useCacheOnReturn }));
}
get canEdit() {
return (this.curCust && this.curCust._id !== '0');
}
@ -154,7 +110,6 @@ export class CustomerListComponent extends BaseComp implements OnInit, OnDestroy
}
editCustomer() {
this.listReturnCache.markPending('customers');
this.router.navigate(['customer', this.curCust._id], { relativeTo: this.route });
}

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