mirror of
https://github.com/SuiteCRM/SuiteCRM-Core.git
synced 2025-08-29 04:21:06 +08:00
Add RecordView configurable TopWidgets
- Structure re-factor -- Move record view components to record view folder -- Move record view store to record view folder -- Add containers folder --- add @containers alias -- Move Subpanels to containers folder -- Move subpanel-statistics.store to single-value-statistics.store - Make administration repair a cache reset action - Add dynamic top widget component - Add statistics top widget component - Add support for recordView defs topWidget configuration - Adjust and standardize Statistic API structure -- Add metadata to response. --- Metadata should drive widgets behavior -- Change existing StatisticsProviderInterface implementations to send back metadata - Add SubpanelDataQueryHandler to make custom queries easier -- Allows to get the query done to fetch subpanel data -- Allows to run custom queries - Add WonOpportunityAmountByYear using custom query - Implement SubpanelOpportunitiesTotal using custom query - Add karma/jasmine tests
This commit is contained in:
parent
3b0fd86837
commit
139ad660b8
97 changed files with 1192 additions and 204 deletions
|
@ -16,6 +16,7 @@ module.exports = {
|
|||
["@fields", "./core/app/fields"],
|
||||
["@services", "./core/app/src/services"],
|
||||
["@components", "./core/app/src/components"],
|
||||
["@containers", "./core/app/src/containers"],
|
||||
["@store", "./core/app/src/store"]
|
||||
],
|
||||
"extensions": [".ts", ".js"]
|
||||
|
|
|
@ -2,3 +2,5 @@ parameters:
|
|||
legacy.cache_reset_actions:
|
||||
users:
|
||||
- edit
|
||||
administration:
|
||||
- repair
|
||||
|
|
9
core/app/src/app-common/metadata/widget.metadata.ts
Normal file
9
core/app/src/app-common/metadata/widget.metadata.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export interface WidgetMetadata {
|
||||
type: string;
|
||||
labelKey?: string;
|
||||
options: WidgetOptionMap;
|
||||
}
|
||||
|
||||
export interface WidgetOptionMap {
|
||||
[key: string]: any;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import {ViewContext} from '@app-common/views/view.model';
|
||||
|
||||
export interface StatisticsQueryMap {
|
||||
[key: string]: StatisticsQuery;
|
||||
}
|
||||
|
@ -5,6 +7,7 @@ export interface StatisticsQueryMap {
|
|||
export interface StatisticsQuery {
|
||||
key: string;
|
||||
params: any;
|
||||
context: ViewContext;
|
||||
}
|
||||
|
||||
export interface StatisticsMap {
|
||||
|
@ -14,14 +17,21 @@ export interface StatisticsMap {
|
|||
export interface Statistic {
|
||||
id: string;
|
||||
data: any;
|
||||
metadata?: StatisticMetadata;
|
||||
}
|
||||
|
||||
export interface SubpanelStatisticsData {
|
||||
export interface StatisticMetadata {
|
||||
[key: string]: any;
|
||||
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SingleValueStatisticsData {
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SubpanelStatistic extends Statistic {
|
||||
export interface SingleValueStatistic extends Statistic {
|
||||
id: string;
|
||||
data: SubpanelStatisticsData;
|
||||
data: SingleValueStatisticsData;
|
||||
}
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
export type ViewMode = 'detail' | 'edit' | 'list';
|
||||
|
||||
export interface ViewContext {
|
||||
module: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import {Router} from '@angular/router';
|
|||
import {AuthGuard} from '@services/auth/auth-guard.service';
|
||||
import {BaseModuleResolver} from '@services/metadata/base-module.resolver';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {RecordComponent} from '@views/record/record.component';
|
||||
import {RecordComponent} from '@views/record/components/record-view/record.component';
|
||||
import {BaseRecordResolver} from '@services/metadata/base-record.resolver';
|
||||
import {LoginAuthGuard} from '@services/auth/login-auth-guard.service';
|
||||
import {BaseMetadataResolver} from '@services/metadata/base-metadata.resolver';
|
||||
|
|
|
@ -22,7 +22,7 @@ import {ModuleTitleModule} from '@components/module-title/module-title.module';
|
|||
import {ListHeaderModule} from '@components/list-header/list-header.module';
|
||||
import {ListContainerModule} from '@components/list-container/list-container.module';
|
||||
import {ListModule} from '@views/list/list.module';
|
||||
import {RecordModule} from '@views/record/record.module';
|
||||
import {RecordModule} from '@views/record/components/record-view/record.module';
|
||||
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
|
|
|
@ -2,8 +2,8 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
|||
import {HistoryTimelineWidgetComponent} from './history-timeline-widget.component';
|
||||
import {CollectionViewer, ListRange} from '@angular/cdk/collections';
|
||||
import {of} from 'rxjs';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ScrollingModule} from '@angular/cdk/scrolling';
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {CollectionViewer, DataSource} from '@angular/cdk/collections';
|
||||
import {HistoryTimelineEntry} from '@components/history-timeline-widget/history-timeline-widget.model';
|
||||
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
||||
|
@ -230,7 +229,7 @@ export class HistoryTimelineAdapter extends DataSource<HistoryTimelineEntry> {
|
|||
private fetchedPages = new Set<number>();
|
||||
private pageSize = 5;
|
||||
|
||||
constructor(protected store: RecordViewStore) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import {SettingsMenuModule} from '@components/settings-menu/settings-menu.module
|
|||
import {ImageModule} from '@components/image/image.module';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ButtonModule} from '@components/button/button.module';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
|
||||
@Component({
|
||||
selector: 'status-bar-test-host-component',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import {Observable} from 'rxjs';
|
||||
import {SubpanelStoreMap} from '@store/subpanel/subpanel.store';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
|
||||
export interface SubpanelContainerConfig {
|
||||
subpanels$: Observable<SubpanelStoreMap>;
|
||||
recordStore: RecordViewStore;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import { ActionHandlerMap } from '@base/app-common/actions/action.model';
|
||||
import {RecordActionData} from '@views/record/actions/record.action';
|
||||
import {SubPanelCreateAction} from './actions/create/create.action';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SubPanelActionManager {
|
||||
|
||||
actions: ActionHandlerMap = {
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected create: SubPanelCreateAction
|
||||
) {
|
||||
this.actions[create.key] = create;
|
||||
}
|
||||
|
||||
run(action: string, data: RecordActionData): void {
|
||||
this.actions[action].run(data);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ActionData, ActionHandler} from '@app-common/actions/action.model';
|
||||
import {ModuleNameMapper} from '@services/navigation/module-name-mapper/module-name-mapper.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {SubpanelActionData, SubpanelActionHandler} from '@containers/subpanel/actions/subpanel.action';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SubPanelCreateAction extends ActionHandler {
|
||||
export class SubpanelCreateAction extends SubpanelActionHandler {
|
||||
key = 'create';
|
||||
|
||||
constructor(
|
||||
|
@ -17,22 +17,20 @@ export class SubPanelCreateAction extends ActionHandler {
|
|||
super();
|
||||
}
|
||||
|
||||
run(data: ActionData): void {
|
||||
const store = data.store;
|
||||
const action = data.action;
|
||||
run(data: SubpanelActionData): void {
|
||||
|
||||
const moduleName = action.module;
|
||||
const moduleName = data.subpanelMeta.module;
|
||||
|
||||
const route = `/${moduleName}/edit`;
|
||||
|
||||
this.router.navigate([route], {
|
||||
queryParams: {
|
||||
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase
|
||||
return_module: this.moduleNameMapper.toLegacy(moduleName),
|
||||
return_module: this.moduleNameMapper.toLegacy(data.parentModule),
|
||||
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase
|
||||
return_action: 'DetailView',
|
||||
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase
|
||||
return_record: (store.getRecord() && store.getRecord().id) || ''
|
||||
return_id: data.parentId || ''
|
||||
}
|
||||
}).then();
|
||||
}
|
15
core/app/src/containers/subpanel/actions/subpanel.action.ts
Normal file
15
core/app/src/containers/subpanel/actions/subpanel.action.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {ActionData, ActionHandler} from '@app-common/actions/action.model';
|
||||
|
||||
export interface SubpanelActionData extends ActionData {
|
||||
parentId: string;
|
||||
parentModule: string;
|
||||
}
|
||||
|
||||
export interface SubpanelActionHandlerMap {
|
||||
[key: string]: SubpanelActionHandler;
|
||||
}
|
||||
|
||||
export abstract class SubpanelActionHandler extends ActionHandler {
|
||||
|
||||
abstract run(data: SubpanelActionData): void;
|
||||
}
|
|
@ -4,7 +4,7 @@ import {LanguageStore} from '@store/language/language.store';
|
|||
import {SortDirection} from '@components/sort-button/sort-button.model';
|
||||
import {TableConfig} from '@components/table/table.model';
|
||||
import {ColumnDefinition} from '@app-common/metadata/list.metadata.model';
|
||||
import {SubpanelStore} from '@store/subpanel/subpanel.store';
|
||||
import {SubpanelStore} from '@containers/subpanel/store/subpanel/subpanel.store';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
|
@ -53,7 +53,6 @@
|
|||
|
||||
<scrm-subpanel class="sub-panel"
|
||||
[store]="item.value"
|
||||
[recordStore]="config.recordStore"
|
||||
[maxColumns$]="maxColumns$"
|
||||
*ngIf="item.value.show">
|
||||
</scrm-subpanel>
|
|
@ -2,8 +2,8 @@ import {Component, Input, OnInit} from '@angular/core';
|
|||
import {map, take, tap} from 'rxjs/operators';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {LanguageStore, LanguageStringMap, LanguageStrings} from '@store/language/language.store';
|
||||
import {SubpanelContainerConfig} from '@components/subpanel-container/subpanel-container.model';
|
||||
import {SubpanelStore, SubpanelStoreMap} from '@store/subpanel/subpanel.store';
|
||||
import {SubpanelContainerConfig} from '@containers/subpanel/components/subpanel-container/subpanel-container.model';
|
||||
import {SubpanelStore, SubpanelStoreMap} from '@containers/subpanel/store/subpanel/subpanel.store';
|
||||
import {MaxColumnsCalculator} from '@services/ui/max-columns-calculator/max-columns-calculator.service';
|
||||
|
||||
interface SubpanelContainerViewModel {
|
||||
|
@ -54,7 +54,7 @@ export class SubpanelContainerComponent implements OnInit {
|
|||
}
|
||||
|
||||
getMaxColumns(): Observable<number> {
|
||||
return this.maxColumnCalculator.getMaxColumns(this.config.recordStore.widgets$);
|
||||
return this.maxColumnCalculator.getMaxColumns(this.config.sidebarActive$);
|
||||
}
|
||||
|
||||
toggleSubPanels(): void {
|
|
@ -0,0 +1,7 @@
|
|||
import {Observable} from 'rxjs';
|
||||
import {SubpanelStoreMap} from '@containers/subpanel/store/subpanel/subpanel.store';
|
||||
|
||||
export interface SubpanelContainerConfig {
|
||||
subpanels$: Observable<SubpanelStoreMap>;
|
||||
sidebarActive$: Observable<boolean>;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AppManagerModule} from '../../app-manager/app-manager.module';
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {SubpanelContainerComponent} from './subpanel-container.component';
|
||||
import {SubpanelModule} from '../subpanel/subpanel.module';
|
||||
import {ImageModule} from '@components/image/image.module';
|
|
@ -0,0 +1,21 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {SubpanelCreateAction} from '../../actions/create/create.action';
|
||||
import {SubpanelActionData, SubpanelActionHandlerMap} from '@containers/subpanel/actions/subpanel.action';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SubpanelActionManager {
|
||||
|
||||
actions: SubpanelActionHandlerMap = {};
|
||||
|
||||
constructor(
|
||||
protected create: SubpanelCreateAction
|
||||
) {
|
||||
this.actions[create.key] = create;
|
||||
}
|
||||
|
||||
run(action: string, data: SubpanelActionData): void {
|
||||
this.actions[action].run(data);
|
||||
}
|
||||
}
|
|
@ -5,16 +5,16 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
|
|||
import {RouterModule} from '@angular/router';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {MetadataStore} from '@base/store/metadata/metadata.store.service';
|
||||
import {MetadataStore} from '@store/metadata/metadata.store.service';
|
||||
import {ImageModule} from '@components/image/image.module';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {languageStoreMock} from '@store/language/language.store.spec.mock';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {ApolloTestingModule} from 'apollo-angular/testing';
|
||||
import {ButtonGroupModule} from '../button-group/button-group.module';
|
||||
import {PanelModule} from '../panel/panel.module';
|
||||
import {ButtonGroupModule} from '@components/button-group/button-group.module';
|
||||
import {PanelModule} from '@components/panel/panel.module';
|
||||
import {SubpanelComponent} from './subpanel.component';
|
||||
import {metadataStoreMock} from '@store/metadata/metadata.store.spec.mock';
|
||||
|
||||
|
@ -22,10 +22,9 @@ const store = recordviewStoreMock.getSubpanels().contacts;
|
|||
|
||||
@Component({
|
||||
selector: 'subpanel-test-host-component',
|
||||
template: '<scrm-subpanel [recordStore]="recordStore" [store]="store"></scrm-subpanel>'
|
||||
template: '<scrm-subpanel [store]="store"></scrm-subpanel>'
|
||||
})
|
||||
class SubpanelComponentTestHostComponent {
|
||||
recordStore = recordviewStoreMock;
|
||||
store = store;
|
||||
}
|
||||
|
15
core/app/src/components/subpanel/subpanel.component.ts → core/app/src/containers/subpanel/components/subpanel/subpanel.component.ts
Executable file → Normal file
15
core/app/src/components/subpanel/subpanel.component.ts → core/app/src/containers/subpanel/components/subpanel/subpanel.component.ts
Executable file → Normal file
|
@ -4,12 +4,11 @@ import {AnyButtonInterface} from '@components/dropdown-button/dropdown-button.mo
|
|||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {shareReplay} from 'rxjs/operators';
|
||||
import {ButtonGroupInterface} from '../button-group/button-group.model';
|
||||
import {SubPanelActionManager} from './actions-mananger.service';
|
||||
import {SubpanelTableAdapter} from '@components/subpanel/adapter/table.adapter';
|
||||
import {SubpanelStore} from '@store/subpanel/subpanel.store';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {ButtonGroupInterface} from '@components/button-group/button-group.model';
|
||||
import {SubpanelTableAdapter} from '@containers/subpanel/adapters/table.adapter';
|
||||
import {SubpanelStore} from '@containers/subpanel/store/subpanel/subpanel.store';
|
||||
import {TableConfig} from '@components/table/table.model';
|
||||
import {SubpanelActionManager} from '@containers/subpanel/components/subpanel/action-manager.service';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-subpanel',
|
||||
|
@ -21,14 +20,13 @@ import {TableConfig} from '@components/table/table.model';
|
|||
export class SubpanelComponent implements OnInit {
|
||||
@Input() store: SubpanelStore;
|
||||
@Input() maxColumns$: Observable<number>;
|
||||
@Input() recordStore: RecordViewStore;
|
||||
|
||||
adapter: SubpanelTableAdapter;
|
||||
config$: Observable<ButtonGroupInterface>;
|
||||
tableConfig: TableConfig;
|
||||
|
||||
constructor(
|
||||
protected actionManager: SubPanelActionManager,
|
||||
protected actionManager: SubpanelActionManager,
|
||||
protected languages: LanguageStore,
|
||||
) {
|
||||
}
|
||||
|
@ -94,7 +92,8 @@ export class SubpanelComponent implements OnInit {
|
|||
onClick: (): void => {
|
||||
this.actionManager.run(action.key, {
|
||||
subpanelMeta: this.store.metadata,
|
||||
store: this.recordStore,
|
||||
parentModule: this.store.parentModule,
|
||||
parentId: this.store.parentId,
|
||||
action
|
||||
});
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AppManagerModule} from '../../app-manager/app-manager.module';
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {SubpanelComponent} from './subpanel.component';
|
||||
import {PanelModule} from '@components/panel/panel.module';
|
||||
import {ImageModule} from '@components/image/image.module';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import {ButtonGroupModule} from '../button-group/button-group.module';
|
||||
import {ButtonGroupModule} from '@components/button-group/button-group.module';
|
||||
import {TableModule} from '@components/table/table.module';
|
||||
|
||||
@NgModule({
|
|
@ -1,8 +1,8 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {SubpanelStore} from '@store/subpanel/subpanel.store';
|
||||
import {SubpanelStore} from '@containers/subpanel/store/subpanel/subpanel.store';
|
||||
import {RecordListStoreFactory} from '@store/record-list/record-list.store.factory';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {SubpanelStatisticsStoreFactory} from '@store/subpanel/subpanel-statistics.store.factory';
|
||||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -12,7 +12,7 @@ export class SubpanelStoreFactory {
|
|||
constructor(
|
||||
protected listStoreFactory: RecordListStoreFactory,
|
||||
protected languageStore: LanguageStore,
|
||||
protected statisticsStoreFactory: SubpanelStatisticsStoreFactory
|
||||
protected statisticsStoreFactory: SingleValueStatisticsStoreFactory
|
||||
) {
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {SubpanelStoreFactory} from '@store/subpanel/subpanel.store.factory';
|
||||
import {SubpanelStoreFactory} from '@containers/subpanel/store/subpanel/subpanel.store.factory';
|
||||
import {languageStoreMock} from '@store/language/language.store.spec.mock';
|
||||
import {listStoreFactoryMock} from '@store/record-list/record-list.store.spec.mock';
|
||||
import {subpanelStatisticsFactoryMock} from '@store/subpanel/subpanel-statistics.store.spec.mock';
|
||||
import {subpanelStatisticsFactoryMock} from '@store/single-value-statistics/single-value-statistics.store.spec.mock';
|
||||
|
||||
export const subpanelFactoryMock = new SubpanelStoreFactory(
|
||||
listStoreFactoryMock,
|
|
@ -6,8 +6,8 @@ import {RecordListStoreFactory} from '@store/record-list/record-list.store.facto
|
|||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {SubPanel} from '@app-common/metadata/subpanel.metadata.model';
|
||||
import {Statistic, StatisticsQuery} from '@app-common/statistics/statistics.model';
|
||||
import {SubpanelStatisticsStore} from '@store/subpanel/subpanel-statistics.store';
|
||||
import {SubpanelStatisticsStoreFactory} from '@store/subpanel/subpanel-statistics.store.factory';
|
||||
import {SingleValueStatisticsStore} from '@store/single-value-statistics/single-value-statistics.store';
|
||||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
|
||||
export interface SubpanelStoreMap {
|
||||
[key: string]: SubpanelStore;
|
||||
|
@ -17,17 +17,19 @@ export interface SubpanelStoreMap {
|
|||
export class SubpanelStore implements StateStore {
|
||||
show = false;
|
||||
parentModule: string;
|
||||
parentId: string;
|
||||
recordList: RecordListStore;
|
||||
statistics: SubpanelStatisticsStore;
|
||||
statistics: SingleValueStatisticsStore;
|
||||
metadata$: Observable<SubPanel>;
|
||||
metadata: SubPanel;
|
||||
loading$: Observable<boolean>;
|
||||
protected metadataState: BehaviorSubject<SubPanel>;
|
||||
|
||||
|
||||
constructor(
|
||||
protected listStoreFactory: RecordListStoreFactory,
|
||||
protected languageStore: LanguageStore,
|
||||
protected statisticsStoreFactory: SubpanelStatisticsStoreFactory
|
||||
protected statisticsStoreFactory: SingleValueStatisticsStoreFactory
|
||||
) {
|
||||
this.recordList = listStoreFactory.create();
|
||||
this.statistics = statisticsStoreFactory.create();
|
||||
|
@ -72,6 +74,7 @@ export class SubpanelStore implements StateStore {
|
|||
*/
|
||||
public init(parentModule: string, parentId: string, meta: SubPanel): void {
|
||||
this.parentModule = parentModule;
|
||||
this.parentId = parentId;
|
||||
this.metadata = meta;
|
||||
this.metadataState.next(this.metadata);
|
||||
this.recordList.init(meta.module, false, 'list_max_entries_per_subpanel');
|
||||
|
@ -134,9 +137,9 @@ export class SubpanelStore implements StateStore {
|
|||
meta.module,
|
||||
{
|
||||
key: meta.module,
|
||||
params: {
|
||||
parentModule,
|
||||
parentId
|
||||
context: {
|
||||
module: parentModule,
|
||||
id: parentId
|
||||
}
|
||||
} as StatisticsQuery,
|
||||
false
|
|
@ -0,0 +1,35 @@
|
|||
<div *ngIf="(vm$ | async) as vm"
|
||||
class="d-flex justify-content-center widget-bar">
|
||||
<div class="p-2 widget-bar-entry-message" *ngIf="this.messageLabelKey">
|
||||
{{vm.appStrings[this.messageLabelKey] || '' | uppercase}}
|
||||
</div>
|
||||
<ng-container *ngFor="let item of statistics | keyvalue">
|
||||
|
||||
<div class="d-flex justify-content-center widget-bar-entry">
|
||||
|
||||
<div class="p-2 widget-bar-entry-label" *ngIf="item.value.labelKey && vm.appStrings[item.value.labelKey]">
|
||||
{{vm.appStrings[item.value.labelKey] | uppercase}}:
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="item.key && vm.statistics[item.key]">
|
||||
|
||||
<div class="p-2 widget-bar-entry-value"
|
||||
*ngIf="!vm.statistics[item.key].loading && vm.statistics[item.key].field">
|
||||
<scrm-field [type]="vm.statistics[item.key].field.type" [field]="vm.statistics[item.key].field"
|
||||
mode="list"></scrm-field>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<div class="p-2 widget-bar-entry-loading" *ngIf="(item.value.store.loading$ | async) as loading">
|
||||
<scrm-inline-loading-spinner></scrm-inline-loading-spinner>
|
||||
|
||||
<ng-container *ngIf="!loading && (!item.key || !vm.statistics[item.key])">
|
||||
-
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
import {StatisticsFetchGQL} from '@store/statistics/graphql/api.statistics.get';
|
||||
import {StatisticsMap, StatisticsQueryMap} from '@app-common/statistics/statistics.model';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {shareReplay} from 'rxjs/operators';
|
||||
|
||||
class StatisticsFetchGQLSpy extends StatisticsFetchGQL {
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public fetch(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
queries: StatisticsQueryMap,
|
||||
): Observable<StatisticsMap> {
|
||||
|
||||
if (queries.opportunities) {
|
||||
return of({
|
||||
history: {
|
||||
id: 'opportunities',
|
||||
data: {
|
||||
value: '5400'
|
||||
},
|
||||
metadata: {
|
||||
dataType: 'currency',
|
||||
type: 'single-value-statistic'
|
||||
}
|
||||
}
|
||||
}).pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
if (queries['accounts-won-opportunity-amount-by-year']) {
|
||||
return of({
|
||||
history: {
|
||||
id: 'accounts-won-opportunity-amount-by-year',
|
||||
data: {
|
||||
value: '1466.6666666666667'
|
||||
},
|
||||
metadata: {
|
||||
dataType: 'currency',
|
||||
type: 'single-value-statistic'
|
||||
}
|
||||
}
|
||||
}).pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const topWidgetStatisticsFactoryMock = new SingleValueStatisticsStoreFactory(
|
||||
new StatisticsFetchGQLSpy(),
|
||||
);
|
|
@ -0,0 +1,124 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {StatisticsTopWidgetComponent} from './statistics-top-widget.component';
|
||||
import {Component} from '@angular/core';
|
||||
import {ViewContext} from '@app-common/views/view.model';
|
||||
import {WidgetMetadata} from '@app-common/metadata/widget.metadata';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {languageStoreMock} from '@store/language/language.store.spec.mock';
|
||||
import {topWidgetStatisticsFactoryMock} from './statistics-top-widget.component.spec.mock';
|
||||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ApolloTestingModule} from 'apollo-angular/testing';
|
||||
|
||||
@Component({
|
||||
selector: 'statistics-top-widget-test-host-component',
|
||||
template: '<scrm-statistics-top-widget [context]="context" [config]="config"></scrm-statistics-top-widget>'
|
||||
})
|
||||
class StatisticsTopWidgetHostComponent {
|
||||
context: ViewContext = {
|
||||
module: 'accounts',
|
||||
id: '123'
|
||||
};
|
||||
|
||||
config: WidgetMetadata = {
|
||||
type: 'statistics',
|
||||
options: {
|
||||
statistics: [
|
||||
{
|
||||
labelKey: 'LBL_AVERAGE_SPEND_PER_YEAR',
|
||||
type: 'accounts-won-opportunity-amount-by-year'
|
||||
},
|
||||
{
|
||||
labelKey: 'LBL_OPPORTUNITIES_TOTAL',
|
||||
type: 'opportunities'
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('StatisticsTopWidgetComponent', () => {
|
||||
let testHostComponent: StatisticsTopWidgetHostComponent;
|
||||
let testHostFixture: ComponentFixture<StatisticsTopWidgetHostComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
StatisticsTopWidgetHostComponent,
|
||||
StatisticsTopWidgetComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserDynamicTestingModule,
|
||||
FieldModule,
|
||||
CommonModule,
|
||||
RouterTestingModule,
|
||||
ApolloTestingModule
|
||||
],
|
||||
providers: [
|
||||
{provide: LanguageStore, useValue: languageStoreMock},
|
||||
{provide: SingleValueStatisticsStoreFactory, useValue: topWidgetStatisticsFactoryMock},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
testHostFixture = TestBed.createComponent(StatisticsTopWidgetHostComponent);
|
||||
testHostComponent = testHostFixture.componentInstance;
|
||||
testHostFixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(testHostComponent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have statistics', () => {
|
||||
expect(testHostComponent).toBeTruthy();
|
||||
|
||||
const widget = testHostFixture.nativeElement.getElementsByTagName('scrm-statistics-top-widget')[0];
|
||||
|
||||
expect(widget).toBeTruthy();
|
||||
|
||||
const widgetBar = widget.getElementsByClassName('widget-bar').item(0);
|
||||
|
||||
expect(widgetBar).toBeTruthy();
|
||||
|
||||
const entries = widgetBar.getElementsByClassName('widget-bar-entry');
|
||||
|
||||
expect(entries).toBeTruthy();
|
||||
expect(entries.length).toEqual(2);
|
||||
|
||||
const averageSpend = entries.item(0);
|
||||
|
||||
expect(averageSpend).toBeTruthy();
|
||||
|
||||
const averageSpendLabel = averageSpend.getElementsByClassName('widget-bar-entry-label').item(0);
|
||||
const averageSpendValue = averageSpend.getElementsByClassName('widget-bar-entry-value').item(0);
|
||||
const averageSpendField = averageSpendValue.getElementsByTagName('scrm-field').item(0);
|
||||
const averageSpendCurrency = averageSpendField.getElementsByTagName('scrm-currency-detail').item(0);
|
||||
|
||||
expect(averageSpendLabel).toBeTruthy();
|
||||
expect(averageSpendLabel.textContent).toContain('AVERAGE SPEND PER YEAR:');
|
||||
expect(averageSpendValue).toBeTruthy();
|
||||
expect(averageSpendField).toBeTruthy();
|
||||
expect(averageSpendCurrency).toBeTruthy();
|
||||
expect(averageSpendCurrency.textContent).toContain('$1,467');
|
||||
|
||||
const oppTotal = entries.item(1);
|
||||
|
||||
expect(oppTotal).toBeTruthy();
|
||||
|
||||
const oppTotalLabel = oppTotal.getElementsByClassName('widget-bar-entry-label').item(0);
|
||||
const oppTotalValue = oppTotal.getElementsByClassName('widget-bar-entry-value').item(0);
|
||||
const oppTotalField = oppTotalValue.getElementsByTagName('scrm-field').item(0);
|
||||
const oppTotalCurrency = oppTotalField.getElementsByTagName('scrm-currency-detail').item(0);
|
||||
|
||||
expect(oppTotalLabel).toBeTruthy();
|
||||
expect(oppTotalLabel.textContent).toContain('TOTAL OPPORTUNITY VALUE:');
|
||||
expect(oppTotalValue).toBeTruthy();
|
||||
expect(oppTotalField).toBeTruthy();
|
||||
expect(oppTotalCurrency).toBeTruthy();
|
||||
expect(oppTotalCurrency.textContent).toContain('$5,400');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {BaseWidgetComponent} from '@containers/top-widget/top-widget.model';
|
||||
import {
|
||||
SingleValueStatisticsState,
|
||||
SingleValueStatisticsStore
|
||||
} from '@store/single-value-statistics/single-value-statistics.store';
|
||||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
import {map, take} from 'rxjs/operators';
|
||||
import {LanguageStore, LanguageStringMap} from '@store/language/language.store';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {StatisticsQuery} from '@app-common/statistics/statistics.model';
|
||||
|
||||
interface StatisticsTopWidgetState {
|
||||
statistics: { [key: string]: SingleValueStatisticsState };
|
||||
appStrings: LanguageStringMap;
|
||||
}
|
||||
|
||||
interface StatisticsEntry {
|
||||
labelKey: string;
|
||||
type: string;
|
||||
store: SingleValueStatisticsStore;
|
||||
}
|
||||
|
||||
interface StatisticsEntryMap {
|
||||
[key: string]: StatisticsEntry;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-statistics-top-widget',
|
||||
templateUrl: './statistics-top-widget.component.html',
|
||||
styles: []
|
||||
})
|
||||
export class StatisticsTopWidgetComponent extends BaseWidgetComponent implements OnInit {
|
||||
statistics: StatisticsEntryMap = {};
|
||||
vm$: Observable<StatisticsTopWidgetState>;
|
||||
messageLabelKey: string;
|
||||
|
||||
constructor(
|
||||
protected language: LanguageStore,
|
||||
protected factory: SingleValueStatisticsStoreFactory
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
|
||||
if (!this.context || !this.context.module) {
|
||||
this.messageLabelKey = 'LBL_BAD_CONFIG_BAD_CONTEXT';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config) {
|
||||
this.messageLabelKey = 'LBL_BAD_CONFIG_NO_CONFIG';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.options || !this.config.options.statistics || !this.config.options.statistics.length) {
|
||||
this.messageLabelKey = 'LBL_BAD_CONFIG_NO_STATISTICS_KEY';
|
||||
return;
|
||||
}
|
||||
|
||||
const statistics$: Observable<SingleValueStatisticsState>[] = [];
|
||||
this.config.options.statistics.forEach(statistic => {
|
||||
|
||||
if (!statistic.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.statistics[statistic.type] = {
|
||||
labelKey: statistic.labelKey || '',
|
||||
type: statistic.type,
|
||||
store: this.factory.create()
|
||||
};
|
||||
|
||||
this.statistics[statistic.type].store.init(
|
||||
this.context.module,
|
||||
{
|
||||
key: statistic.type,
|
||||
context: {...this.context}
|
||||
} as StatisticsQuery,
|
||||
).pipe(take(1)).subscribe();
|
||||
|
||||
statistics$.push(this.statistics[statistic.type].store.state$);
|
||||
});
|
||||
|
||||
|
||||
this.vm$ = combineLatest([combineLatest(statistics$), this.language.appStrings$]).pipe(
|
||||
map(([statistics, appStrings]) => {
|
||||
const statsMap: { [key: string]: SingleValueStatisticsState } = {};
|
||||
statistics.forEach(value => {
|
||||
statsMap[value.statistic.id] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
statistics: statsMap,
|
||||
appStrings
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {StatisticsTopWidgetComponent} from '@base/containers/top-widget/components/statistics-top-widget/statistics-top-widget.component';
|
||||
import {InlineLoadingSpinnerModule} from '@components/inline-loading-spinner/inline-loading-spinner.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [StatisticsTopWidgetComponent],
|
||||
exports: [
|
||||
StatisticsTopWidgetComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FieldModule,
|
||||
InlineLoadingSpinnerModule
|
||||
]
|
||||
})
|
||||
export class StatisticsTopWidgetModule {
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<ndc-dynamic *ngIf="type && config"
|
||||
[ndcDynamicComponent]="componentType"
|
||||
[ndcDynamicInputs]="{
|
||||
'config': config,
|
||||
'context': context
|
||||
}"
|
||||
></ndc-dynamic>
|
|
@ -0,0 +1,131 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {ViewContext} from '@app-common/views/view.model';
|
||||
import {WidgetMetadata} from '@app-common/metadata/widget.metadata';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {languageStoreMock} from '@store/language/language.store.spec.mock';
|
||||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
import {topWidgetStatisticsFactoryMock} from '../statistics-top-widget/statistics-top-widget.component.spec.mock';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ApolloTestingModule} from 'apollo-angular/testing';
|
||||
import {TopWidgetModule} from '@containers/top-widget/components/top-widget/top-widget.module';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'top-widget-test-host-component',
|
||||
template: '<scrm-top-widget [type]="type" [context]="context" [config]="config"></scrm-top-widget>'
|
||||
})
|
||||
class TopWidgetHostComponent {
|
||||
type = 'statistics';
|
||||
|
||||
context: ViewContext = {
|
||||
module: 'accounts',
|
||||
id: '123'
|
||||
};
|
||||
|
||||
config: WidgetMetadata = {
|
||||
type: 'statistics',
|
||||
options: {
|
||||
statistics: [
|
||||
{
|
||||
labelKey: 'LBL_AVERAGE_SPEND_PER_YEAR',
|
||||
type: 'accounts-won-opportunity-amount-by-year'
|
||||
},
|
||||
{
|
||||
labelKey: 'LBL_OPPORTUNITIES_TOTAL',
|
||||
type: 'opportunities'
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('TopWidgetComponent', () => {
|
||||
let testHostComponent: TopWidgetHostComponent;
|
||||
let testHostFixture: ComponentFixture<TopWidgetHostComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TopWidgetHostComponent,
|
||||
],
|
||||
imports: [
|
||||
TopWidgetModule,
|
||||
BrowserDynamicTestingModule,
|
||||
FieldModule,
|
||||
CommonModule,
|
||||
RouterTestingModule,
|
||||
ApolloTestingModule
|
||||
],
|
||||
providers: [
|
||||
{provide: LanguageStore, useValue: languageStoreMock},
|
||||
{provide: SingleValueStatisticsStoreFactory, useValue: topWidgetStatisticsFactoryMock},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
testHostFixture = TestBed.createComponent(TopWidgetHostComponent);
|
||||
testHostComponent = testHostFixture.componentInstance;
|
||||
testHostFixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(testHostComponent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have statistics widget', () => {
|
||||
expect(testHostComponent).toBeTruthy();
|
||||
|
||||
const topWidget = testHostFixture.nativeElement.getElementsByTagName('scrm-top-widget')[0];
|
||||
|
||||
expect(topWidget).toBeTruthy();
|
||||
|
||||
const widget = topWidget.getElementsByTagName('scrm-statistics-top-widget').item(0);
|
||||
|
||||
expect(widget).toBeTruthy();
|
||||
|
||||
const widgetBar = widget.getElementsByClassName('widget-bar').item(0);
|
||||
|
||||
expect(widgetBar).toBeTruthy();
|
||||
|
||||
const entries = widgetBar.getElementsByClassName('widget-bar-entry');
|
||||
|
||||
expect(entries).toBeTruthy();
|
||||
expect(entries.length).toEqual(2);
|
||||
|
||||
const averageSpend = entries.item(0);
|
||||
|
||||
expect(averageSpend).toBeTruthy();
|
||||
|
||||
const averageSpendLabel = averageSpend.getElementsByClassName('widget-bar-entry-label').item(0);
|
||||
const averageSpendValue = averageSpend.getElementsByClassName('widget-bar-entry-value').item(0);
|
||||
const averageSpendField = averageSpendValue.getElementsByTagName('scrm-field').item(0);
|
||||
const averageSpendCurrency = averageSpendField.getElementsByTagName('scrm-currency-detail').item(0);
|
||||
|
||||
expect(averageSpendLabel).toBeTruthy();
|
||||
expect(averageSpendLabel.textContent).toContain('AVERAGE SPEND PER YEAR:');
|
||||
expect(averageSpendValue).toBeTruthy();
|
||||
expect(averageSpendField).toBeTruthy();
|
||||
expect(averageSpendCurrency).toBeTruthy();
|
||||
expect(averageSpendCurrency.textContent).toContain('$1,467');
|
||||
|
||||
const oppTotal = entries.item(1);
|
||||
|
||||
expect(oppTotal).toBeTruthy();
|
||||
|
||||
const oppTotalLabel = oppTotal.getElementsByClassName('widget-bar-entry-label').item(0);
|
||||
const oppTotalValue = oppTotal.getElementsByClassName('widget-bar-entry-value').item(0);
|
||||
const oppTotalField = oppTotalValue.getElementsByTagName('scrm-field').item(0);
|
||||
const oppTotalCurrency = oppTotalField.getElementsByTagName('scrm-currency-detail').item(0);
|
||||
|
||||
expect(oppTotalLabel).toBeTruthy();
|
||||
expect(oppTotalLabel.textContent).toContain('TOTAL OPPORTUNITY VALUE:');
|
||||
expect(oppTotalValue).toBeTruthy();
|
||||
expect(oppTotalField).toBeTruthy();
|
||||
expect(oppTotalCurrency).toBeTruthy();
|
||||
expect(oppTotalCurrency.textContent).toContain('$5,400');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {componentTypeMap} from '@containers/top-widget/components/top-widget/top-widget.manifest';
|
||||
import {BaseWidgetComponent} from '@containers/top-widget/top-widget.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-top-widget',
|
||||
templateUrl: './top-widget.component.html',
|
||||
styles: []
|
||||
})
|
||||
export class TopWidgetComponent extends BaseWidgetComponent implements OnInit {
|
||||
|
||||
@Input('type') type: string;
|
||||
|
||||
map = componentTypeMap;
|
||||
|
||||
get componentType(): any {
|
||||
return this.map[this.type];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import {StatisticsTopWidgetModule} from '@containers/top-widget/components/statistics-top-widget/statistics-top-widget.module';
|
||||
import {StatisticsTopWidgetComponent} from '@containers/top-widget/components/statistics-top-widget/statistics-top-widget.component';
|
||||
|
||||
export const topWidgetModules = [
|
||||
StatisticsTopWidgetModule,
|
||||
];
|
||||
|
||||
export const componentTypeMap = {
|
||||
statistics: StatisticsTopWidgetComponent,
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {TopWidgetComponent} from './top-widget.component';
|
||||
import {DynamicModule} from 'ng-dynamic-component';
|
||||
import {topWidgetModules} from '@containers/top-widget/components/top-widget/top-widget.manifest';
|
||||
|
||||
@NgModule({
|
||||
declarations: [TopWidgetComponent],
|
||||
exports: [TopWidgetComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
...topWidgetModules,
|
||||
DynamicModule,
|
||||
]
|
||||
})
|
||||
export class TopWidgetModule {
|
||||
}
|
8
core/app/src/containers/top-widget/top-widget.model.ts
Normal file
8
core/app/src/containers/top-widget/top-widget.model.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import {Input} from '@angular/core';
|
||||
import {WidgetMetadata} from '@app-common/metadata/widget.metadata';
|
||||
import {ViewContext} from '@app-common/views/view.model';
|
||||
|
||||
export class BaseWidgetComponent {
|
||||
@Input('config') config: WidgetMetadata;
|
||||
@Input('context') context: ViewContext;
|
||||
}
|
|
@ -40,7 +40,9 @@ export const languageMockData = {
|
|||
LBL_PANEL_ASSIGNMENT: 'OTHER',
|
||||
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase
|
||||
lbl_account_information: 'OVERVIEW',
|
||||
LBL_PANEL_ADVANCED: 'MORE INFORMATION'
|
||||
LBL_PANEL_ADVANCED: 'MORE INFORMATION',
|
||||
LBL_AVERAGE_SPEND_PER_YEAR: 'Average Spend Per Year',
|
||||
LBL_OPPORTUNITIES_TOTAL: 'Total Opportunity Value',
|
||||
},
|
||||
appListStrings: {
|
||||
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase
|
||||
|
|
|
@ -10,8 +10,10 @@ import {ModeActions} from '@app-common/actions/action.model';
|
|||
import {ColumnDefinition, ListViewMeta, SearchMeta} from '@app-common/metadata/list.metadata.model';
|
||||
import {LineAction} from '@app-common/actions/line-action.model';
|
||||
import {SubPanelMeta} from '@app-common/metadata/subpanel.metadata.model';
|
||||
import {WidgetMetadata} from '@app-common/metadata/widget.metadata';
|
||||
|
||||
export interface RecordViewMetadata {
|
||||
topWidget?: WidgetMetadata;
|
||||
actions: ModeActions;
|
||||
templateMeta: RecordTemplateMetadata;
|
||||
panels: Panel[];
|
||||
|
@ -310,7 +312,7 @@ export class MetadataStore implements StateStore {
|
|||
};
|
||||
|
||||
const receivedMeta = data.viewDefinition.recordView;
|
||||
const entries = {templateMeta: 'templateMeta', actions: 'actions', panels: 'panels'};
|
||||
const entries = {templateMeta: 'templateMeta', actions: 'actions', panels: 'panels', topWidget: 'topWidget'};
|
||||
|
||||
this.addDefinedMeta(recordViewMeta, receivedMeta, entries);
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {StatisticsFetchGQL} from '@store/statistics/graphql/api.statistics.get';
|
||||
import {SingleValueStatisticsStore} from '@store/single-value-statistics/single-value-statistics.store';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SingleValueStatisticsStoreFactory {
|
||||
|
||||
constructor(protected fetchGQL: StatisticsFetchGQL) {
|
||||
}
|
||||
|
||||
create(): SingleValueStatisticsStore {
|
||||
return new SingleValueStatisticsStore(this.fetchGQL);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import {SubpanelStatisticsStoreFactory} from '@store/subpanel/subpanel-statistics.store.factory';
|
||||
import {SingleValueStatisticsStoreFactory} from '@store/single-value-statistics/single-value-statistics.store.factory';
|
||||
import {StatisticsFetchGQL} from '@store/statistics/graphql/api.statistics.get';
|
||||
import {StatisticsMap, StatisticsQueryMap} from '@app-common/statistics/statistics.model';
|
||||
import {Observable, of} from 'rxjs';
|
||||
|
@ -27,6 +27,6 @@ class StatisticsFetchGQLSpy extends StatisticsFetchGQL {
|
|||
}
|
||||
}
|
||||
|
||||
export const subpanelStatisticsFactoryMock = new SubpanelStatisticsStoreFactory(
|
||||
export const subpanelStatisticsFactoryMock = new SingleValueStatisticsStoreFactory(
|
||||
new StatisticsFetchGQLSpy(),
|
||||
);
|
|
@ -2,10 +2,10 @@ import {Injectable} from '@angular/core';
|
|||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {deepClone} from '@base/utils/object-utils';
|
||||
import {
|
||||
SingleValueStatistic,
|
||||
SingleValueStatisticsData,
|
||||
Statistic,
|
||||
StatisticsQuery,
|
||||
SubpanelStatistic,
|
||||
SubpanelStatisticsData
|
||||
StatisticsQuery
|
||||
} from '@app-common/statistics/statistics.model';
|
||||
import {StatisticsFetchGQL} from '@store/statistics/graphql/api.statistics.get';
|
||||
import {StatisticsState, StatisticsStore} from '@store/statistics/statistics.store';
|
||||
|
@ -17,24 +17,24 @@ const initialState = {
|
|||
query: {} as StatisticsQuery,
|
||||
statistic: {
|
||||
id: '',
|
||||
data: {} as SubpanelStatisticsData
|
||||
} as SubpanelStatistic,
|
||||
data: {} as SingleValueStatisticsData
|
||||
} as SingleValueStatistic,
|
||||
loading: false
|
||||
} as SubpanelStatisticsState;
|
||||
} as SingleValueStatisticsState;
|
||||
|
||||
export interface SubpanelStatisticsState extends StatisticsState {
|
||||
statistic: SubpanelStatistic;
|
||||
export interface SingleValueStatisticsState extends StatisticsState {
|
||||
statistic: SingleValueStatistic;
|
||||
field?: Field;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SubpanelStatisticsStore extends StatisticsStore {
|
||||
state$: Observable<SubpanelStatisticsState>;
|
||||
export class SingleValueStatisticsStore extends StatisticsStore {
|
||||
state$: Observable<SingleValueStatisticsState>;
|
||||
statistic$: Observable<Statistic>;
|
||||
loading$: Observable<boolean>;
|
||||
protected cache$: Observable<any> = null;
|
||||
protected internalState: SubpanelStatisticsState = deepClone(initialState);
|
||||
protected store = new BehaviorSubject<SubpanelStatisticsState>(this.internalState);
|
||||
protected internalState: SingleValueStatisticsState = deepClone(initialState);
|
||||
protected store = new BehaviorSubject<SingleValueStatisticsState>(this.internalState);
|
||||
|
||||
constructor(
|
||||
protected fetchGQL: StatisticsFetchGQL,
|
||||
|
@ -46,7 +46,12 @@ export class SubpanelStatisticsStore extends StatisticsStore {
|
|||
}
|
||||
|
||||
protected addNewState(statistic: Statistic): void {
|
||||
const field = FieldManager.buildShallowField(statistic.data.type, statistic.data.value);
|
||||
|
||||
if (!statistic.metadata || !statistic.metadata.dataType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = FieldManager.buildShallowField(statistic.metadata.dataType, statistic.data.value);
|
||||
|
||||
field.metadata = {
|
||||
digits: 0
|
||||
|
@ -65,7 +70,7 @@ export class SubpanelStatisticsStore extends StatisticsStore {
|
|||
*
|
||||
* @param {object} state to set
|
||||
*/
|
||||
protected updateState(state: SubpanelStatisticsState): void {
|
||||
protected updateState(state: SingleValueStatisticsState): void {
|
||||
super.updateState(state);
|
||||
}
|
||||
}
|
|
@ -21,12 +21,12 @@ export class StateManager {
|
|||
protected navigationStore: NavigationStore,
|
||||
protected systemConfigStore: SystemConfigStore,
|
||||
protected themeImagesStore: ThemeImagesStore,
|
||||
protected userPreferenceStore: UserPreferenceStore
|
||||
protected userPreferenceStore: UserPreferenceStore,
|
||||
) {
|
||||
this.stateStores.appStore = this.buildMapEntry(appStore, false);
|
||||
this.stateStores.navigationStore = this.buildMapEntry(navigationStore, true);
|
||||
this.stateStores.languageStore = this.buildMapEntry(languageStore, true);
|
||||
this.stateStores.listViewMetaStore = this.buildMapEntry(metadataStore, false);
|
||||
this.stateStores.metadataStore = this.buildMapEntry(metadataStore, false);
|
||||
this.stateStores.systemConfigStore = this.buildMapEntry(systemConfigStore, false);
|
||||
this.stateStores.themeImagesStore = this.buildMapEntry(themeImagesStore, false);
|
||||
this.stateStores.userPreferenceStore = this.buildMapEntry(userPreferenceStore, true);
|
||||
|
|
|
@ -33,7 +33,8 @@ export class StatisticsFetchGQL {
|
|||
edges {
|
||||
node {
|
||||
_id,
|
||||
data
|
||||
data,
|
||||
metadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +57,8 @@ export class StatisticsFetchGQL {
|
|||
statistics[key] = {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
id: edge.node._id,
|
||||
data: edge.node.data
|
||||
data: edge.node.data,
|
||||
metadata: edge.node.metadata
|
||||
} as Statistic;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {StatisticsFetchGQL} from '@store/statistics/graphql/api.statistics.get';
|
||||
import {SubpanelStatisticsStore} from '@store/subpanel/subpanel-statistics.store';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SubpanelStatisticsStoreFactory {
|
||||
|
||||
constructor(protected fetchGQL: StatisticsFetchGQL) {
|
||||
}
|
||||
|
||||
create(): SubpanelStatisticsStore {
|
||||
return new SubpanelStatisticsStore(this.fetchGQL);
|
||||
}
|
||||
}
|
15
core/app/themes/suite8/css/components/_widget-bar.scss
Normal file
15
core/app/themes/suite8/css/components/_widget-bar.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
.widget-bar {
|
||||
background-color: $coral-pink;
|
||||
color: $white;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.widget-bar .widget-bar-entry-value {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.widget-bar .inline-spinner > div {
|
||||
width: 0.45em;
|
||||
height: 0.45em;
|
||||
background-color: white;
|
||||
}
|
|
@ -20,6 +20,7 @@
|
|||
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
@import 'components/loading-spinner';
|
||||
@import 'components/inline-loading-spinner';
|
||||
@import 'components/widget';
|
||||
@import 'components/widget-bar';
|
||||
@import 'components/chips';
|
||||
@import 'layout/panel';
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
"@components/*": [
|
||||
"./src/components/*"
|
||||
],
|
||||
"@containers/*": [
|
||||
"./src/containers/*"
|
||||
],
|
||||
"@store/*": [
|
||||
"./src/store/*"
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {RecordActionData, RecordActionHandler} from '@views/record/actions/record.action';
|
||||
import {ViewMode} from '@app-common/views/view.model';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {ActionData, ActionHandler} from '@app-common/actions/action.model';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {ViewMode} from '@app-common/views/view.model';
|
||||
|
||||
export interface RecordActionData extends ActionData {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {RecordActionsAdapter} from '@store/record-view/adapters/actions.adapter';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {RecordActionsAdapter} from '@views/record/adapters/actions.adapter';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {languageStoreMock} from '@store/language/language.store.spec.mock';
|
||||
import {metadataStoreMock} from '@store/metadata/metadata.store.spec.mock';
|
||||
import {recordActionsManagerMock} from '@views/record/actions/record-action-manager.service.spec.mock';
|
|
@ -1,5 +1,5 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {MetadataStore} from '@store/metadata/metadata.store.service';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {Action, ActionDataSource, ModeActions} from '@app-common/actions/action.model';
|
||||
|
@ -79,9 +79,7 @@ export class RecordActionsAdapter implements ActionDataSource {
|
|||
meta,
|
||||
mode,
|
||||
record,
|
||||
languages,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
widgets
|
||||
languages
|
||||
]
|
||||
) => {
|
||||
if (!mode || !meta) {
|
|
@ -1,4 +1,4 @@
|
|||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {MetadataStore, RecordViewMetadata} from '@store/metadata/metadata.store.service';
|
|
@ -1,11 +1,21 @@
|
|||
<!-- Start Record View Container Section -->
|
||||
|
||||
<div class="record-view-container view-container container-fluid pt-1">
|
||||
<div *ngIf="(vm$ | async) as vm"
|
||||
class="record-view-container view-container container-fluid pt-3">
|
||||
<div class="row">
|
||||
<div class="col-lg-9" [ngClass]="{ 'col-lg-12': !getDisplayWidgets() }">
|
||||
|
||||
<div class="container-fluid pl-0 pr-0">
|
||||
|
||||
<div class="row no-gutters" *ngIf="vm.recordMeta && vm.recordMeta.topWidget && vm.recordMeta.topWidget.type">
|
||||
<div class="col pb-3">
|
||||
<scrm-top-widget [type]="vm.recordMeta.topWidget.type"
|
||||
[context]="getViewContext()"
|
||||
[config]="vm.recordMeta.topWidget">
|
||||
</scrm-top-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<scrm-record-content [dataSource]="getContentAdapter()"></scrm-record-content>
|
|
@ -5,8 +5,8 @@ import {RecordContainerComponent} from './record-container.component';
|
|||
import {WidgetModule} from '@components/widget/widget.module';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {ThemeImagesStore} from '@store/theme-images/theme-images.store';
|
||||
import {of} from 'rxjs';
|
||||
import {themeImagesMockData} from '@store/theme-images/theme-images.store.spec.mock';
|
||||
|
@ -29,7 +29,7 @@ import {metadataStoreMock} from '@store/metadata/metadata.store.spec.mock';
|
|||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
import {appStateStoreMock} from '@store/app-state/app-state.store.spec.mock';
|
||||
import {Router} from '@angular/router';
|
||||
import {SubpanelModule} from '@components/subpanel/subpanel.module';
|
||||
import {SubpanelModule} from '@containers/subpanel/components/subpanel/subpanel.module';
|
||||
|
||||
describe('RecordContainerComponent', () => {
|
||||
let component: RecordContainerComponent;
|
|
@ -1,12 +1,13 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {MetadataStore, RecordViewMetadata} from '@store/metadata/metadata.store.service';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {LanguageStore, LanguageStrings} from '@store/language/language.store';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {RecordContentAdapter} from '@store/record-view/adapters/record-content.adapter';
|
||||
import {RecordContentAdapter} from '@views/record/adapters/record-content.adapter';
|
||||
import {RecordContentDataSource} from '@components/record-content/record-content.model';
|
||||
import {SubpanelContainerConfig} from '@components/subpanel-container/subpanel-container.model';
|
||||
import {SubpanelContainerConfig} from '@containers/subpanel/components/subpanel-container/subpanel-container.model';
|
||||
import {ViewContext} from '@app-common/views/view.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-record-container',
|
||||
|
@ -14,7 +15,6 @@ import {SubpanelContainerConfig} from '@components/subpanel-container/subpanel-c
|
|||
providers: [RecordContentAdapter]
|
||||
})
|
||||
export class RecordContainerComponent implements OnInit {
|
||||
@Input() module;
|
||||
type = '';
|
||||
widgetTitle = '';
|
||||
|
||||
|
@ -60,7 +60,11 @@ export class RecordContainerComponent implements OnInit {
|
|||
getSubpanelsConfig(): SubpanelContainerConfig {
|
||||
return {
|
||||
subpanels$: this.recordViewStore.subpanels$,
|
||||
recordStore: this.recordViewStore
|
||||
sidebarActive$: this.recordViewStore.widgets$
|
||||
} as SubpanelContainerConfig;
|
||||
}
|
||||
|
||||
getViewContext(): ViewContext {
|
||||
return this.recordViewStore.getViewContext();
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AppManagerModule} from '../../app-manager/app-manager.module';
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {RecordContainerComponent} from './record-container.component';
|
||||
import {WidgetModule} from '../widget/widget.module';
|
||||
import {WidgetModule} from '@components/widget/widget.module';
|
||||
import {AngularSvgIconModule} from 'angular-svg-icon';
|
||||
import {SubpanelContainerModule} from '@components/subpanel-container/subpanel-container.module';
|
||||
import {SubpanelContainerModule} from '@containers/subpanel/components/subpanel-container/subpanel-container.module';
|
||||
import {RecordContentModule} from '@components/record-content/record-content.module';
|
||||
import {HistoryTimelineWidgetModule} from '@components/history-timeline-widget/history-timeline-widget.module';
|
||||
import {TopWidgetModule} from '@containers/top-widget/components/top-widget/top-widget.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RecordContainerComponent],
|
||||
|
@ -18,7 +19,8 @@ import {HistoryTimelineWidgetModule} from '@components/history-timeline-widget/h
|
|||
AngularSvgIconModule,
|
||||
SubpanelContainerModule,
|
||||
RecordContentModule,
|
||||
HistoryTimelineWidgetModule
|
||||
HistoryTimelineWidgetModule,
|
||||
TopWidgetModule
|
||||
]
|
||||
})
|
||||
export class RecordContainerModule {
|
|
@ -7,8 +7,8 @@ import {ModuleTitleModule} from '@components/module-title/module-title.module';
|
|||
import {ImageModule} from '@components/image/image.module';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ButtonModule} from '@components/button/button.module';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {ThemeImagesStore} from '@store/theme-images/theme-images.store';
|
||||
import {themeImagesStoreMock} from '@store/theme-images/theme-images.store.spec.mock';
|
||||
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
|
||||
|
@ -25,9 +25,9 @@ import {MetadataStore} from '@store/metadata/metadata.store.service';
|
|||
import {metadataStoreMock} from '@store/metadata/metadata.store.spec.mock';
|
||||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
import {appStateStoreMock} from '@store/app-state/app-state.store.spec.mock';
|
||||
import {RecordSettingsMenuModule} from '@components/record-settings-menu/record-settings-menu.module';
|
||||
import {RecordActionsAdapter} from '@store/record-view/adapters/actions.adapter';
|
||||
import {recordActionsMock} from '@store/record-view/adapters/actions.adapter.spec.mock';
|
||||
import {RecordSettingsMenuModule} from '@views/record/components/record-settings-menu/record-settings-menu.module';
|
||||
import {RecordActionsAdapter} from '@views/record/adapters/actions.adapter';
|
||||
import {recordActionsMock} from '@views/record/adapters/actions.adapter.spec.mock';
|
||||
|
||||
@Component({
|
||||
selector: 'record-header-test-host-component',
|
|
@ -1,9 +1,9 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
|
||||
import {combineLatest} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {RecordActionsAdapter} from '@store/record-view/adapters/actions.adapter';
|
||||
import {RecordActionsAdapter} from '@views/record/adapters/actions.adapter';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-record-header',
|
|
@ -1,9 +1,9 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AppManagerModule} from '../../app-manager/app-manager.module';
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {RecordHeaderComponent} from './record-header.component';
|
||||
import {ModuleTitleModule} from '../module-title/module-title.module';
|
||||
import {RecordSettingsMenuModule} from '@components/record-settings-menu/record-settings-menu.module';
|
||||
import {ModuleTitleModule} from '@components/module-title/module-title.module';
|
||||
import {RecordSettingsMenuModule} from '@views/record/components/record-settings-menu/record-settings-menu.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RecordHeaderComponent],
|
|
@ -8,8 +8,8 @@ import {themeImagesMockData} from '@store/theme-images/theme-images.store.spec.m
|
|||
import {take} from 'rxjs/operators';
|
||||
import {ImageModule} from '@components/image/image.module';
|
||||
import {ButtonModule} from '@components/button/button.module';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
|
||||
import {mockModuleNavigation,} from '@services/navigation/module-navigation/module-navigation.service.spec.mock';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
|
@ -25,10 +25,10 @@ import {metadataStoreMock} from '@store/metadata/metadata.store.spec.mock';
|
|||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
import {appStateStoreMock} from '@store/app-state/app-state.store.spec.mock';
|
||||
import {Component} from '@angular/core';
|
||||
import {RecordSettingsMenuModule} from '@components/record-settings-menu/record-settings-menu.module';
|
||||
import {RecordSettingsMenuModule} from '@views/record/components/record-settings-menu/record-settings-menu.module';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {RecordActionsAdapter} from '@store/record-view/adapters/actions.adapter';
|
||||
import {recordActionsMock} from '@store/record-view/adapters/actions.adapter.spec.mock';
|
||||
import {RecordActionsAdapter} from '@views/record/adapters/actions.adapter';
|
||||
import {recordActionsMock} from '@views/record/adapters/actions.adapter.spec.mock';
|
||||
|
||||
@Component({
|
||||
selector: 'record-setting-test-host-component',
|
|
@ -1,7 +1,7 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {BehaviorSubject, combineLatest, Subscription} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {RecordActionsAdapter} from '@store/record-view/adapters/actions.adapter';
|
||||
import {RecordActionsAdapter} from '@views/record/adapters/actions.adapter';
|
||||
import {ButtonGroupInterface} from '@components/button-group/button-group.model';
|
||||
import {Action} from '@app-common/actions/action.model';
|
||||
import {ScreenSize, ScreenSizeObserverService} from '@services/ui/screen-size-observer/screen-size-observer.service';
|
|
@ -1,6 +1,6 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AppManagerModule} from '../../app-manager/app-manager.module';
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {RecordSettingsMenuComponent} from './record-settings-menu.component';
|
||||
import {ImageModule} from '@components/image/image.module';
|
||||
import {ButtonModule} from '@components/button/button.module';
|
|
@ -8,12 +8,12 @@ import {ImageModule} from '@components/image/image.module';
|
|||
import {DynamicModule} from 'ng-dynamic-component';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {DropdownButtonModule} from '@components/dropdown-button/dropdown-button.module';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordComponent} from '@views/record/record.component';
|
||||
import {RecordContainerModule} from '@components/record-container/record-container.module';
|
||||
import {RecordHeaderModule} from '@components/record-header/record-header.module';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {RecordComponent} from '@views/record/components/record-view/record.component';
|
||||
import {RecordContainerModule} from '@views/record/components/record-container/record-container.module';
|
||||
import {RecordHeaderModule} from '@views/record/components/record-header/record-header.module';
|
||||
import {StatusBarModule} from '@components/status-bar/status-bar.module';
|
||||
import {recordviewStoreMock} from '@store/record-view/record-view.store.spec.mock';
|
||||
import {recordviewStoreMock} from '@views/record/store/record-view/record-view.store.spec.mock';
|
||||
import {ThemeImagesStore} from '@store/theme-images/theme-images.store';
|
||||
import {of} from 'rxjs';
|
||||
import {themeImagesMockData} from '@store/theme-images/theme-images.store.spec.mock';
|
|
@ -1,9 +1,9 @@
|
|||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
import {Observable, Subscription} from 'rxjs';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {RecordViewModel} from '@store/record-view/record-view.store.model';
|
||||
import {RecordViewModel} from '@views/record/store/record-view/record-view.store.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-record',
|
|
@ -2,11 +2,11 @@ import {NgModule} from '@angular/core';
|
|||
import {CommonModule} from '@angular/common';
|
||||
import {RecordComponent} from './record.component';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {RecordContainerModule} from '@components/record-container/record-container.module';
|
||||
import {RecordHeaderModule} from '@components/record-header/record-header.module';
|
||||
import {RecordContainerModule} from '@views/record/components/record-container/record-container.module';
|
||||
import {RecordHeaderModule} from '@views/record/components/record-header/record-header.module';
|
||||
import {StatusBarModule} from '@components/status-bar/status-bar.module';
|
||||
import {RecordSettingsMenuModule} from '@components/record-settings-menu/record-settings-menu.module';
|
||||
import {SubpanelModule} from '@base/components/subpanel/subpanel.module';
|
||||
import {RecordSettingsMenuModule} from '@views/record/components/record-settings-menu/record-settings-menu.module';
|
||||
import {SubpanelModule} from '@containers/subpanel/components/subpanel/subpanel.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RecordComponent],
|
|
@ -8,11 +8,11 @@ import {mockModuleNavigation} from '@services/navigation/module-navigation/modul
|
|||
import {localStorageServiceMock} from '@services/local-storage/local-storage.service.spec.mock';
|
||||
import {deepClone} from '@base/utils/object-utils';
|
||||
import {messageServiceMock} from '@services/message/message.service.spec.mock';
|
||||
import {RecordViewStore} from '@store/record-view/record-view.store';
|
||||
import {RecordViewStore} from '@views/record/store/record-view/record-view.store';
|
||||
import {RecordFetchGQL} from '@store/record/graphql/api.record.get';
|
||||
import {RecordSaveGQL} from '@store/record/graphql/api.record.save';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
import {subpanelFactoryMock} from '@store/subpanel/subpanel.store.spec.mock';
|
||||
import {subpanelFactoryMock} from '@containers/subpanel/store/subpanel/subpanel.store.spec.mock';
|
||||
|
||||
/* eslint-disable camelcase, @typescript-eslint/camelcase */
|
||||
export const recordViewMockData = {
|
|
@ -1,7 +1,7 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ViewStore} from '@store/view/view.store';
|
||||
import {MetadataStore, RecordViewMetadata} from '@store/metadata/metadata.store.service';
|
||||
import {BehaviorSubject, combineLatest, Observable, of} from 'rxjs';
|
||||
import {BehaviorSubject, combineLatest, Observable, of, Subscription} from 'rxjs';
|
||||
import {StateStore} from '@store/state';
|
||||
import {deepClone} from '@base/utils/object-utils';
|
||||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
|
@ -13,13 +13,17 @@ import {MessageService} from '@services/message/message.service';
|
|||
import {catchError, distinctUntilChanged, finalize, map, tap} from 'rxjs/operators';
|
||||
import {RecordFetchGQL} from '@store/record/graphql/api.record.get';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
import {ViewMode} from '@base/app-common/views/view.model';
|
||||
import {RecordViewData, RecordViewModel, RecordViewState} from '@store/record-view/record-view.store.model';
|
||||
import {ViewContext, ViewMode} from '@app-common/views/view.model';
|
||||
import {
|
||||
RecordViewData,
|
||||
RecordViewModel,
|
||||
RecordViewState
|
||||
} from '@views/record/store/record-view/record-view.store.model';
|
||||
import {RecordManager} from '@store/record/record.manager';
|
||||
import {ViewFieldDefinition} from '@app-common/metadata/metadata.model';
|
||||
import {RecordSaveGQL} from '@store/record/graphql/api.record.save';
|
||||
import {SubpanelStoreMap} from '@store/subpanel/subpanel.store';
|
||||
import {SubpanelStoreFactory} from '@store/subpanel/subpanel.store.factory';
|
||||
import {SubpanelStoreMap} from '@containers/subpanel/store/subpanel/subpanel.store';
|
||||
import {SubpanelStoreFactory} from '@containers/subpanel/store/subpanel/subpanel.store.factory';
|
||||
import {SubPanelMeta} from '@app-common/metadata/subpanel.metadata.model';
|
||||
|
||||
const initialState: RecordViewState = {
|
||||
|
@ -60,6 +64,7 @@ export class RecordViewStore extends ViewStore implements StateStore {
|
|||
protected state$ = this.store.asObservable();
|
||||
protected subpanels: SubpanelStoreMap;
|
||||
protected subpanelsState: BehaviorSubject<SubpanelStoreMap>;
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
protected recordFetchGQL: RecordFetchGQL,
|
||||
|
@ -132,6 +137,21 @@ export class RecordViewStore extends ViewStore implements StateStore {
|
|||
});
|
||||
}
|
||||
|
||||
getModuleName(): string {
|
||||
return this.internalState.module;
|
||||
}
|
||||
|
||||
getRecordId(): string {
|
||||
return this.internalState.recordID;
|
||||
}
|
||||
|
||||
getViewContext(): ViewContext {
|
||||
return {
|
||||
module: this.getModuleName(),
|
||||
id: this.getRecordId(),
|
||||
};
|
||||
}
|
||||
|
||||
getSubpanels(): SubpanelStoreMap {
|
||||
return this.subpanels;
|
||||
}
|
102
core/legacy/Data/PresetDataHandlers/SubpanelDataQueryHandler.php
Normal file
102
core/legacy/Data/PresetDataHandlers/SubpanelDataQueryHandler.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace SuiteCRM\Core\Legacy\Data\PresetDataHandlers;
|
||||
|
||||
use App\Service\ModuleNameMapperInterface;
|
||||
use BeanFactory;
|
||||
use SubpanelCustomQueryPort;
|
||||
use SuiteCRM\Core\Legacy\LegacyHandler;
|
||||
use SuiteCRM\Core\Legacy\LegacyScopeState;
|
||||
|
||||
class SubpanelDataQueryHandler extends LegacyHandler
|
||||
{
|
||||
public const HANDLER_KEY = 'subpanel-custom-query-handlers';
|
||||
|
||||
/**
|
||||
* @var ModuleNameMapperInterface
|
||||
*/
|
||||
private $moduleNameMapper;
|
||||
|
||||
/**
|
||||
* @var SubpanelCustomQueryPort
|
||||
*/
|
||||
private $queryHandler;
|
||||
|
||||
/**
|
||||
* ListDataHandler constructor.
|
||||
* @param string $projectDir
|
||||
* @param string $legacyDir
|
||||
* @param string $legacySessionName
|
||||
* @param string $defaultSessionName
|
||||
* @param LegacyScopeState $legacyScopeState
|
||||
* @param ModuleNameMapperInterface $moduleNameMapper
|
||||
*/
|
||||
public function __construct(
|
||||
string $projectDir,
|
||||
string $legacyDir,
|
||||
string $legacySessionName,
|
||||
string $defaultSessionName,
|
||||
LegacyScopeState $legacyScopeState,
|
||||
ModuleNameMapperInterface $moduleNameMapper
|
||||
) {
|
||||
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState);
|
||||
$this->moduleNameMapper = $moduleNameMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getHandlerKey(): string
|
||||
{
|
||||
return self::HANDLER_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return 'subpanel';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getQueries(string $parentModule, string $parentId, string $subpanel): array
|
||||
{
|
||||
$this->initQueryHandler();
|
||||
|
||||
if ($parentModule) {
|
||||
$parentModule = $this->moduleNameMapper->toLegacy($parentModule);
|
||||
}
|
||||
|
||||
$this->initController($parentModule);
|
||||
|
||||
$parentBean = BeanFactory::getBean($parentModule, $parentId);
|
||||
|
||||
return $this->queryHandler->getQueries($parentBean, $subpanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function runQuery(string $query): array
|
||||
{
|
||||
$this->initQueryHandler();
|
||||
|
||||
return $this->queryHandler->runQuery($query);
|
||||
}
|
||||
|
||||
protected function initQueryHandler(): void
|
||||
{
|
||||
|
||||
if ($this->queryHandler !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* @noinspection PhpIncludeInspection */
|
||||
require_once 'include/portability/Subpanels/SubpanelCustomQueryPort.php';
|
||||
|
||||
$this->queryHandler = new SubpanelCustomQueryPort();
|
||||
}
|
||||
}
|
79
core/legacy/Statistics/StatisticsHandlingTrait.php
Normal file
79
core/legacy/Statistics/StatisticsHandlingTrait.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace SuiteCRM\Core\Legacy\Statistics;
|
||||
|
||||
use App\Entity\Statistic;
|
||||
|
||||
trait StatisticsHandlingTrait
|
||||
{
|
||||
|
||||
/**
|
||||
* Build empty response statistic
|
||||
* @param string $key
|
||||
* @return Statistic
|
||||
*/
|
||||
protected function getEmptyResponse(string $key): Statistic
|
||||
{
|
||||
$statistic = new Statistic();
|
||||
$statistic->setId($key);
|
||||
$statistic->setData(['value' => '-']);
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'varchar',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build currency statistic result
|
||||
* @param array $result
|
||||
* @return Statistic
|
||||
*/
|
||||
protected function buildCurrencyResult(array $result): Statistic
|
||||
{
|
||||
$value = $result['value'] ?? 0;
|
||||
|
||||
if (empty($value)) {
|
||||
$result = ['value' => 0];
|
||||
}
|
||||
|
||||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData($result);
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'currency',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $parts
|
||||
* @return string
|
||||
*/
|
||||
protected function joinQueryParts(array $parts): string
|
||||
{
|
||||
$queryParts = [];
|
||||
$queryParts[] = $parts['select'] ?? '';
|
||||
$queryParts[] = $parts['from'] ?? '';
|
||||
$queryParts[] = $parts['where'] ?? '';
|
||||
$queryParts[] = $parts['group_by'] ?? '';
|
||||
|
||||
return implode(' ', $queryParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
* @return array
|
||||
*/
|
||||
protected function extractContext(array $query): array
|
||||
{
|
||||
$module = $query['context']['module'] ?? '';
|
||||
$id = $query['context']['id'] ?? '';
|
||||
|
||||
return array($module, $id);
|
||||
}
|
||||
|
||||
}
|
|
@ -26,10 +26,14 @@ class SubpanelAccountsCount implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'int',
|
||||
'value' => '3'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'int',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelActivitiesNextDate implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'date',
|
||||
'value' => '2020-12-05'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'date',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelCampaignsLastReceived implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'date',
|
||||
'value' => '2020-08-28'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'date',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelCasesCount implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'int',
|
||||
'value' => '5'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'int',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelContactsCount implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'int',
|
||||
'value' => '10'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'int',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,11 @@ class SubpanelContractsRenewalDate implements StatisticsProviderInterface
|
|||
'value' => '2021-02-15'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'date',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelDefault implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'int',
|
||||
'value' => '0'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'int',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelDocumentsCount implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'int',
|
||||
'value' => '11'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'int',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelEmpty implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'varchar',
|
||||
'value' => '-'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'varchar',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelEventsLastDate implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'date',
|
||||
'value' => '2020-09-10'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'date',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelHistoryLastDate implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'date',
|
||||
'value' => '2020-09-23'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'date',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelInvoicesTotal implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'currency',
|
||||
'value' => '50000'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'currency',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelLeadsTotal implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'int',
|
||||
'value' => '4'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'int',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ namespace SuiteCRM\Core\Legacy\Statistics;
|
|||
|
||||
use App\Entity\Statistic;
|
||||
use App\Service\StatisticsProviderInterface;
|
||||
use SuiteCRM\Core\Legacy\Data\PresetDataHandlers\SubpanelDataQueryHandler;
|
||||
|
||||
class SubpanelOpportunitiesTotal implements StatisticsProviderInterface
|
||||
class SubpanelOpportunitiesTotal extends SubpanelDataQueryHandler implements StatisticsProviderInterface
|
||||
{
|
||||
use StatisticsHandlingTrait;
|
||||
|
||||
public const KEY = 'opportunities';
|
||||
|
||||
/**
|
||||
|
@ -20,15 +23,32 @@ class SubpanelOpportunitiesTotal implements StatisticsProviderInterface
|
|||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData(array $param): Statistic
|
||||
public function getData(array $query): Statistic
|
||||
{
|
||||
|
||||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'currency',
|
||||
'value' => '20000'
|
||||
]);
|
||||
$subpanel = 'opportunities';
|
||||
[$module, $id] = $this->extractContext($query);
|
||||
|
||||
if (empty($module) || empty($id)) {
|
||||
return $this->getEmptyResponse(self::KEY);
|
||||
}
|
||||
|
||||
$this->init();
|
||||
$this->startLegacyApp();
|
||||
|
||||
$queries = $this->getQueries($module, $id, $subpanel);
|
||||
|
||||
$parts = $queries[0];
|
||||
$parts['select'] = 'SELECT SUM(opportunities.amount) as value ';
|
||||
$parts['where'] .= ' AND opportunities.amount is not null ';
|
||||
|
||||
$dbQuery = $this->joinQueryParts($parts);
|
||||
|
||||
$result = $this->runQuery($dbQuery);
|
||||
|
||||
$statistic = $this->buildCurrencyResult($result);
|
||||
|
||||
$this->close();
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
|
|
|
@ -26,10 +26,14 @@ class SubpanelQuotesTotal implements StatisticsProviderInterface
|
|||
$statistic = new Statistic();
|
||||
$statistic->setId(self::KEY);
|
||||
$statistic->setData([
|
||||
'type' => 'currency',
|
||||
'value' => '25000'
|
||||
]);
|
||||
|
||||
$statistic->setMetadata([
|
||||
'type' => 'single-value-statistic',
|
||||
'dataType' => 'currency',
|
||||
]);
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
||||
|
|
58
core/legacy/Statistics/WonOpportunityAmountByYear.php
Normal file
58
core/legacy/Statistics/WonOpportunityAmountByYear.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace SuiteCRM\Core\Legacy\Statistics;
|
||||
|
||||
use App\Entity\Statistic;
|
||||
use App\Service\StatisticsProviderInterface;
|
||||
use SuiteCRM\Core\Legacy\Data\PresetDataHandlers\SubpanelDataQueryHandler;
|
||||
|
||||
class WonOpportunityAmountByYear extends SubpanelDataQueryHandler implements StatisticsProviderInterface
|
||||
{
|
||||
use StatisticsHandlingTrait;
|
||||
|
||||
public const KEY = 'accounts-won-opportunity-amount-by-year';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getKey(): string
|
||||
{
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData(array $query): Statistic
|
||||
{
|
||||
[$module, $id] = $this->extractContext($query);
|
||||
$subpanel = 'opportunities';
|
||||
|
||||
if (empty($module) || empty($id)) {
|
||||
return $this->getEmptyResponse(self::KEY);
|
||||
}
|
||||
|
||||
$this->init();
|
||||
$this->startLegacyApp();
|
||||
|
||||
$queries = $this->getQueries($module, $id, $subpanel);
|
||||
|
||||
$parts = $queries[0];
|
||||
$parts['select'] = 'SELECT SUM(opportunities.amount) as amount_by_year ';
|
||||
$parts['where'] .= ' AND opportunities.date_closed is not null ';
|
||||
$parts['where'] .= " AND opportunities.sales_stage = 'Closed Won' ";
|
||||
$parts['group_by'] = ' GROUP BY EXTRACT(YEAR FROM opportunities.date_closed) ';
|
||||
|
||||
$innerQuery = $this->joinQueryParts($parts);
|
||||
|
||||
$dbQuery = 'SELECT AVG(opp_data.amount_by_year) as value FROM ( ' . $innerQuery . ' ) as opp_data';
|
||||
|
||||
$result = $this->runQuery($dbQuery);
|
||||
|
||||
$statistic = $this->buildCurrencyResult($result);
|
||||
|
||||
$this->close();
|
||||
|
||||
return $statistic;
|
||||
}
|
||||
}
|
|
@ -90,6 +90,7 @@ class RecordViewDefinitionHandler extends LegacyHandler
|
|||
|
||||
$metadata = [
|
||||
'templateMeta' => [],
|
||||
'topWidget' => [],
|
||||
'actions' => [],
|
||||
'panels' => [],
|
||||
];
|
||||
|
@ -98,6 +99,7 @@ class RecordViewDefinitionHandler extends LegacyHandler
|
|||
$vardefs = $fieldDefinition->getVardef();
|
||||
|
||||
$this->addTemplateMeta($viewDefs, $metadata);
|
||||
$this->addTopWidgetConfig($viewDefs, $metadata);
|
||||
$this->addPanelDefinitions($viewDefs, $vardefs, $metadata);
|
||||
|
||||
return $metadata;
|
||||
|
@ -259,6 +261,15 @@ class RecordViewDefinitionHandler extends LegacyHandler
|
|||
$metadata['templateMeta']['tabDefs'] = $viewDefs['templateMeta']['tabDefs'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $viewDefs
|
||||
* @param array $metadata
|
||||
*/
|
||||
protected function addTopWidgetConfig(array $viewDefs, array &$metadata): void
|
||||
{
|
||||
$metadata['topWidget'] = $viewDefs['topWidget'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return array
|
||||
|
|
|
@ -60,6 +60,12 @@ class Statistic
|
|||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @ApiProperty
|
||||
* @var array|null
|
||||
*/
|
||||
protected $metadata;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
|
@ -135,4 +141,23 @@ class Statistic
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getMetadata(): ?array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $metadata
|
||||
* @return Statistic
|
||||
*/
|
||||
public function setMetadata(?array $metadata): Statistic
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ interface StatisticsProviderInterface
|
|||
|
||||
/**
|
||||
* Get statistics data
|
||||
* @param array $param
|
||||
* @param array $query
|
||||
* @return Statistic
|
||||
*/
|
||||
public function getData(array $param): Statistic;
|
||||
public function getData(array $query): Statistic;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue