FE-142: update to Angular 2 (#58)

Angular 2 migration
This commit is contained in:
Ildar Galeev 2016-12-22 18:09:40 +03:00 committed by GitHub
parent 987895efd6
commit 3ee25fbe2e
133 changed files with 3791 additions and 24 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea/
/node_modules/
/dist/
Dockerfile

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "build_utils"]
path = build_utils
url = git@github.com:rbkmoney/build_utils.git
branch = master

24
Dockerfile.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
cat <<EOF
FROM $BASE_IMAGE
MAINTAINER Ildar Galeev <i.galeev@rbkmoney.com>
COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/vhosts.d/koffing.conf
COPY containerpilot.json /etc/containerpilot.json
CMD /bin/containerpilot -config file:///etc/containerpilot.json /usr/sbin/nginx -g "daemon off;"
LABEL base_image_tag=$BASE_IMAGE_TAG
LABEL build_image_tag=$BUILD_IMAGE_TAG
# A bit of magic to get a proper branch name
# even when the HEAD is detached (Hey Jenkins!
# BRANCH_NAME is available in Jenkins env).
LABEL branch=$( \
if [ "HEAD" != $(git rev-parse --abbrev-ref HEAD) ]; then \
echo $(git rev-parse --abbrev-ref HEAD); \
elif [ -n "$BRANCH_NAME" ]; then \
echo $BRANCH_NAME; \
else \
echo $(git name-rev --name-only HEAD); \
fi)
LABEL commit=$(git rev-parse HEAD)
LABEL commit_number=$(git rev-list --count HEAD)
EOF

39
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,39 @@
#!groovy
build('koffing', 'docker-host') {
checkoutRepo()
loadBuildUtils()
def pipeDefault
runStage('load pipeline') {
env.JENKINS_LIB = "build_utils/jenkins_lib"
pipeDefault = load("${env.JENKINS_LIB}/pipeDefault.groovy")
}
pipeDefault() {
//ToDo: npm stuff should be in a cache, when caching is implemented!
runStage('init') {
withGithubSshCredentials {
sh 'make wc_init'
}
}
runStage('build') {
sh 'make wc_build'
}
runStage('build image') {
sh 'make build_image'
}
try {
if (env.BRANCH_NAME == 'master') {
runStage('push image') {
sh 'make push_image'
}
}
} finally {
runStage('rm local image') {
sh 'make rm_local_image'
}
}
}
}

42
Makefile Normal file
View File

@ -0,0 +1,42 @@
SUBMODULES = build_utils
SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES))
UTILS_PATH := build_utils
TEMPLATES_PATH := .
# Name of the service
SERVICE_NAME := koffing
# Service image default tag
SERVICE_IMAGE_TAG ?= $(shell git rev-parse HEAD)
# The tag for service image to be pushed with
SERVICE_IMAGE_PUSH_TAG ?= $(SERVICE_IMAGE_TAG)
# Base image for the service
BASE_IMAGE_NAME := service-fe
BASE_IMAGE_TAG := a58a828755e9d342ecbd7071e7dc224ffe546378
BUILD_IMAGE_TAG := 80c38dc638c0879687f6661f4e16e8de9fc0d2c6
CALL_W_CONTAINER := init build clean submodules
.PHONY: $(CALL_W_CONTAINER)
all: build
-include $(UTILS_PATH)/make_lib/utils_image.mk
-include $(UTILS_PATH)/make_lib/utils_container.mk
$(SUBTARGETS): %/.git: %
git submodule update --init $<
touch $@
submodules: $(SUBTARGETS)
init:
npm install
build:
npm run build
clean:
rm -rf dist

View File

@ -1,23 +0,0 @@
# Koffing
[![Build Status](http://ci.rbkmoney.com/buildStatus/icon?job=rbkmoney_private/koffing/master)](http://ci.rbkmoney.com/job/rbkmoney_private/view/Frontend/job/koffing/job/master/)
Личный кабинет мерчанта
## Настройка
Конфигурация приложения происходит в файле [appConfig.json](/app/appConfig.json)
Конфигурация keycloak клиента koffing происходит в файле [koffingKeycloakConfig.json](/app/koffingKeycloakConfig.json)
Конфигурация keycloak клиента tokenizer происходит в файле [tokenizationKeycloakConfig.json](/tokenization/tokenizationKeycloakConfig.json)
Для изменения конфигурации в рантайме достаточно заменить нужный json файл
Например в случае с nginx, json файлы нужно положить в `/usr/share/nginx/html`
## Установка
npm install
npm run build
## Интеграция с Keycloak
Настройка происходит с помощью файла [keycloak.json](/app/keycloak.json)

@ -1 +1 @@
Subproject commit 4858499fdd62af516a2239d51d12d82be0921857
Subproject commit b9a3a1d845b76b07bd964bbcb363a48249e2a0e7

10
config/helpers.js Normal file
View File

@ -0,0 +1,10 @@
var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

21
config/karma-test-shim.js Normal file
View File

@ -0,0 +1,21 @@
Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var appContext = require.context('../src', true, /\.spec\.ts/);
appContext.keys().forEach(appContext);
var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');
testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());

37
config/karma.conf.js Normal file
View File

@ -0,0 +1,37 @@
var webpackConfig = require('./webpack.test');
module.exports = function (config) {
var _config = {
basePath: '',
frameworks: ['jasmine'],
files: [
{pattern: './config/karma-test-shim.js', watched: false}
],
preprocessors: {
'./config/karma-test-shim.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-only'
},
webpackServer: {
noInfo: true
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['PhantomJS'],
singleRun: true
};
config.set(_config);
};

View File

@ -0,0 +1,3 @@
{
"capiUrl": "http://localhost:9000/v1"
}

View File

@ -0,0 +1,8 @@
{
"realm" : "external",
"realm-public-key" : "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2KZPnAJs0SS6/w39mDTrWYRjM86IFteU/dPGpGQdOPwNe85Ep2leVN3/FBKVUMHsFTtFkVsg/VcCEfYBj22B0mZ8zV2hQUCNq1NV2b2LnYYrDmThmFOOTnpbBhEOMS8Wrzj3Yk7mcDtKlMzoBNIQ/Z54ffymkyiKX8XOw45K9Cx1Bp/SVjOnJlm0Qu/+zE40/XVpzgjbaqSc9+8B3tur2E03EVemOa6EFhu7ocKsbSR7/fG1nYOGKjACS1Z+VYQTMcRqxZlLw7kv3fxUaMDcK5p/16YWpWggflVy5w26IIJeDcsXG+X3LV6f6dyo9ZfOEvGqQZmPCohaSOgi+IRwAQIDAQAB",
"auth-server-url" : "http://localhost:8080/auth/",
"ssl-required" : "external",
"resource" : "koffing",
"public-client" : true
}

View File

@ -0,0 +1,5 @@
{
"realm": "external",
"auth-server-url": "http://localhost:8080/auth/",
"resource": "tokenizer"
}

89
config/webpack.common.js Normal file
View File

@ -0,0 +1,89 @@
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var helpers = require('./helpers');
module.exports = {
entry: {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'vendorjs': [
'./node_modules/jquery/dist/jquery.js',
'./node_modules/bootstrap/dist/js/bootstrap.js',
'./node_modules/gentelella/build/js/custom.js',
'./node_modules/keycloak-js/dist/keycloak.js'
],
'app': './src/main.ts'
},
resolve: {
root: __dirname + '/node_modules',
alias: {
'Keycloak': 'keycloak-js/dist/keycloak.js',
'jquery': 'jquery/dist/jquery',
'koffing': __dirname + '/../src/app'
},
extensions: ['', '.ts', '.js']
},
module: {
preLoaders: [
{
test: /\.ts$/,
loader: 'tslint-loader'
}
],
loaders: [
{
test: /(jquery.js$)|(keycloak.js$)/,
loader: 'script-loader'
},
{
test: /\.ts$/,
loaders: ['awesome-typescript-loader', 'angular2-template-loader']
},
{
test: /\.html$/,
loader: 'html'
},
{
test: /\.pug$/,
include: /\.pug/,
loader: 'pug-html-loader'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file?name=assets/[name].[hash].[ext]'
},
{
test: /\.css$/,
exclude: helpers.root('src', 'app'),
loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
},
{
test: /\.less$/,
exclude: helpers.root('src', 'app'),
loader: ExtractTextPlugin.extract('style', ['css?sourceMap', 'less'])
},
{
test: /\.less$/,
include: helpers.root('src', 'app'),
loader: 'raw!less'
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills', 'vendorjs']
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/index.pug'
}),
new CopyWebpackPlugin([{
from: 'config/runtime'
}])
]
};

24
config/webpack.dev.js Normal file
View File

@ -0,0 +1,24 @@
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
module.exports = webpackMerge(commonConfig, {
devtool: 'source-map',
output: {
path: helpers.root('dist'),
publicPath: '/',
filename: '[name].js',
chunkFilename: '[id].chunk.js'
},
plugins: [
new ExtractTextPlugin('[name].css')
],
devServer: {
historyApiFallback: true,
stats: 'minimal'
}
});

45
config/webpack.prod.js Normal file
View File

@ -0,0 +1,45 @@
var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = webpackMerge(commonConfig, {
devtool: 'source-map',
output: {
path: helpers.root('dist'),
publicPath: '/',
filename: '[name].[hash].js',
chunkFilename: '[id].[hash].chunk.js'
},
htmlLoader: {
minimize: false
},
plugins: [
new webpack.NoErrorsPlugin(),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin({
mangle: {
keep_fnames: true
},
compress: {
warnings: false
}
}),
new ExtractTextPlugin('[name].[hash].css'),
new webpack.DefinePlugin({
'process.env': {
'ENV': JSON.stringify(ENV)
}
})
],
devServer: {
historyApiFallback: true
}
});

37
config/webpack.test.js Normal file
View File

@ -0,0 +1,37 @@
var helpers = require('./helpers');
module.exports = {
devtool: 'inline-source-map',
resolve: {
extensions: ['', '.ts', '.js']
},
module: {
loaders: [
{
test: /\.ts$/,
loaders: ['awesome-typescript-loader', 'angular2-template-loader']
},
{
test: /\.html$/,
loader: 'html'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'null'
},
{
test: /\.css$/,
exclude: helpers.root('src', 'app'),
loader: 'null'
},
{
test: /\.css$/,
include: helpers.root('src', 'app'),
loader: 'raw'
}
]
}
}

13
containerpilot.json Normal file
View File

@ -0,0 +1,13 @@
{
"consul": "{{ .CONSUL_ADDR }}",
"services": [
{
"name": "{{ .SERVICE_NAME }}",
"port": 8080,
"health": "/usr/bin/curl --silent --show-error --output /dev/null localhost:8080",
"poll": 1,
"ttl": 2,
"interfaces": ["inet6", "inet"]
}
]
}

1
karma.conf.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./config/karma.conf.js');

15
nginx.conf Normal file
View File

@ -0,0 +1,15 @@
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

70
package.json Normal file
View File

@ -0,0 +1,70 @@
{
"name": "koffing",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"start": "webpack-dev-server --config config/webpack.dev.js --inline --progress --port 8000",
"test": "karma start",
"build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
},
"licenses": [],
"dependencies": {
"@angular/common": "2.3.1",
"@angular/compiler": "2.3.1",
"@angular/core": "2.3.1",
"@angular/forms": "2.3.1",
"@angular/http": "2.3.1",
"@angular/platform-browser": "2.3.1",
"@angular/platform-browser-dynamic": "2.3.1",
"@angular/router": "3.3.1",
"bootstrap": "^3.3.7",
"chart.js": "^2.4.0",
"core-js": "^2.4.1",
"gentelella": "git@github.com:rbkmoney/gentelella.git",
"jquery": "^3.1.1",
"keycloak-js": "^2.3.0",
"lodash": "^4.16.6",
"moment": "^2.16.0",
"ng2-charts": "^1.4.1",
"primeng": "^1.0.0",
"rxjs": "5.0.0-rc.4",
"zone.js": "0.7.4"
},
"devDependencies": {
"@types/core-js": "^0.9.34",
"@types/jasmine": "^2.5.35",
"@types/lodash": "^4.14.38",
"@types/node": "^6.0.45",
"angular2-template-loader": "^0.4.0",
"awesome-typescript-loader": "^3.0.0-beta.17",
"codelyzer": "^2.0.0-beta.3",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.23.1",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"html-loader": "^0.4.3",
"html-webpack-plugin": "^2.15.0",
"jasmine-core": "^2.4.1",
"karma": "^1.2.0",
"karma-jasmine": "^1.0.2",
"karma-phantomjs-launcher": "^1.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.8.0",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"null-loader": "^0.1.1",
"phantomjs-prebuilt": "^2.1.7",
"pug-html-loader": "^1.0.9",
"raw-loader": "^0.5.1",
"rimraf": "^2.5.2",
"script-loader": "^0.7.0",
"style-loader": "^0.13.1",
"tslint": "4.0.2",
"tslint-loader": "^3.3.0",
"typescript": "^2.1.4",
"webpack": "^1.13.0",
"webpack-dev-server": "^1.14.1",
"webpack-merge": "^0.14.0"
}
}

View File

@ -0,0 +1,39 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { DashboardComponent } from './components/analytic-selection/dashboard/dashboard.component';
import { FinanceComponent } from './components/analytic-selection/finance/finance.component';
import { AnalyticSelectionComponent } from './components/analytic-selection/analytic-selection.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'analytics',
component: AnalyticSelectionComponent
},
{
path: 'analytics/:shopID',
component: AnalyticSelectionComponent
},
{
path: 'analytics/:shopID',
component: AnalyticSelectionComponent,
children: [
{
path: 'dashboard',
component: DashboardComponent
},
{
path: 'finance',
component: FinanceComponent
}
]
}
])
],
exports: [
RouterModule
]
})
export class AnalyticsRoutingModule { }

View File

@ -0,0 +1,54 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BaseChartDirective } from 'ng2-charts';
import { AnalyticsRoutingModule } from './analytics-routing.module';
import { DashboardComponent } from './components/analytic-selection/dashboard/dashboard.component';
import { FinanceComponent } from './components/analytic-selection/finance/finance.component';
import { PaginateComponent } from './components/analytic-selection/finance/paginate/paginate.component';
import { SearchFormComponent } from './components/analytic-selection/finance/search-form/search-form.component';
import { SearchResultComponent } from './components/analytic-selection/finance/search-result/search-result.component';
import { PaymentMethodComponent } from './components/analytic-selection/dashboard/payment-method/payment-method.component';
import { ConversionComponent } from './components/analytic-selection/dashboard/conversion/conversion.component';
import { RevenueComponent } from './components/analytic-selection/dashboard/revenue/revenue.component';
import { GeolocationComponent } from './components/analytic-selection/dashboard/geolocation/geolocation.component';
import { InfoPanelComponent } from './components/analytic-selection/dashboard/info-panel/info-panel.component';
import { AnalyticSelectionComponent } from './components/analytic-selection/analytic-selection.component';
import { PaginationPipe } from './components/analytic-selection/finance/paginate/pagination.pipe';
import { PaymentStatusPipe } from './components/analytic-selection/finance/search-result/payment-statuses.pipe';
import { RoubleCurrencyPipe } from './components/analytic-selection/rouble-currency.pipe';
import { CommonModule } from 'koffing/common/common.module';
import { CalendarModule } from 'primeng/components/calendar/calendar';
import { BackendModule } from 'koffing/backend/backend.module';
@NgModule({
imports: [
AnalyticsRoutingModule,
BrowserModule,
FormsModule,
CommonModule,
CalendarModule,
BackendModule
],
declarations: [
AnalyticSelectionComponent,
DashboardComponent,
FinanceComponent,
PaginateComponent,
SearchFormComponent,
SearchResultComponent,
BaseChartDirective,
PaymentMethodComponent,
ConversionComponent,
RevenueComponent,
GeolocationComponent,
InfoPanelComponent,
PaymentStatusPipe,
RoubleCurrencyPipe,
PaginationPipe
]
})
export class AnalyticsModule { }
export * from './components/analytic-selection/analytic-selection.component';

View File

@ -0,0 +1,17 @@
.row
.col-xs-12
.x_panel.tile
.x_content
form.form-horizontal
.form-group
.col-xs-12.col-sm-1
label.control-label Магазин
.col-xs-12.col-sm-6
kof-select(*ngIf="selectItems.length", [(ngModel)]="selectedShopID", [items]="selectItems", (onChange)="navigateToShop()", [modelOptions]="{standalone: true}", name='shopId')
ul.nav.nav-tabs.bar_tabs(*ngIf="selectedShopID")
li.hand-cursor([routerLink]="['dashboard']", [routerLinkActive]="['active']")
a Статистика
li.hand-cursor([routerLink]="['finance']", [routerLinkActive]="['active']")
a Финансы
.tab-content
router-outlet

View File

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import * as _ from 'lodash';
import { ShopService } from 'koffing/backend/backend.module';
import { Shop } from 'koffing/backend/backend.module';
import { SelectItem } from 'koffing/common/common.module';
@Component({
selector: 'kof-analytic-selection',
templateUrl: './analytic-selection.component.pug'
})
export class AnalyticSelectionComponent implements OnInit {
public selectedShopID: string;
public selectItems: SelectItem[] = [];
constructor(
private route: ActivatedRoute,
private router: Router,
private shopService: ShopService
) { }
public ngOnInit() {
this.shopService.getShops().then((shops: Shop[]) => {
const routeShopID = this.route.snapshot.params['shopID'];
this.selectItems = _.map(shops, (shop: Shop) => new SelectItem(shop.shopID, shop.shopDetails.name));
this.selectedShopID = routeShopID ? routeShopID : this.selectItems[0].value;
this.navigateToShop();
});
}
public navigateToShop() {
const hasChildren = this.route.children.length > 0;
const childComponent = hasChildren ? this.route.children[0].routeConfig.path : 'dashboard';
this.router.navigate(['analytics', this.selectedShopID, childComponent]);
}
}

View File

@ -0,0 +1,56 @@
import * as _ from 'lodash';
export class ChartDataConversionService {
public static toPaymentMethodChartData(paymentMethodStat: any): any {
return _.map(paymentMethodStat, (item: any) => {
return {
totalCount: item.totalCount,
paymentSystem: item.paymentSystem
};
});
}
public static toRevenueChartData(revenueStat: any): any {
return _.map(revenueStat, (item: any) => {
return {
profit: item.profit,
offset: item.offset
};
});
}
public static toGeoChartData(geoStat: any): any {
return _.map(geoStat, (item: any) => {
return {
cityName: item.cityName,
profit: item.profit
};
});
}
public static toTotalProfit(revenueStat: any): any {
return _.reduce(revenueStat, (acc: any, item: any) => acc + item.profit, 0);
}
public static toPaymentCountInfo(conversionStat: any): any {
return _.reduce(conversionStat, (acc: any, item: any) => {
return {
successfulCount: acc.successfulCount + item.successfulCount,
unfinishedCount: acc.unfinishedCount + (item.totalCount - item.successfulCount)
};
}, {
successfulCount: 0,
unfinishedCount: 0
});
}
public static toConversionChartData(conversionStat: any): any {
return _.map(conversionStat, (item: any) => {
return {
conversion: item.conversion,
offset: item.offset
};
});
}
}

View File

@ -0,0 +1,36 @@
export class CHART_OPTIONS {
public static LINE: any = {
COLORS: {
backgroundColor: 'rgba(148,159,177,0.2)',
borderColor: '#73879C',
pointBackgroundColor: '#73879C',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#79909c',
pointHoverBorderColor: '#fff',
}
};
public static DOUGHNUT: any = {
COLORS: {
backgroundColor : [
'#ddf0e1',
'#cee9f6',
'#fddadb',
'#ebdaff',
'#f5ecdd',
'#f6d4dc',
'#fdc478',
'#aec4e8',
'#c0f1f0',
'#949fb1',
'#bba8dc',
'#d4cfcd',
'#ffb6b6',
'#f2fdeb',
'#f8f8f8',
'#ccfaf6'
]
}
};
}

View File

@ -0,0 +1,11 @@
kof-loading([isLoading]="isLoading")
div
canvas(
baseChart,
[data]="data",
[labels]="labels",
[chartType]="type",
[options]="options",
[colors]="chartColors",
height="80"
)

View File

@ -0,0 +1,65 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import * as _ from 'lodash';
import * as moment from 'moment';
import { CHART_OPTIONS } from './../chart-options.const';
@Component({
selector: 'kof-conversion',
templateUrl: './conversion.component.pug'
})
export class ConversionComponent implements OnInit, OnChanges {
@Input()
public fromTime: any;
@Input()
public chartData: any;
public labels: string[];
public data: number[] | any[] = [];
public type: string = 'line';
public options: any = {
animation: false,
elements: {
line: {
tension: 0.2
}
},
scales: {
yAxes: [{
stacked: true
}],
xAxes: [{
ticks: {
fontSize: 11
}
}]
},
legend: {
display: false
}
};
public chartColors = [CHART_OPTIONS.LINE.COLORS];
private isLoading: boolean;
public ngOnInit() {
this.isLoading = true;
}
public ngOnChanges() {
if (this.chartData) {
this.isLoading = false;
this.labels = _.map(this.chartData,
(item: any) => moment(this.fromTime).add(item.offset, 's').format('DD.MM HH:mm')
);
this.data = _.chain(this.chartData)
.map(
(item: any) => _.round(item.conversion * 100, 0)
)
.chunk(this.chartData.length)
.value();
}
}
}

View File

@ -0,0 +1,12 @@
div.ui-calendar.statistics {
width: 100%;
}
p-calendar.statistics > span {
width: 100%;
}
p-calendar.statistics input.ui-inputtext {
width: 100%;
height: 34px;
}

View File

@ -0,0 +1,44 @@
.row
.col-xs-12
form.form-inline.form-label-right
.form-group
label.control-label Статистика с&nbsp;
.ui-calendar
p-calendar.ui-inputtext.statistics(name="fromTime", [(ngModel)]="fromTimeDate", dateFormat="dd.mm.yy")
.form-group
label.control-label &nbsp;&nbsp;по&nbsp;
.ui-calendar
p-calendar.ui-inputtext.statistics(name="toTime", [(ngModel)]="toTimeDate", dateFormat = "dd.mm.yy")
span &nbsp;
button.btn.btn-default(style="margin-bottom: 0; margin-right: 0; margin-left: 5px;", (click)="loadData()") Показать
.row
.col-xs-12
kof-info-panel([uniqueCount]="uniqueCount", [successfulCount]="successfulCount",
[unfinishedCount]="unfinishedCount", [profit]="profit", [account]="account", [isLoading]="isInfoPanelLoading")
.row
.col-xs-12
.x_panel.tile
.x_title
h4 Оборот
.x_content
kof-revenue([fromTime]="chartFromTime", [chartData]="revenueChartData")
.row
.col-xs-12
.x_panel.tile
.x_title
h4 Конверсия оплат
.x_content
kof-conversion([fromTime]="chartFromTime", [chartData]="conversionChartData")
.row
.col-xs-12.col-sm-6
.x_panel.tile
.x_title
h4 Способы оплаты
.x_content
kof-payment-method([chartData]="paymentMethodChartData")
.col-xs-12.col-sm-6
.x_panel.tile
.x_title
h4 Геолокация
.x_content
kof-geolocation([chartData]="geoChartData")

View File

@ -0,0 +1,214 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import * as _ from 'lodash';
import * as moment from 'moment';
import { ChartDataConversionService } from './chart-data-conversion.service';
import { AccountService } from 'koffing/backend/backend.module';
import { CustomerService } from 'koffing/backend/backend.module';
import { RequestParams } from 'koffing/backend/backend.module';
import { GeoData } from 'koffing/backend/backend.module';
import { PaymentsService } from 'koffing/backend/backend.module';
import { Conversion } from 'koffing/backend/backend.module';
@Component({
templateUrl: './dashboard.component.pug',
styleUrls: ['./dashboard.component.less'],
encapsulation: ViewEncapsulation.None
})
export class DashboardComponent implements OnInit {
public fromTime: any;
public toTime: any;
public uniqueCount: any;
public successfulCount: any;
public unfinishedCount: any;
public profit: any;
public account: any = {
general: {
ownAmount: 1
},
guarantee: {
ownAmount: 2
}
};
public chartFromTime: any;
public revenueChartData: any;
public conversionChartData: any;
public geoChartData: GeoData[] = [];
public paymentMethodChartData: any;
public isInfoPanelLoading: boolean;
private fromTimeDate: Date;
private toTimeDate: Date;
private shopID: string;
constructor(private route: ActivatedRoute,
private customer: CustomerService,
private payments: PaymentsService,
private accounts: AccountService) { }
public ngOnInit() {
this.route.parent.params.subscribe((params: Params) => {
this.shopID = params['shopID'];
this.setInitialDate();
this.loadData();
});
}
private loadRate() {
return new Promise((resolve) => {
this.customer.getRate(
this.shopID,
new RequestParams(
this.fromTime,
this.toTime
)
).then(
(rateStat: any) => {
this.uniqueCount = rateStat[0] ? rateStat[0].uniqueCount : 0;
resolve();
}
);
});
}
private loadPaymentMethod() {
this.customer.getPaymentMethod(
this.shopID,
new RequestParams(
this.fromTime,
this.toTime,
'minute',
'1',
'bank_card'
)
).then(
(paymentMethodState: any) => {
this.paymentMethodChartData = ChartDataConversionService.toPaymentMethodChartData(paymentMethodState);
}
);
}
private loadConversionStat() {
return new Promise((resolve) => {
this.payments.getConversionStat(
this.shopID,
new RequestParams(
this.fromTime,
this.toTime,
'minute',
'1'
)
).then(
(conversionStat: Conversion[]) => {
let paymentCountInfo: any;
paymentCountInfo = ChartDataConversionService.toPaymentCountInfo(conversionStat);
this.conversionChartData = ChartDataConversionService.toConversionChartData(conversionStat);
this.successfulCount = paymentCountInfo.successfulCount;
this.unfinishedCount = paymentCountInfo.unfinishedCount;
resolve();
}
);
});
}
private loadGeoChartData() {
this.payments.getGeoChartData(
this.shopID,
new RequestParams(
this.fromTime,
this.toTime,
'day',
'1'
)
).then(
(geoData: GeoData[]) => {
this.geoChartData = ChartDataConversionService.toGeoChartData(geoData);
}
);
}
private loadRevenueStat() {
return new Promise((resolve) => {
this.payments.getRevenueStat(
this.shopID,
new RequestParams(
this.fromTime,
this.toTime,
'minute',
'1'
)
).then(
(revenueStat: any) => {
this.revenueChartData = ChartDataConversionService.toRevenueChartData(revenueStat);
this.profit = ChartDataConversionService.toTotalProfit(revenueStat);
resolve();
}
);
});
}
private loadShopAccounts() {
return new Promise((resolve) => {
this.accounts.getShopAccounts(this.shopID).then(
(shopAccounts) => {
if (shopAccounts.length > 1) {
console.warn('shop accounts size > 1');
}
_.forEach(shopAccounts, item => {
this.accounts.getShopAccountDetails(
this.shopID,
item.generalID
).then(
(generalAccount: any) => {
this.account.general = generalAccount;
}
);
this.accounts.getShopAccountDetails(
this.shopID,
item.guaranteeID
).then(
(guaranteeAccount: any) => {
this.account.guarantee = guaranteeAccount;
}
);
});
resolve();
}
);
});
}
private loadData() {
this.fromTime = moment(this.fromTimeDate).format();
this.toTime = moment(this.toTimeDate).format();
this.chartFromTime = this.fromTime;
this.loadPaymentMethod();
this.loadGeoChartData();
this.isInfoPanelLoading = true;
Promise.all([
this.loadRate(),
this.loadConversionStat(),
this.loadRevenueStat(),
this.loadShopAccounts()
]).then(() => {
this.isInfoPanelLoading = false;
});
}
private setInitialDate() {
this.toTimeDate = new Date();
this.fromTimeDate = new Date();
this.fromTimeDate.setMonth( this.fromTimeDate.getMonth() - 1 );
}
}

View File

@ -0,0 +1,11 @@
kof-loading([isLoading]="isLoading")
div
canvas(
baseChart,
[data]="data",
[labels]="labels",
[chartType]="type",
[options]="options",
[colors]="chartColors",
height=180
)

View File

@ -0,0 +1,47 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import * as _ from 'lodash';
import { CHART_OPTIONS } from './../chart-options.const';
@Component({
selector: 'kof-geolocation',
templateUrl: './geolocation.component.pug'
})
export class GeolocationComponent implements OnInit, OnChanges {
@Input()
public chartData: any;
public labels: string[];
public data: number[] = [];
public type: string = 'doughnut';
public options: any = {
animation: false,
legend: {
display: true,
position: 'left'
}
};
public chartColors = [CHART_OPTIONS.DOUGHNUT.COLORS];
private isLoading: boolean;
public ngOnInit() {
this.isLoading = true;
}
public ngOnChanges() {
if (this.chartData) {
this.isLoading = false;
const grouped: any = _.groupBy(this.chartData, 'cityName');
const cities: any = _.keys(grouped);
const data: any[] = [];
_.forEach(cities, city => {
const accumulatedValue = _.reduce(grouped[city], (acc: any, item: any) => acc + item.profit, 0);
data.push(accumulatedValue / 100);
});
this.labels = cities;
this.data = data;
}
}
}

View File

@ -0,0 +1,34 @@
.row.tile_count
.col-xs-6.col-sm-4.col-md-2.tile_stats_count
span.count_top Оборот
.dashboard_stats_count
kof-loading([isLoading]="isLoading")
div {{profit | kofRoubleCurrency}}
span.count_bottom Рублей
.col-xs-6.col-sm-4.col-md-2.tile_stats_count
span.count_top Основной счет
.dashboard_stats_count
kof-loading([isLoading]="isLoading")
div {{account.general.ownAmount | kofRoubleCurrency}}
span.count_bottom Рублей
.col-xs-6.col-sm-4.col-md-2.tile_stats_count
span.count_top Гарантийный счет
.dashboard_stats_count
kof-loading([isLoading]="isLoading")
div {{account.guarantee.ownAmount | kofRoubleCurrency}}
span.count_bottom Рублей
.col-xs-6.col-sm-4.col-md-2.tile_stats_count
span.count_top Успешных платежей
.dashboard_stats_count
kof-loading([isLoading]="isLoading")
div {{successfulCount}}
.col-xs-6.col-sm-4.col-md-2.tile_stats_count
span.count_top Незавершенных платежей
.dashboard_stats_count
kof-loading([isLoading]="isLoading")
div {{unfinishedCount}}
.col-xs-6.col-sm-4.col-md-2.tile_stats_count
span.count_top Уникальных плательщиков
.dashboard_stats_count
kof-loading([isLoading]="isLoading")
div {{uniqueCount}}

View File

@ -0,0 +1,21 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'kof-info-panel',
templateUrl: './info-panel.component.pug'
})
export class InfoPanelComponent {
@Input()
public uniqueCount: any;
@Input()
public successfulCount: any;
@Input()
public unfinishedCount: any;
@Input()
public profit: any;
@Input()
public account: any;
@Input()
public isLoading: boolean;
}

View File

@ -0,0 +1,11 @@
kof-loading([isLoading]="isLoading")
div
canvas(
baseChart,
[data]="data",
[labels]="labels",
[chartType]="type",
[options]="options",
[colors]="chartColors",
height=180
)

View File

@ -0,0 +1,65 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import * as _ from 'lodash';
import { CHART_OPTIONS } from './../chart-options.const';
@Component({
selector: 'kof-payment-method',
templateUrl: './payment-method.component.pug'
})
export class PaymentMethodComponent implements OnInit, OnChanges {
@Input()
public chartData: any;
public labels: string[] | any[];
public data: number[] = [];
public type: string = 'doughnut';
public options: any = {
animation: false,
legend: {
display: true,
position: 'left'
}
};
public chartColors = [CHART_OPTIONS.DOUGHNUT.COLORS];
private isLoading: boolean;
public ngOnInit() {
this.isLoading = true;
}
public ngOnChanges() {
let grouped: any;
let paymentSystem: any;
let data: any[];
if (this.chartData) {
this.isLoading = false;
grouped = _.groupBy(this.chartData, 'paymentSystem');
paymentSystem = _.keys(grouped);
data = [];
_.forEach(paymentSystem, system => data.push(
_.chain(grouped[system])
.reduce(
(acc: any, item: any) => acc + item.totalCount, 0
)
.value()
));
this.labels = _.map(paymentSystem, system => {
let result = system;
if (system === 'visa') {
result = 'Visa';
} else if (system === 'mastercard' || system === 'master_card' ) {
result = 'Master Card';
} else if (system === 'nspkmir') {
result = 'Mir';
}
return result;
});
this.data = data;
}
}
}

View File

@ -0,0 +1,11 @@
kof-loading([isLoading]="isLoading")
div
canvas.revenueChart(
baseChart,
[data]="data",
[labels]="labels",
[chartType]="type",
[options]="options",
[colors]="chartColors",
height="80"
)

View File

@ -0,0 +1,64 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import * as _ from 'lodash';
import * as moment from 'moment';
import { CHART_OPTIONS } from './../chart-options.const';
@Component({
selector: 'kof-revenue',
templateUrl: './revenue.component.pug'
})
export class RevenueComponent implements OnInit, OnChanges {
@Input()
public fromTime: any;
@Input()
public chartData: any;
public labels: string[];
public data: number[] | any[] = [];
public type: string = 'line';
public options: any = {
elements: {
line: {
tension: 0.2
}
},
scales: {
yAxes: [{
stacked: true
}],
xAxes: [{
ticks: {
fontSize: 11
}
}]
},
legend: {
display: false
}
};
public chartColors = [CHART_OPTIONS.LINE.COLORS];
private isLoading: boolean;
public ngOnInit() {
this.isLoading = true;
}
public ngOnChanges() {
if (this.chartData) {
this.isLoading = false;
this.labels = _.map(this.chartData,
(item: any) => moment(this.fromTime).add(item.offset, 's').format('DD.MM HH:mm')
);
this.data = _.chain(this.chartData)
.map(
(item: any) => _.round(item.profit / 100, 2)
).chunk(this.chartData.length)
.value();
}
}
}

View File

@ -0,0 +1,14 @@
.row
.col-xs-12
.x_panel
.x_title
h4 Поиск
.x_content
kof-search-form([searchParams]="searchParams", (onSearch)="search()")
.row
.col-xs-12
.x_panel.tile
.x_content
kof-loading([isLoading]="isLoading")
kof-search-result([invoices]="invoices")
kof-paginate([size]="totalCount", [limit]="searchParams.limit", [offset]="searchParams.offset", [pagesOnScreen]="10", (onChange)="onChangePage($event)")

View File

@ -0,0 +1,53 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import * as moment from 'moment';
import { Invoice } from 'koffing/backend/backend.module';
import { InvoiceService } from 'koffing/backend/backend.module';
@Component({
templateUrl: 'finance.component.pug'
})
export class FinanceComponent implements OnInit {
public invoices: Invoice[];
public totalCount: number;
public isLoading: boolean = false;
public searchParams: any;
public shopID: string;
constructor(
private route: ActivatedRoute,
private invoiceService: InvoiceService
) { }
public ngOnInit() {
// todo: описать class SearchParams и создать как экземпляр класса
this.searchParams = {
fromTime: moment().subtract(1, 'M').utc().format(),
toTime: moment().utc().format(),
limit: 20,
offset: 0,
invoiceID: null
};
this.route.parent.params.subscribe((params: Params) => {
this.shopID = params['shopID'];
this.search();
});
}
public search(offset?: number) {
this.searchParams.offset = offset ? offset : 0;
this.isLoading = true;
this.invoiceService.getInvoices(this.shopID, this.searchParams).then(response => {
this.isLoading = false;
this.invoices = response.invoices;
this.totalCount = response.totalCount;
});
}
public onChangePage(offset: number) {
this.search(offset);
}
}

View File

@ -0,0 +1,33 @@
import * as _ from 'lodash';
export class CalculatePagesService {
public static initPages(itemsSize: number, itemsLimit: number, itemsOffset: number): any[] {
const size = this.initParam(itemsSize);
const limit = this.initParam(itemsLimit);
const offset = this.initParam(itemsOffset);
let pages: any[] = [];
for (let page = 1; page <= this.calcPages(size, limit); page++) {
const calcOffset = (page - 1) * limit;
pages.push({
active: calcOffset === offset,
label: page,
offset: calcOffset
});
}
return pages;
}
private static initParam(param: number): number {
const result = _.toNumber(param);
return _.isNaN(result) ? 0 : result;
}
private static calcPages(size: number, limit: number): number {
if (limit === 0 || size < limit) {
return 0;
}
const res = size / limit;
return (size % limit > 0) ? res + 1 : res;
}
}

View File

@ -0,0 +1,8 @@
.dataTables_paginate.paging_simple_numbers(*ngIf="size > limit")
ul.pagination.custom_pagination
li.paginate_button.previous((click)="back($event)")
a.fa.fa-chevron-left(href="")
li.paginate_button(*ngFor="let page of pages | kofPagination : pagesOnScreen : pageOffset()", (click)="select($event, page)", [ngClass]="{'active': page.active}")
a(href="") {{page.label}}
li.paginate_button.next((click)="forward($event)")
a.fa.fa-chevron-right(href="")

View File

@ -0,0 +1,70 @@
import { Component, Input, Output, EventEmitter, OnChanges } from '@angular/core';
import * as _ from 'lodash';
import { CalculatePagesService } from './calculate-pages.service';
@Component({
selector: 'kof-paginate',
templateUrl: 'paginate.component.pug'
})
export class PaginateComponent implements OnChanges {
@Input()
public size: number;
@Input()
public limit: number;
@Input()
public offset: number;
@Input()
public pagesOnScreen: number;
@Output()
public onChange: EventEmitter<any> = new EventEmitter<any>();
private pages: any[];
public select(event: MouseEvent, page: any) {
event.preventDefault();
return this.activatePage(page);
}
public forward(event: MouseEvent) {
event.preventDefault();
const index = _.indexOf(this.pages, this.getActive()) + 1;
if (this.pages.length > index) {
return this.activatePage(this.pages[index]);
}
}
public back(event: MouseEvent) {
event.preventDefault();
const index = _.indexOf(this.pages, this.getActive()) - 1;
if (index >= 0) {
return this.activatePage(this.pages[index]);
}
}
public getActive() {
return _.find(this.pages, page => page.active);
}
public activatePage(page: any) {
this.getActive().active = false;
page.active = true;
this.onChange.emit(page.offset);
return page;
}
public pageOffset() {
const currentPageIndex = (this.offset / this.limit);
const offset = _.round(this.pagesOnScreen / 2);
return currentPageIndex > offset ? currentPageIndex - offset : 0;
}
public ngOnChanges() {
this.pages = CalculatePagesService.initPages(this.size, this.limit, this.offset);
}
}

View File

@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
import * as _ from 'lodash';
@Pipe({
name: 'kofPagination'
})
export class PaginationPipe implements PipeTransform {
public transform(input: any[], total: number, offset: number): any[] {
return _.filter(input, (value, index) => {
if (offset <= index && index < total + offset) {
return value;
}
});
}
}

View File

@ -0,0 +1,13 @@
div.ui-calendar.search-form {
width: 100%;
}
p-calendar.search-form > span {
width: 100%;
}
p-calendar.search-form input.ui-inputtext {
width: 100%;
height: 34px;
margin: 0;
}

View File

@ -0,0 +1,29 @@
form.form-horizontal.form-search-invoices
.row
.col-xs-12.col-sm-6
.form-group
.col-xs-12.col-sm-4
label.control-label Дата с
.col-xs-12.col-sm-6
.ui-calendar.search-form
p-calendar.ui-inputtext.search-form(name="searchFromTime", [(ngModel)]="searchFromTime", dateFormat = "dd.mm.yy", required)
.form-group
.col-xs-12.col-sm-4
label.control-label Дата по
.col-xs-12.col-sm-6
.ui-calendar.search-form
p-calendar.ui-inputtext.search-form(name="searchToTime", [(ngModel)]="searchToTime", dateFormat = "dd.mm.yy", required)
.col-xs-12.col-sm-6
.form-group
.col-xs-12.col-sm-4
label.control-label Статус платежа
.col-xs-12.col-sm-6
kof-select([(ngModel)]="searchParams.status", [items]="statuses", [placeholder]="'Любой статус'", [modelOptions]="{standalone: true}", name='status')
.form-group
.col-xs-12.col-sm-4
label.control-label Номер
.col-xs-12.col-sm-6
input.form-control(type="text", [(ngModel)]="searchParams.invoiceID", name="invoiceID")
.form-group
.col-xs-12.col-sm-10
button.btn.btn-primary.pull-right.btn-search-invoices((click)="search()") Найти

View File

@ -0,0 +1,56 @@
import { Component, Input, Output, OnInit, EventEmitter, ViewEncapsulation } from '@angular/core';
import * as _ from 'lodash';
import * as moment from 'moment';
import { PAYMENT_STATUSES } from '../search-result/payment-statuses.const';
import { SelectItem } from 'koffing/common/common.module';
@Component({
selector: 'kof-search-form',
templateUrl: 'search-form.component.pug',
styleUrls: ['search-form.component.less'],
encapsulation: ViewEncapsulation.None
})
export class SearchFormComponent implements OnInit {
@Input()
public searchParams: any;
@Output()
public onSearch: EventEmitter<any> = new EventEmitter<any>();
private statuses: any;
private fromTime: Date;
private toTime: Date;
get searchFromTime() {
return this.fromTime;
}
set searchFromTime(value: Date) {
this.searchParams.fromTime = moment(value).utc().format();
this.fromTime = value;
}
get searchToTime() {
return this.toTime;
}
set searchToTime(value: Date) {
this.searchParams.toTime = moment(value).utc().format();
this.toTime = value;
}
public ngOnInit() {
this.statuses = _.map(PAYMENT_STATUSES.GET, (name: string, key: string) => new SelectItem(key, name));
this.fromTime = new Date(this.searchParams.fromTime);
this.toTime = new Date(this.searchParams.toTime);
}
public search() {
this.onSearch.emit();
}
}

View File

@ -0,0 +1,11 @@
export class PAYMENT_STATUSES {
public static get GET(): any {
return {
unpaid: 'Неоплаченный',
cancelled: 'Отмененный',
paid: 'Оплаченный',
refunded: 'Возвращенный',
fulfilled: 'Выполненный'
};
}
}

View File

@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
import { PAYMENT_STATUSES } from './payment-statuses.const';
@Pipe({
name: 'kofPaymentStatus'
})
export class PaymentStatusPipe implements PipeTransform {
public transform(input: string): string {
const status = PAYMENT_STATUSES.GET[input];
return status ? status : input;
}
}

View File

@ -0,0 +1,17 @@
table.table.table-hover.table-striped
thead
tr
th Номер
th Сумма
th.hidden-xs Валюта
th Дата
th Статус
th.hidden-xs Описание
tbody
tr(*ngFor="let invoice of invoices")
td {{invoice.id}}
td {{invoice.amount | kofRoubleCurrency}}
td.hidden-xs {{invoice.currency}}
td {{invoice.dueDate | date: "dd.MM.yyyy HH:mm:ss"}}
td {{invoice.status | kofPaymentStatus}}
td.hidden-xs {{invoice.description}}

View File

@ -0,0 +1,11 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'kof-search-result',
templateUrl: 'search-result.component.pug'
})
export class SearchResultComponent {
@Input()
public invoices: any;
}

View File

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import * as _ from 'lodash';
@Pipe({
name: 'kofRoubleCurrency'
})
export class RoubleCurrencyPipe implements PipeTransform {
public transform(input: number): number {
let value = _.round(input / 100, 2);
return value ? format(value, 2, 3, ' ', '.') : input;
}
}
function format(value: any, decimalLength: number, wholeLength: number, delimiter: string, decimalDelimiter: string) {
let exp = '\\d(?=(\\d{' + (wholeLength || 3) + '})+' + (decimalLength > 0 ? '\\D' : '$') + ')';
// tslint:disable-next-line
let num = value.toFixed(Math.max(0, ~~decimalLength));
return (decimalDelimiter ? num.replace('.', decimalDelimiter) : num)
.replace(new RegExp(exp, 'g'), '$&' + (delimiter || ','));
}

34
src/app/app.module.ts Normal file
View File

@ -0,0 +1,34 @@
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { Http, XHRBackend, RequestOptions, HttpModule } from '@angular/http';
import { RootModule } from './root/root.module';
import { AuthHttpInterceptor } from './auth/interceptors/auth-http.interceptor';
import { ConfigService } from './backend/services/config.service';
import { ContainerComponent } from './root/components/container/container.component';
import { HttpErrorBroadcaster } from 'koffing/broadcaster/services/http-error-broadcaster.service';
@NgModule({
imports: [
HttpModule,
RootModule
],
providers: [
{
provide: Http,
useFactory: (
backend: XHRBackend,
defaultOptions: RequestOptions,
httpErrorBroadcaster: HttpErrorBroadcaster
) => new AuthHttpInterceptor(backend, defaultOptions, httpErrorBroadcaster),
deps: [XHRBackend, RequestOptions, HttpErrorBroadcaster]
},
{
provide: APP_INITIALIZER,
useFactory: (config: ConfigService) => () => config.load(),
deps: [ConfigService],
multi: true
}
],
bootstrap: [ContainerComponent]
})
export class AppModule { }

View File

@ -0,0 +1,4 @@
export * from './classes/AuthInfo.class';
export * from './interceptors/auth-http.interceptor';
export * from './services/auth.service';
export * from './services/offline-token.service';

View File

@ -0,0 +1,7 @@
export class AuthInfo {
public profileName: string = '';
public token: string = '';
}

View File

@ -0,0 +1,113 @@
import { Http, ConnectionBackend, RequestOptions, Request, RequestOptionsArgs, Response, Headers } from '@angular/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { HttpErrorBroadcaster } from 'koffing/broadcaster/services/http-error-broadcaster.service';
export class AuthHttpInterceptor extends Http {
constructor(
connectionBackend: ConnectionBackend,
defaultOptions: RequestOptions,
private httpErrorBroadcaster: HttpErrorBroadcaster
) {
super(connectionBackend, defaultOptions);
}
public request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.request, url, options);
}
public get(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.get, url, options);
}
public post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.post, url, options, body);
}
public put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.put, url, options, body);
}
public delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.delete, url, options);
}
public patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.patch, url, options, body);
}
public head(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.head, url, options);
}
public options(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this.configureRequest(super.options, url, options);
}
private configureRequest(
f: Function,
url: string | Request,
options: RequestOptionsArgs,
body?: any
) {
const tokenPromise: Promise<string> = this.getToken();
const tokenObservable: Observable<string> = Observable.fromPromise(tokenPromise);
const tokenUpdateObservable: Observable<any> = Observable.create((observer: any) => {
if (!options) {
const headers = new Headers();
options = new RequestOptions({headers});
}
this.setHeaders(options);
observer.next();
observer.complete();
});
const requestObservable: Observable<Response> = Observable.create((observer: any) => {
let result: any;
if (body) {
result = f.apply(this, [url, body, options]);
} else {
result = f.apply(this, [url, options]);
}
result.subscribe((response: any) => {
observer.next(response);
observer.complete();
}, (error: any) => {
this.httpErrorBroadcaster.fire(error.status);
});
});
return <Observable<Response>> Observable
.merge(tokenObservable, tokenUpdateObservable, requestObservable, 1)
.filter((response) => response instanceof Response);
}
private getToken(): Promise<string> {
const minValidity = 5;
return new Promise<string>((resolve, reject) => {
const token: string = AuthService.getAccountInfo().token;
if (token) {
AuthService.updateToken(minValidity)
.success(() => resolve(token))
.error(() => reject('Failed to refresh token'));
}
});
}
private setHeaders(options: RequestOptionsArgs) {
if (!options.headers) {
options.headers = new Headers();
}
options.headers.set('Authorization', 'Bearer ' + AuthService.getAccountInfo().token);
options.headers.set('X-Request-ID', this.guid());
options.headers.set('Accept', 'application/json');
options.headers.set('Content-Type', 'application/json; charset=UTF-8');
}
private guid(): string {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
}

View File

@ -0,0 +1,61 @@
import { OfflineTokenService } from './offline-token.service';
import { AuthInfo } from '../classes/AuthInfo.class';
declare const Keycloak: any;
export class AuthService {
public static koffingInstance: any;
public static init(): Promise<any> {
return OfflineTokenService.isTokenizationFlowPath() ? this.initTokenization() : this.initKoffing();
}
public static logout() {
this.koffingInstance.logout();
OfflineTokenService.clearToken();
}
public static updateToken(minValidity: number) {
return this.koffingInstance.updateToken(minValidity);
}
public static getAccountInfo(): AuthInfo {
const result = new AuthInfo();
if (this.koffingInstance) {
result.profileName = this.koffingInstance.tokenParsed.name;
result.token = this.koffingInstance.token;
}
return result;
}
public static getOfflineToken(): string {
return OfflineTokenService.getToken(this.koffingInstance.sessionId);
}
private static initKoffing(): Promise<any> {
const keycloakAuth: any = new Keycloak('koffingKeycloakConfig.json');
return new Promise((resolve, reject) => {
keycloakAuth.init({onLoad: 'login-required'}).success(() => {
this.koffingInstance = keycloakAuth;
resolve();
}).error(() => reject());
});
}
private static initTokenization(): Promise<any> {
const keycloakAuth: any = new Keycloak('tokenizationKeycloakConfig.json');
return new Promise((resolve, reject) => {
keycloakAuth.init().success((authenticated: any) => {
if (!authenticated) {
keycloakAuth.login({
scope: 'offline_access'
});
} else {
OfflineTokenService.setToken(keycloakAuth.refreshToken, keycloakAuth.sessionId, '/tokenization');
resolve();
}
}).error(() => reject());
});
}
}

View File

@ -0,0 +1,38 @@
export class OfflineTokenService {
public static authFlowPath: string = '/getOfflineToken';
public static storageKey: string = 'offlineToken';
public static sessionKey: string = 'sessionId';
public static isTokenizationFlowPath() {
return window.location.pathname === this.authFlowPath;
}
public static setToken(token: string, sessionId: string, redirectPath: string) {
localStorage.setItem(this.storageKey, token);
localStorage.setItem(this.sessionKey, sessionId);
window.location.href = redirectPath;
}
public static getToken(sessionId: string): any {
const storageSessionId = localStorage.getItem(this.sessionKey);
if (sessionId === storageSessionId) {
const token = localStorage.getItem(this.storageKey);
return token ? token : this.goToAuthFlowPath();
} else {
this.clearToken();
this.goToAuthFlowPath();
}
}
public static clearToken() {
localStorage.removeItem(this.storageKey);
localStorage.removeItem(this.sessionKey);
}
private static goToAuthFlowPath() {
window.location.href = this.authFlowPath;
}
}

View File

@ -0,0 +1,48 @@
import { NgModule } from '@angular/core';
import { AccountService } from './services/accounts.service';
import { CategoryService } from './services/category.service';
import { ClaimService } from './services/claim.service';
import { CustomerService } from './services/customer.service';
import { InvoiceService } from './services/invoice.service';
import { PaymentsService } from './services/payments.service';
import { ShopService } from './services/shop.service';
import { ConfigService } from './services/config.service';
@NgModule({
providers: [
AccountService,
CategoryService,
ClaimService,
CustomerService,
InvoiceService,
PaymentsService,
ShopService,
ConfigService
]
})
export class BackendModule { }
export * from './services/accounts.service';
export * from './services/category.service';
export * from './services/claim.service';
export * from './services/config.service';
export * from './services/customer.service';
export * from './services/invoice.service';
export * from './services/payments.service';
export * from './services/shop.service';
export * from './classes/category.class';
export * from './classes/claim.class';
export * from './classes/contract.class';
export * from './classes/contractor.class';
export * from './classes/conversion.class';
export * from './classes/geodata.class';
export * from './classes/invoice.class';
export * from './classes/request-params.class';
export * from './classes/revenue.class';
export * from './classes/shop.class';
export * from './classes/shop-creation.class';
export * from './classes/shop-detail.class';
export * from './classes/shop-item.class';
export * from './classes/shop-modification.class';

View File

@ -0,0 +1,8 @@
export class Category {
public categoryRef: number;
public description: string;
public name: string;
}

View File

@ -0,0 +1,10 @@
export class Claim {
public id: number;
public changeset: any[];
public status: {
status: string;
};
}

View File

@ -0,0 +1,14 @@
export class Contract {
public number: string;
public systemContractorRef: string;
public concludedAt: string;
public validSince: string;
public validUntil: string;
public terminatedAt: string;
}

View File

@ -0,0 +1,6 @@
export class Contractor {
public registeredName: string;
public legalEntity: string;
}

View File

@ -0,0 +1,10 @@
export class Conversion {
public conversion: number;
public offset: number;
public successfulCount: number;
public totalCount: number;
}

View File

@ -0,0 +1,12 @@
export class GeoData {
public cityName: string;
public currency: string;
public offset: number;
public profit: number;
public revenue: number;
}

View File

@ -0,0 +1,18 @@
export class Invoice {
public id: number;
public shopID: number;
public amount: number;
public currency: string;
public description: string;
public dueDate: string;
public product: string;
public status: string;
}

View File

@ -0,0 +1,20 @@
export class RequestParams {
public fromTime: string;
public toTime: string;
public splitUnit: string = 'minute';
public splitSize: string = '1';
public paymentMethod: string = 'bank_card';
constructor(fromTime: string, toTime: string, splitUnit?: string, splitSize?: string, paymentMethod?: string) {
this.fromTime = fromTime;
this.toTime = toTime;
this.splitUnit = splitUnit ? splitUnit : this.splitUnit;
this.splitSize = splitSize ? splitSize : this.splitSize;
this.paymentMethod = paymentMethod ? paymentMethod : this.paymentMethod;
}
}

View File

@ -0,0 +1,10 @@
export class Revenue {
public currency: string;
public offset: number;
public profit: number;
public revenue: number;
}

View File

@ -0,0 +1,8 @@
import { Shop } from './shop.class';
export class ShopCreation {
public modificationType: string;
public shop: Shop;
}

View File

@ -0,0 +1,8 @@
export class ShopDetail {
public name: string;
public description: string;
public location: string;
}

View File

@ -0,0 +1,11 @@
export class ShopItem {
public value: string;
public label: string;
constructor(value: string, label: string) {
this.value = value;
this.label = label;
}
}

View File

@ -0,0 +1,22 @@
export class ShopModification {
public modificationType: string;
public shopID: string;
public details: {
modificationType: string;
details: {
shopDetails: {
name: string;
description: string;
location: string;
},
contractor: {
registeredName: string;
legalEntity: string;
},
categoryRef: string;
}
};
}

View File

@ -0,0 +1,20 @@
import { ShopDetail } from './shop-detail.class';
import { Contractor } from './contractor.class';
import { Contract } from './contract.class';
export class Shop {
public shopID: string;
public isBlocked: boolean;
public isSuspended: boolean;
public categoryRef: number;
public shopDetails: ShopDetail;
public contractor: Contractor;
public contract: Contract;
}

View File

@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { ConfigService } from './config.service';
@Injectable()
export class AccountService {
constructor(private http: Http, private config: ConfigService) { }
public getShopAccounts(shopID: string): Promise<any> {
return this.http.get(`${this.config.capiUrl}/processing/shops/${shopID}/accounts`)
.toPromise()
.then(response => response.json());
}
public getShopAccountDetails(shopID: string, accountID: string): Promise<any> {
return this.http.get(`${this.config.capiUrl}/processing/shops/${shopID}/accounts/${accountID}`)
.toPromise()
.then(response => response.json());
}
}

View File

@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Category } from '../classes/category.class';
import { ConfigService } from './config.service';
@Injectable()
export class CategoryService {
constructor(private http: Http, private config: ConfigService) { }
public getCategories(): Promise<Category[]> {
return this.http.get(`${this.config.capiUrl}/processing/categories`)
.toPromise()
.then(response => response.json());
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Claim } from '../classes/claim.class';
import { ConfigService } from './config.service';
@Injectable()
export class ClaimService {
constructor(private http: Http, private config: ConfigService) { }
public getClaim(queryParams: any): Promise<Claim> {
let params = new URLSearchParams();
params.set('claimStatus', queryParams.status);
return this.http.get(`${this.config.capiUrl}/processing/claims`, {
search: params
})
.toPromise()
.then(response => response.json());
}
public revokeClaim(claimID: any, revokeDetails: any): Promise<string> {
const url = `${this.config.capiUrl}/processing/claims/${claimID}/revoke`;
const params = {
reason: revokeDetails.reason
};
return this.http.post(url, params)
.toPromise()
.then(response => response.statusText);
}
}

View File

@ -0,0 +1,20 @@
import { Http } from '@angular/http';
import { Injectable } from '@angular/core';
@Injectable()
export class ConfigService {
public capiUrl: string;
constructor(private http: Http) { }
public load() {
return new Promise(resolve => {
this.http.get('appConfig.json').map(res => res.json())
.subscribe(data => {
this.capiUrl = data.capiUrl;
resolve();
});
});
}
}

View File

@ -0,0 +1,56 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import * as moment from 'moment';
import { RequestParams } from '../classes/request-params.class';
import { ConfigService } from './config.service';
@Injectable()
export class CustomerService {
constructor(private http: Http, private config: ConfigService) { }
public getPaymentMethod(shopID: string, requestParams: RequestParams): Promise<any> {
let params = new URLSearchParams();
let fromTime = moment(requestParams.fromTime).utc().format();
let toTime = moment(requestParams.toTime).utc().format();
params.set('fromTime', fromTime);
params.set('toTime', toTime);
params.set('splitUnit', requestParams.splitUnit);
params.set('splitSize', requestParams.splitSize);
params.set('paymentMethod', requestParams.paymentMethod);
return this.http.get(`${this.config.capiUrl}/analytics/shops/${shopID}/customers/stats/payment_method`, {
search: params
})
.toPromise()
.then(
response => {
return response.json();
}
);
}
public getRate(shopID: string, requestParams: RequestParams): Promise<any> {
let params = new URLSearchParams();
let fromTime = moment(requestParams.fromTime).utc().format();
let toTime = moment(requestParams.toTime).utc().format();
params.set('fromTime', fromTime);
params.set('toTime', toTime);
return this.http.get(`${this.config.capiUrl}/analytics/shops/${shopID}/customers/stats/rate`, {
search: params
})
.toPromise()
.then(
response => {
return response.json();
}
);
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { ConfigService } from './config.service';
@Injectable()
export class InvoiceService {
constructor(private http: Http, private config: ConfigService) { }
public getInvoices(shopID: string, request: any): Promise<any> {
let params = new URLSearchParams();
params.set('fromTime', request.fromTime);
params.set('toTime', request.toTime);
params.set('limit', request.limit);
params.set('offset', request.offset);
if (request.invoiceID) {
params.set('invoiceID', request.invoiceID);
}
if (request.status) {
params.set('status', request.status);
}
return this.http.get(`${this.config.capiUrl}/analytics/shops/${shopID}/invoices`, {
search: params
}).toPromise().then(response => response.json());
}
}

View File

@ -0,0 +1,70 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import * as moment from 'moment';
import { GeoData } from '../classes/geodata.class';
import { Revenue } from '../classes/revenue.class';
import { Conversion } from '../classes/conversion.class';
import { RequestParams } from '../classes/request-params.class';
import { ConfigService } from './config.service';
@Injectable()
export class PaymentsService {
constructor(private http: Http, private config: ConfigService) { }
public getGeoChartData(shopID: string, requestParams: RequestParams): Promise<GeoData[]> {
const params = new URLSearchParams();
const fromTime = moment(requestParams.fromTime).utc().format();
const toTime = moment(requestParams.toTime).utc().format();
params.set('fromTime', fromTime);
params.set('toTime', toTime);
params.set('splitUnit', requestParams.splitUnit);
params.set('splitSize', requestParams.splitSize);
return this.http.get(`${this.config.capiUrl}/analytics/shops/${shopID}/payments/stats/geo`, {
search: params
})
.toPromise()
.then(response => response.json());
}
public getRevenueStat(shopID: string, requestParams: RequestParams): Promise<Revenue[]> {
const params = new URLSearchParams();
const fromTime = moment(requestParams.fromTime).utc().format();
const toTime = moment(requestParams.toTime).utc().format();
params.set('fromTime', fromTime);
params.set('toTime', toTime);
params.set('splitUnit', requestParams.splitUnit);
params.set('splitSize', requestParams.splitSize);
return this.http.get(`${this.config.capiUrl}/analytics/shops/${shopID}/payments/stats/revenue`, {
search: params
})
.toPromise()
.then(response => response.json());
}
public getConversionStat(shopID: string, requestParams: RequestParams): Promise<Conversion[]> {
const params = new URLSearchParams();
const fromTime = moment(requestParams.fromTime).utc().format();
const toTime = moment(requestParams.toTime).utc().format();
params.set('fromTime', fromTime);
params.set('toTime', toTime);
params.set('splitUnit', requestParams.splitUnit);
params.set('splitSize', requestParams.splitSize);
return this.http.get(`${this.config.capiUrl}/analytics/shops/${shopID}/payments/stats/conversion`, {
search: params
})
.toPromise()
.then(response => response.json());
}
}

View File

@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Shop } from '../classes/shop.class';
import { ConfigService } from './config.service';
@Injectable()
export class ShopService {
constructor(private http: Http, private config: ConfigService) { }
public getShops(): Promise<Shop[]> {
return this.http.get(`${this.config.capiUrl}/processing/me`)
.toPromise()
.then(response => response.json().shops as Shop[]);
}
public createShop(args: any): Promise<string> {
const params = {
categoryRef: Number(args.categoryRef),
shopDetails: args.shopDetails,
contractor: args.contractor
};
return this.http.post(`${this.config.capiUrl}/processing/shops`, params)
.toPromise()
.then(response => response.json());
}
public updateShop(shopID: any, args: any): Promise<string> {
const url = `${this.config.capiUrl}/processing/shops/${shopID}`;
const params = {
categoryRef: Number(args.categoryRef),
shopDetails: args.shopDetails,
contractor: args.contractor
};
return this.http.post(url, params)
.toPromise()
.then(response => response.json());
}
public activateShop(shopID: any): Promise<string> {
const url = `${this.config.capiUrl}/processing/shops/${shopID}/activate`;
const params = {};
return this.http.put(url, params)
.toPromise()
.then(response => response.json());
}
}

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Broadcaster } from './services/broadcaster.service';
import { ToggleMenuBroadcaster } from './services/toggle-menu-broadcaster.service';
import { HttpErrorBroadcaster } from './services/http-error-broadcaster.service';
@NgModule({
providers: [
Broadcaster,
ToggleMenuBroadcaster,
HttpErrorBroadcaster
]
})
export class BroadcasterModule { }
export * from './services/broadcaster.service';
export * from './services/toggle-menu-broadcaster.service';
export * from './services/http-error-broadcaster.service';

View File

@ -0,0 +1,23 @@
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
export class Broadcaster {
private eventBus: Subject<any>;
constructor() {
this.eventBus = new Subject<any>();
}
public broadcast(key: any, data?: any) {
this.eventBus.next({key, data});
}
public on<T>(key: any): Observable<T> {
return this.eventBus.asObservable()
.filter(event => event.key === key)
.map((event: any) => <T> event.data);
}
}

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Broadcaster } from './broadcaster.service';
@Injectable()
export class HttpErrorBroadcaster {
constructor(
private broadcaster: Broadcaster
) { }
public fire(status: number) {
this.broadcaster.broadcast(HttpErrorBroadcaster, status);
}
public on(): Observable<string> {
return this.broadcaster.on<string>(HttpErrorBroadcaster);
}
}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Broadcaster } from './broadcaster.service';
@Injectable()
export class ToggleMenuBroadcaster {
constructor(private broadcaster: Broadcaster) {}
public fire() {
this.broadcaster.broadcast(ToggleMenuBroadcaster);
}
public on(): Observable<string> {
return this.broadcaster.on<string>(ToggleMenuBroadcaster);
}
}

View File

@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { SelectComponent } from './components/select/select.component';
import { LoadingComponent } from './components/loading/loading.component';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
SelectComponent,
LoadingComponent
],
exports: [
SelectComponent,
LoadingComponent
]
})
export class CommonModule { }
export * from './components/loading/loading.component';
export * from './components/select/select.component';
export * from './components/select/select.class';

View File

@ -0,0 +1,2 @@
a.fa.fa-cog.fa-spin.fa-fw.loading_spinner(*ngIf="isLoading")
ng-content(*ngIf="!isLoading")

View File

@ -0,0 +1,11 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'kof-loading',
templateUrl: 'loading.component.pug'
})
export class LoadingComponent {
@Input()
public isLoading: boolean;
}

View File

@ -0,0 +1,11 @@
export class SelectItem {
public value: any;
public label: string;
constructor(value: any, label: string) {
this.value = value;
this.label = label;
}
}

View File

@ -0,0 +1,32 @@
.kof-select {
overflow: hidden;
border: 1px solid #ccc;
position: relative;
select {
background: transparent;
-webkit-appearance: none;
-moz-appearance: none;
outline: 0;
border: 0;
border-radius: 0;
width: 100%;
padding: 0 20px 0 10px;
font-size: 13px;
line-height: 2;
color: #000;
color: rgba(0,0,0,0);
text-shadow: 0 0 0 #000;
}
&::before {
content: '';
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #888;
position: absolute;
right: 10px;
top: 50%;
transform: translate(0, -50%);
}
}

View File

@ -0,0 +1,4 @@
.kof-select
select.form-control([(ngModel)]="selectedValue", [ngModelOptions]="modelOptions")
option(*ngIf="placeholder" ,[value]="''") {{placeholder}}
option(*ngFor="let item of items", [value]="item.value") {{item.label}}

View File

@ -0,0 +1,65 @@
import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { SelectItem } from './select.class';
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectComponent),
multi: true
};
@Component({
selector: 'kof-select',
templateUrl: 'select.component.pug',
styleUrls: ['select.component.less'],
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class SelectComponent {
@Input()
public items: SelectItem[];
@Input()
public placeholder: any;
@Input()
public modelOptions: any;
@Output()
public onChange: EventEmitter<any> = new EventEmitter<any>();
private innerSelectedValue: any;
private onTouchedCallback: Function;
private onChangeCallback: Function;
set selectedValue(value: any) {
if (value !== this.innerSelectedValue) {
this.innerSelectedValue = value;
this.onChangeCallback(value);
this.onChange.emit();
}
}
get selectedValue(): any {
if (!this.innerSelectedValue && this.placeholder) {
this.innerSelectedValue = '';
}
return this.innerSelectedValue;
};
public writeValue(value: any) {
if (value !== this.innerSelectedValue) {
this.innerSelectedValue = value;
}
}
public registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
public registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}

View File

@ -0,0 +1,8 @@
kof-http-error-handle
div([ngClass]="{'nav-md': !isSidebarOpened, 'nav-sm': isSidebarOpened}")
.container.body
.main_container
kof-sidebar
kof-top-panel
.right_col.outlet
router-outlet

View File

@ -0,0 +1,30 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ToggleMenuBroadcaster } from 'koffing/broadcaster/broadcaster.module';
import { SidebarStateService } from './sidebarState.service';
@Component({
selector: 'kof-app',
templateUrl: './container.component.pug',
encapsulation: ViewEncapsulation.None
})
export class ContainerComponent implements OnInit {
public isSidebarOpened: boolean;
constructor(
private toggleMenuBroadcaster: ToggleMenuBroadcaster
) { }
public ngOnInit() {
this.isSidebarOpened = SidebarStateService.isOpened();
this.registerToggleMenuBroadcast();
}
private registerToggleMenuBroadcast() {
this.toggleMenuBroadcaster.on().subscribe(() => {
SidebarStateService.toggleState();
this.isSidebarOpened = SidebarStateService.isOpened();
});
}
}

View File

@ -0,0 +1,17 @@
export class SidebarStateService {
public static isOpened(): boolean {
return Boolean(localStorage.getItem(this.key));
}
public static toggleState() {
if (this.isOpened()) {
localStorage.removeItem(this.key);
} else {
localStorage.setItem(this.key, 'true');
}
}
private static key: string = 'isSidebarOpened';
}

View File

@ -0,0 +1 @@
p-growl([value]="messages", [life]="lifeTime")

View File

@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Message } from 'primeng/primeng';
import { HttpErrorBroadcaster } from 'koffing/broadcaster/broadcaster.module';
@Component({
selector: 'kof-http-error-handle',
templateUrl: './http-error-handle.component.pug'
})
export class HttpErrorHandleComponent implements OnInit {
public messages: Message[] = [];
public lifeTime: number = 60000;
constructor(
private router: Router,
private httpErrorBroadcaster: HttpErrorBroadcaster
) { }
public ngOnInit() {
this.router.events.subscribe(() => this.messages = []);
this.httpErrorBroadcaster.on().subscribe((status: any) => {
let message: string = '';
if (status === 0 || status >= 500 && status < 600) {
message = 'Произошла ошибка на сервере. Повторите действие позже.';
}
this.messages.push({
severity: 'error',
summary: `Код ошибки: ${status}`,
detail: message
});
});
}
}

View File

@ -0,0 +1,21 @@
.col-md-3.left_col
.left_col.scroll-view
.navbar.nav_title
a.site_title(href="/")
span RBKmoney
.clearfix
#sidebar-menu.main_menu_side.hidden-print.main_menu
.menu_section
ul.nav.side-menu
li([routerLinkActive]="['active']")
a(routerLink="/analytics")
i.fa.fa-bar-chart-o
| Аналитика
li([routerLinkActive]="['active']")
a(routerLink="/shops")
i.fa.fa-shopping-cart
| Мои магазины
li([routerLinkActive]="['active']")
a(routerLink="/tokenization")
i.fa.fa-key
| Токенизация

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