mirror of
https://github.com/valitydev/koffing.git
synced 2024-11-06 01:05:19 +00:00
parent
987895efd6
commit
3ee25fbe2e
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
/node_modules/
|
||||
/dist/
|
||||
Dockerfile
|
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal 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
24
Dockerfile.sh
Executable 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
39
Jenkinsfile
vendored
Normal 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
42
Makefile
Normal 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
|
23
README.md
23
README.md
@ -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
10
config/helpers.js
Normal 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
21
config/karma-test-shim.js
Normal 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
37
config/karma.conf.js
Normal 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);
|
||||
};
|
3
config/runtime/appConfig.json
Normal file
3
config/runtime/appConfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"capiUrl": "http://localhost:9000/v1"
|
||||
}
|
8
config/runtime/koffingKeycloakConfig.json
Normal file
8
config/runtime/koffingKeycloakConfig.json
Normal 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
|
||||
}
|
5
config/runtime/tokenizationKeycloakConfig.json
Normal file
5
config/runtime/tokenizationKeycloakConfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"realm": "external",
|
||||
"auth-server-url": "http://localhost:8080/auth/",
|
||||
"resource": "tokenizer"
|
||||
}
|
89
config/webpack.common.js
Normal file
89
config/webpack.common.js
Normal 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
24
config/webpack.dev.js
Normal 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
45
config/webpack.prod.js
Normal 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
37
config/webpack.test.js
Normal 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
13
containerpilot.json
Normal 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
1
karma.conf.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./config/karma.conf.js');
|
15
nginx.conf
Normal file
15
nginx.conf
Normal 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
70
package.json
Normal 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"
|
||||
}
|
||||
}
|
39
src/app/analytics/analytics-routing.module.ts
Normal file
39
src/app/analytics/analytics-routing.module.ts
Normal 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 { }
|
54
src/app/analytics/analytics.module.ts
Normal file
54
src/app/analytics/analytics.module.ts
Normal 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';
|
@ -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
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -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'
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
kof-loading([isLoading]="isLoading")
|
||||
div
|
||||
canvas(
|
||||
baseChart,
|
||||
[data]="data",
|
||||
[labels]="labels",
|
||||
[chartType]="type",
|
||||
[options]="options",
|
||||
[colors]="chartColors",
|
||||
height="80"
|
||||
)
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
.row
|
||||
.col-xs-12
|
||||
form.form-inline.form-label-right
|
||||
.form-group
|
||||
label.control-label Статистика с
|
||||
.ui-calendar
|
||||
p-calendar.ui-inputtext.statistics(name="fromTime", [(ngModel)]="fromTimeDate", dateFormat="dd.mm.yy")
|
||||
.form-group
|
||||
label.control-label по
|
||||
.ui-calendar
|
||||
p-calendar.ui-inputtext.statistics(name="toTime", [(ngModel)]="toTimeDate", dateFormat = "dd.mm.yy")
|
||||
span
|
||||
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")
|
@ -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 );
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
kof-loading([isLoading]="isLoading")
|
||||
div
|
||||
canvas(
|
||||
baseChart,
|
||||
[data]="data",
|
||||
[labels]="labels",
|
||||
[chartType]="type",
|
||||
[options]="options",
|
||||
[colors]="chartColors",
|
||||
height=180
|
||||
)
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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}}
|
@ -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;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
kof-loading([isLoading]="isLoading")
|
||||
div
|
||||
canvas(
|
||||
baseChart,
|
||||
[data]="data",
|
||||
[labels]="labels",
|
||||
[chartType]="type",
|
||||
[options]="options",
|
||||
[colors]="chartColors",
|
||||
height=180
|
||||
)
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
kof-loading([isLoading]="isLoading")
|
||||
div
|
||||
canvas.revenueChart(
|
||||
baseChart,
|
||||
[data]="data",
|
||||
[labels]="labels",
|
||||
[chartType]="type",
|
||||
[options]="options",
|
||||
[colors]="chartColors",
|
||||
height="80"
|
||||
)
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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)")
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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="")
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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()") Найти
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
export class PAYMENT_STATUSES {
|
||||
public static get GET(): any {
|
||||
return {
|
||||
unpaid: 'Неоплаченный',
|
||||
cancelled: 'Отмененный',
|
||||
paid: 'Оплаченный',
|
||||
refunded: 'Возвращенный',
|
||||
fulfilled: 'Выполненный'
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}}
|
@ -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;
|
||||
}
|
@ -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
34
src/app/app.module.ts
Normal 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 { }
|
4
src/app/auth/auth.module.ts
Normal file
4
src/app/auth/auth.module.ts
Normal 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';
|
7
src/app/auth/classes/AuthInfo.class.ts
Normal file
7
src/app/auth/classes/AuthInfo.class.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class AuthInfo {
|
||||
|
||||
public profileName: string = '';
|
||||
|
||||
public token: string = '';
|
||||
|
||||
}
|
113
src/app/auth/interceptors/auth-http.interceptor.ts
Normal file
113
src/app/auth/interceptors/auth-http.interceptor.ts
Normal 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()}`;
|
||||
}
|
||||
}
|
61
src/app/auth/services/auth.service.ts
Normal file
61
src/app/auth/services/auth.service.ts
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
38
src/app/auth/services/offline-token.service.ts
Normal file
38
src/app/auth/services/offline-token.service.ts
Normal 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;
|
||||
}
|
||||
}
|
48
src/app/backend/backend.module.ts
Normal file
48
src/app/backend/backend.module.ts
Normal 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';
|
8
src/app/backend/classes/category.class.ts
Normal file
8
src/app/backend/classes/category.class.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export class Category {
|
||||
|
||||
public categoryRef: number;
|
||||
|
||||
public description: string;
|
||||
|
||||
public name: string;
|
||||
}
|
10
src/app/backend/classes/claim.class.ts
Normal file
10
src/app/backend/classes/claim.class.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class Claim {
|
||||
|
||||
public id: number;
|
||||
|
||||
public changeset: any[];
|
||||
|
||||
public status: {
|
||||
status: string;
|
||||
};
|
||||
}
|
14
src/app/backend/classes/contract.class.ts
Normal file
14
src/app/backend/classes/contract.class.ts
Normal 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;
|
||||
}
|
6
src/app/backend/classes/contractor.class.ts
Normal file
6
src/app/backend/classes/contractor.class.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class Contractor {
|
||||
|
||||
public registeredName: string;
|
||||
|
||||
public legalEntity: string;
|
||||
}
|
10
src/app/backend/classes/conversion.class.ts
Normal file
10
src/app/backend/classes/conversion.class.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class Conversion {
|
||||
|
||||
public conversion: number;
|
||||
|
||||
public offset: number;
|
||||
|
||||
public successfulCount: number;
|
||||
|
||||
public totalCount: number;
|
||||
}
|
12
src/app/backend/classes/geodata.class.ts
Normal file
12
src/app/backend/classes/geodata.class.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class GeoData {
|
||||
|
||||
public cityName: string;
|
||||
|
||||
public currency: string;
|
||||
|
||||
public offset: number;
|
||||
|
||||
public profit: number;
|
||||
|
||||
public revenue: number;
|
||||
}
|
18
src/app/backend/classes/invoice.class.ts
Normal file
18
src/app/backend/classes/invoice.class.ts
Normal 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;
|
||||
}
|
20
src/app/backend/classes/request-params.class.ts
Normal file
20
src/app/backend/classes/request-params.class.ts
Normal 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;
|
||||
}
|
||||
}
|
10
src/app/backend/classes/revenue.class.ts
Normal file
10
src/app/backend/classes/revenue.class.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class Revenue {
|
||||
|
||||
public currency: string;
|
||||
|
||||
public offset: number;
|
||||
|
||||
public profit: number;
|
||||
|
||||
public revenue: number;
|
||||
}
|
8
src/app/backend/classes/shop-creation.class.ts
Normal file
8
src/app/backend/classes/shop-creation.class.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Shop } from './shop.class';
|
||||
|
||||
export class ShopCreation {
|
||||
|
||||
public modificationType: string;
|
||||
|
||||
public shop: Shop;
|
||||
}
|
8
src/app/backend/classes/shop-detail.class.ts
Normal file
8
src/app/backend/classes/shop-detail.class.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export class ShopDetail {
|
||||
|
||||
public name: string;
|
||||
|
||||
public description: string;
|
||||
|
||||
public location: string;
|
||||
}
|
11
src/app/backend/classes/shop-item.class.ts
Normal file
11
src/app/backend/classes/shop-item.class.ts
Normal 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;
|
||||
}
|
||||
}
|
22
src/app/backend/classes/shop-modification.class.ts
Normal file
22
src/app/backend/classes/shop-modification.class.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
20
src/app/backend/classes/shop.class.ts
Normal file
20
src/app/backend/classes/shop.class.ts
Normal 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;
|
||||
}
|
23
src/app/backend/services/accounts.service.ts
Normal file
23
src/app/backend/services/accounts.service.ts
Normal 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());
|
||||
}
|
||||
}
|
18
src/app/backend/services/category.service.ts
Normal file
18
src/app/backend/services/category.service.ts
Normal 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());
|
||||
}
|
||||
}
|
35
src/app/backend/services/claim.service.ts
Normal file
35
src/app/backend/services/claim.service.ts
Normal 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);
|
||||
}
|
||||
}
|
20
src/app/backend/services/config.service.ts
Normal file
20
src/app/backend/services/config.service.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
56
src/app/backend/services/customer.service.ts
Normal file
56
src/app/backend/services/customer.service.ts
Normal 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();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
28
src/app/backend/services/invoice.service.ts
Normal file
28
src/app/backend/services/invoice.service.ts
Normal 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());
|
||||
}
|
||||
}
|
70
src/app/backend/services/payments.service.ts
Normal file
70
src/app/backend/services/payments.service.ts
Normal 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());
|
||||
}
|
||||
}
|
49
src/app/backend/services/shop.service.ts
Normal file
49
src/app/backend/services/shop.service.ts
Normal 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());
|
||||
}
|
||||
}
|
17
src/app/broadcaster/broadcaster.module.ts
Normal file
17
src/app/broadcaster/broadcaster.module.ts
Normal 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';
|
23
src/app/broadcaster/services/broadcaster.service.ts
Normal file
23
src/app/broadcaster/services/broadcaster.service.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
26
src/app/common/common.module.ts
Normal file
26
src/app/common/common.module.ts
Normal 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';
|
2
src/app/common/components/loading/loading.component.pug
Normal file
2
src/app/common/components/loading/loading.component.pug
Normal file
@ -0,0 +1,2 @@
|
||||
a.fa.fa-cog.fa-spin.fa-fw.loading_spinner(*ngIf="isLoading")
|
||||
ng-content(*ngIf="!isLoading")
|
11
src/app/common/components/loading/loading.component.ts
Normal file
11
src/app/common/components/loading/loading.component.ts
Normal 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;
|
||||
}
|
11
src/app/common/components/select/select.class.ts
Normal file
11
src/app/common/components/select/select.class.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
32
src/app/common/components/select/select.component.less
Normal file
32
src/app/common/components/select/select.component.less
Normal 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%);
|
||||
}
|
||||
}
|
4
src/app/common/components/select/select.component.pug
Normal file
4
src/app/common/components/select/select.component.pug
Normal 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}}
|
65
src/app/common/components/select/select.component.ts
Normal file
65
src/app/common/components/select/select.component.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
30
src/app/root/components/container/container.component.ts
Normal file
30
src/app/root/components/container/container.component.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
17
src/app/root/components/container/sidebarState.service.ts
Normal file
17
src/app/root/components/container/sidebarState.service.ts
Normal 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';
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
p-growl([value]="messages", [life]="lifeTime")
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
21
src/app/root/components/sidebar/sidebar.component.pug
Normal file
21
src/app/root/components/sidebar/sidebar.component.pug
Normal 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
Loading…
Reference in New Issue
Block a user