├── .gitignore
├── app.js
├── client
├── .angular-cli.json
├── .editorconfig
├── .gitignore
├── README.md
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── proxy.conf.json
├── src
│ ├── app
│ │ ├── analytics-page
│ │ │ ├── analytics-page.component.css
│ │ │ ├── analytics-page.component.html
│ │ │ └── analytics-page.component.ts
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── app.routing-module.ts
│ │ ├── history-page
│ │ │ ├── history-filter
│ │ │ │ ├── history-filter.component.css
│ │ │ │ ├── history-filter.component.html
│ │ │ │ └── history-filter.component.ts
│ │ │ ├── history-list
│ │ │ │ ├── history-list.component.css
│ │ │ │ ├── history-list.component.html
│ │ │ │ └── history-list.component.ts
│ │ │ ├── history-page.component.css
│ │ │ ├── history-page.component.html
│ │ │ └── history-page.component.ts
│ │ ├── login-page
│ │ │ ├── login-page.component.css
│ │ │ ├── login-page.component.html
│ │ │ └── login-page.component.ts
│ │ ├── new-order-page
│ │ │ ├── new-order-page.component.css
│ │ │ ├── new-order-page.component.html
│ │ │ ├── new-order-page.component.ts
│ │ │ ├── order-categories
│ │ │ │ ├── order-categories.component.css
│ │ │ │ ├── order-categories.component.html
│ │ │ │ └── order-categories.component.ts
│ │ │ ├── order-production
│ │ │ │ ├── order-production.component.css
│ │ │ │ ├── order-production.component.html
│ │ │ │ └── order-production.component.ts
│ │ │ └── order.service.ts
│ │ ├── overview-page
│ │ │ ├── overview-page.component.css
│ │ │ ├── overview-page.component.html
│ │ │ └── overview-page.component.ts
│ │ ├── products-page
│ │ │ ├── product-form-page
│ │ │ │ ├── positions-form
│ │ │ │ │ ├── positions-form.component.css
│ │ │ │ │ ├── positions-form.component.html
│ │ │ │ │ └── positions-form.component.ts
│ │ │ │ ├── product-form-page.component.css
│ │ │ │ ├── product-form-page.component.html
│ │ │ │ └── product-form-page.component.ts
│ │ │ ├── products-page.component.css
│ │ │ ├── products-page.component.html
│ │ │ └── products-page.component.ts
│ │ ├── registration-page
│ │ │ ├── registration-page.component.css
│ │ │ ├── registration-page.component.html
│ │ │ └── registration-page.component.ts
│ │ └── shared
│ │ │ ├── classes
│ │ │ ├── auth.guard.ts
│ │ │ ├── material.service.ts
│ │ │ └── token.interceptor.ts
│ │ │ ├── components
│ │ │ └── loader
│ │ │ │ ├── loader.component.css
│ │ │ │ ├── loader.component.html
│ │ │ │ └── loader.component.ts
│ │ │ ├── interfaces.ts
│ │ │ ├── layouts
│ │ │ ├── empty-layout
│ │ │ │ ├── empty-layout.component.css
│ │ │ │ ├── empty-layout.component.html
│ │ │ │ └── empty-layout.component.ts
│ │ │ └── site-layout
│ │ │ │ ├── site-layout.component.css
│ │ │ │ ├── site-layout.component.html
│ │ │ │ └── site-layout.component.ts
│ │ │ └── services
│ │ │ ├── analytics.service.ts
│ │ │ ├── auth.service.ts
│ │ │ ├── categories.service.ts
│ │ │ ├── orders.service.ts
│ │ │ └── positions.service.ts
│ ├── assets
│ │ ├── .gitkeep
│ │ └── cake.jpg
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ ├── test.ts
│ ├── theme
│ │ ├── materialize.min.css
│ │ └── styles.css
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
├── tsconfig.json
└── tslint.json
├── config
├── keys.js
└── keys.prod.js
├── controllers
├── analytics.js
├── auth.js
├── category.js
├── order.js
└── position.js
├── index.js
├── middleware
├── passport.js
└── upload.js
├── models
├── category.js
├── order.js
├── position.js
└── user.js
├── package-lock.json
├── package.json
├── routes
├── analytics.js
├── auth.js
├── category.js
├── order.js
└── position.js
├── uploads
├── .gitkeep
├── 17042018-174213_055-coffee.png
├── 17042018-174233_978-cake.jpg
├── 17042018-214450_614-cake.jpg
├── 19042018-145815_822-cake.jpg
├── 20042018-143422_242-cake.jpg
├── 20042018-143459_550-coffee.png
└── 21042018-122114_479-cake.jpg
└── utils
└── error.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | .idea
3 | node_modules
4 | .vscode
5 | config/keys.dev.js
6 | uploads/*
7 | client/etc/
8 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const mongoose = require('mongoose')
3 | const bodyParser = require('body-parser')
4 | const path = require('path')
5 | const passport = require('passport')
6 | const keys = require('./config/keys')
7 | const authRoutes = require('./routes/auth')
8 | const orderRoutes = require('./routes/order')
9 | const categoryRoutes = require('./routes/category')
10 | const positionRoutes = require('./routes/position')
11 | const analyticsRoutes = require('./routes/analytics')
12 | const app = express()
13 |
14 | mongoose.connect(keys.mongoURI)
15 | .then(() => console.log('MongoDB connected'))
16 | .catch(error => console.log(error))
17 |
18 | app.use(passport.initialize())
19 | require('./middleware/passport')(passport)
20 |
21 | app.use(require('morgan')('dev'))
22 | app.use('/uploads', express.static('uploads'))
23 | app.use(bodyParser.urlencoded({extended: true}))
24 | app.use(bodyParser.json())
25 | app.use(require('cors')())
26 |
27 | app.use('/api/auth', authRoutes)
28 | app.use('/api/order', orderRoutes)
29 | app.use('/api/category', categoryRoutes)
30 | app.use('/api/position', positionRoutes)
31 | app.use('/api/analytics', analyticsRoutes)
32 |
33 | if (process.env.NODE_ENV === 'production') {
34 | app.use(express.static('client/dist'))
35 |
36 | app.get('*', (req, res) => {
37 | res.sendFile(path.resolve(__dirname, 'client', 'dist', 'index.html'))
38 | })
39 | }
40 |
41 | module.exports = app
--------------------------------------------------------------------------------
/client/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "fullstack-ng-frontend"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "app",
21 | "styles": [
22 | "styles.css"
23 | ],
24 | "scripts": [],
25 | "environmentSource": "environments/environment.ts",
26 | "environments": {
27 | "dev": "environments/environment.ts",
28 | "prod": "environments/environment.prod.ts"
29 | }
30 | }
31 | ],
32 | "e2e": {
33 | "protractor": {
34 | "config": "./protractor.conf.js"
35 | }
36 | },
37 | "lint": [
38 | {
39 | "project": "src/tsconfig.app.json",
40 | "exclude": "**/node_modules/**"
41 | },
42 | {
43 | "project": "src/tsconfig.spec.json",
44 | "exclude": "**/node_modules/**"
45 | },
46 | {
47 | "project": "e2e/tsconfig.e2e.json",
48 | "exclude": "**/node_modules/**"
49 | }
50 | ],
51 | "test": {
52 | "karma": {
53 | "config": "./karma.conf.js"
54 | }
55 | },
56 | "defaults": {
57 | "styleExt": "css",
58 | "component": {}
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/client/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /dist-server
6 | /tmp
7 | /out-tsc
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.sass-cache
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | npm-debug.log
34 | yarn-error.log
35 | testem.log
36 | /typings
37 |
38 | # e2e
39 | /e2e/*.js
40 | /e2e/*.map
41 |
42 | # System Files
43 | .DS_Store
44 | Thumbs.db
45 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # FullstackNgFrontend
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.4.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/client/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | angularCli: {
23 | environment: 'dev'
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome'],
31 | singleRun: false
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fullstack-ng-frontend",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "engines": {
6 | "node": "9.9.0",
7 | "npm": "5.6.0"
8 | },
9 | "scripts": {
10 | "ng": "ng",
11 | "start": "ng serve --proxy-config proxy.conf.json",
12 | "build": "ng build --prod",
13 | "test": "ng test",
14 | "lint": "ng lint",
15 | "e2e": "ng e2e"
16 | },
17 | "private": true,
18 | "dependencies": {
19 | "@angular/animations": "^5.2.0",
20 | "@angular/common": "^5.2.0",
21 | "@angular/compiler": "^5.2.0",
22 | "@angular/core": "^5.2.0",
23 | "@angular/forms": "^5.2.0",
24 | "@angular/http": "^5.2.0",
25 | "@angular/platform-browser": "^5.2.0",
26 | "@angular/platform-browser-dynamic": "^5.2.0",
27 | "@angular/router": "^5.2.0",
28 | "chart.js": "^2.7.2",
29 | "core-js": "^2.4.1",
30 | "materialize-css": "^1.0.0-beta",
31 | "moment": "^2.22.1",
32 | "rxjs": "^5.5.6",
33 | "zone.js": "^0.8.19",
34 | "@angular/cli": "~1.7.4",
35 | "@angular/compiler-cli": "^5.2.0",
36 | "typescript": "~2.5.3"
37 | },
38 | "devDependencies": {
39 | "@angular/language-service": "^5.2.0",
40 | "@types/jasmine": "~2.8.3",
41 | "@types/jasminewd2": "~2.0.2",
42 | "@types/node": "~6.0.60",
43 | "codelyzer": "^4.0.1",
44 | "jasmine-core": "~2.8.0",
45 | "jasmine-spec-reporter": "~4.2.1",
46 | "karma": "~2.0.0",
47 | "karma-chrome-launcher": "~2.2.0",
48 | "karma-coverage-istanbul-reporter": "^1.2.1",
49 | "karma-jasmine": "~1.1.0",
50 | "karma-jasmine-html-reporter": "^0.2.2",
51 | "protractor": "~5.1.2",
52 | "ts-node": "~4.1.0",
53 | "tslint": "~5.9.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/client/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api/*": {
3 | "target": "http://localhost:5000",
4 | "secure": false,
5 | "logLevel": "debug",
6 | "changeOrigin": true
7 | },
8 | "/uploads/*": {
9 | "target": "http://localhost:5000",
10 | "secure": false,
11 | "logLevel": "debug",
12 | "changeOrigin": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/app/analytics-page/analytics-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/analytics-page/analytics-page.component.css
--------------------------------------------------------------------------------
/client/src/app/analytics-page/analytics-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Аналитика
3 |
4 |
5 |
6 |
7 |
Средний чек {{average}} р.
8 |
9 |
10 |
11 |
Выручка
12 |
13 |
14 |
15 |
16 |
Заказы
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/client/src/app/analytics-page/analytics-page.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'
2 | import {Chart} from 'chart.js'
3 | import {AnalyticsService} from '../shared/services/analytics.service'
4 |
5 | @Component({
6 | selector: 'app-analytics-page',
7 | templateUrl: './analytics-page.component.html',
8 | styleUrls: ['./analytics-page.component.css']
9 | })
10 | export class AnalyticsPageComponent implements AfterViewInit {
11 |
12 | @ViewChild('gainChartRef') gainChartRef: ElementRef
13 | @ViewChild('orderChartRef') orderChartRef: ElementRef
14 |
15 | loading = true
16 | average: number
17 |
18 | constructor(private analyticsService: AnalyticsService) {
19 | }
20 |
21 | ngAfterViewInit() {
22 | const gainConfig: any = {
23 | label: 'Выручка',
24 | color: 'rgb(255, 99, 132)'
25 | }
26 |
27 | const orderConfig: any = {
28 | label: 'Заказы',
29 | color: 'rgb(54, 162, 235)'
30 | }
31 |
32 | this.analyticsService.fetchAnalytics().subscribe(({chart, average}) => {
33 | this.loading = false
34 | this.average = average
35 |
36 | gainConfig.labels = chart.map(i => i.label)
37 | gainConfig.data = chart.map(i => i.gain)
38 |
39 | orderConfig.labels = chart.map(i => i.label)
40 | orderConfig.data = chart.map(i => i.order)
41 |
42 | const gainCtx = this.gainChartRef.nativeElement.getContext('2d')
43 | const orderCtx = this.orderChartRef.nativeElement.getContext('2d')
44 | gainCtx.canvas.height = 300 + 'px'
45 | orderCtx.canvas.height = 300 + 'px'
46 | new Chart(gainCtx, createConfig(gainConfig))
47 | new Chart(orderCtx, createConfig(orderConfig))
48 | })
49 | }
50 | }
51 |
52 | function createConfig({labels, data, label, color, title}) {
53 | return {
54 | type: 'line',
55 | data: {
56 | labels: labels,
57 | datasets: [
58 | {
59 | label: label,
60 | steppedLine: false,
61 | data: data,
62 | borderColor: color,
63 | fill: false
64 | }
65 | ]
66 | },
67 | options: {
68 | responsive: true
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/client/src/app/app.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/app.component.css
--------------------------------------------------------------------------------
/client/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/client/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core'
2 | import {AuthService} from './shared/services/auth.service'
3 |
4 | @Component({
5 | selector: 'app-root',
6 | templateUrl: './app.component.html',
7 | styleUrls: ['./app.component.css']
8 | })
9 | export class AppComponent implements OnInit {
10 |
11 | constructor(private auth: AuthService) {
12 | }
13 |
14 | ngOnInit() {
15 | const potentialToken = localStorage.getItem('auth-token')
16 | if (potentialToken !== null) {
17 | this.auth.setToken(potentialToken)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser'
2 | import {NgModule} from '@angular/core'
3 | import {AppComponent} from './app.component'
4 | import {LoginPageComponent} from './login-page/login-page.component'
5 | import {RegistrationPageComponent} from './registration-page/registration-page.component'
6 | import {AppRoutingModule} from './app.routing-module'
7 | import {EmptyLayoutComponent} from './shared/layouts/empty-layout/empty-layout.component'
8 | import {SiteLayoutComponent} from './shared/layouts/site-layout/site-layout.component'
9 | import {OverviewPageComponent} from './overview-page/overview-page.component'
10 | import {AnalyticsPageComponent} from './analytics-page/analytics-page.component'
11 | import {HistoryPageComponent} from './history-page/history-page.component'
12 | import {NewOrderPageComponent} from './new-order-page/new-order-page.component'
13 | import {ProductsPageComponent} from './products-page/products-page.component'
14 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'
15 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'
16 | import {AuthService} from './shared/services/auth.service'
17 | import {AuthGuard} from './shared/classes/auth.guard'
18 | import {ProductFormPageComponent} from './products-page/product-form-page/product-form-page.component'
19 | import {CategoriesService} from './shared/services/categories.service'
20 | import {TokenInterceptor} from './shared/classes/token.interceptor'
21 | import {PositionsFormComponent} from './products-page/product-form-page/positions-form/positions-form.component'
22 | import {PositionsService} from './shared/services/positions.service'
23 | import {OrderCategoriesComponent} from './new-order-page/order-categories/order-categories.component'
24 | import {OrderProductionComponent} from './new-order-page/order-production/order-production.component'
25 | import {OrdersService} from './shared/services/orders.service'
26 | import {HistoryListComponent} from './history-page/history-list/history-list.component'
27 | import {HistoryFilterComponent} from './history-page/history-filter/history-filter.component'
28 | import {LoaderComponent} from './shared/components/loader/loader.component'
29 | import {AnalyticsService} from './shared/services/analytics.service'
30 |
31 |
32 | @NgModule({
33 | declarations: [
34 | AppComponent,
35 | LoginPageComponent,
36 | RegistrationPageComponent,
37 | EmptyLayoutComponent,
38 | SiteLayoutComponent,
39 | OverviewPageComponent,
40 | AnalyticsPageComponent,
41 | HistoryPageComponent,
42 | NewOrderPageComponent,
43 | ProductsPageComponent,
44 | ProductFormPageComponent,
45 | PositionsFormComponent,
46 | OrderCategoriesComponent,
47 | OrderProductionComponent,
48 | HistoryListComponent,
49 | HistoryFilterComponent,
50 | LoaderComponent
51 | ],
52 | imports: [
53 | BrowserModule,
54 | AppRoutingModule,
55 | FormsModule,
56 | ReactiveFormsModule,
57 | HttpClientModule
58 | ],
59 | providers: [
60 | AuthService,
61 | AuthGuard,
62 | CategoriesService,
63 | PositionsService,
64 | OrdersService,
65 | AnalyticsService,
66 | {
67 | provide: HTTP_INTERCEPTORS,
68 | useClass: TokenInterceptor,
69 | multi: true
70 | }
71 | ],
72 | bootstrap: [AppComponent]
73 | })
74 | export class AppModule {
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/app/app.routing-module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core'
2 | import {RouterModule, Routes} from '@angular/router'
3 | import {AuthGuard} from './shared/classes/auth.guard'
4 | import {LoginPageComponent} from './login-page/login-page.component'
5 | import {RegistrationPageComponent} from './registration-page/registration-page.component'
6 | import {EmptyLayoutComponent} from './shared/layouts/empty-layout/empty-layout.component'
7 | import {SiteLayoutComponent} from './shared/layouts/site-layout/site-layout.component'
8 | import {AnalyticsPageComponent} from './analytics-page/analytics-page.component'
9 | import {HistoryPageComponent} from './history-page/history-page.component'
10 | import {NewOrderPageComponent} from './new-order-page/new-order-page.component'
11 | import {OverviewPageComponent} from './overview-page/overview-page.component'
12 | import {ProductsPageComponent} from './products-page/products-page.component'
13 | import {ProductFormPageComponent} from './products-page/product-form-page/product-form-page.component'
14 | import {OrderCategoriesComponent} from './new-order-page/order-categories/order-categories.component'
15 | import {OrderProductionComponent} from './new-order-page/order-production/order-production.component'
16 |
17 | const routes: Routes = [
18 | {
19 | path: '', component: EmptyLayoutComponent, children: [
20 | {path: '', redirectTo: '/login', pathMatch: 'full'},
21 | {path: 'login', component: LoginPageComponent},
22 | {path: 'registration', component: RegistrationPageComponent}
23 | ]
24 | },
25 | {
26 | path: '', component: SiteLayoutComponent, canActivate: [AuthGuard], children: [
27 | {path: 'overview', component: OverviewPageComponent},
28 | {path: 'history', component: HistoryPageComponent},
29 | {path: 'order', component: NewOrderPageComponent, children: [
30 | {path: '', component: OrderCategoriesComponent},
31 | {path: ':id', component: OrderProductionComponent}
32 | ]},
33 | {path: 'categories', component: ProductsPageComponent},
34 | {path: 'categories/new', component: ProductFormPageComponent},
35 | {path: 'categories/:id', component: ProductFormPageComponent},
36 | {path: 'analytics', component: AnalyticsPageComponent}
37 | ]
38 | }
39 | ]
40 |
41 | @NgModule({
42 | imports: [RouterModule.forRoot(routes)],
43 | exports: [RouterModule]
44 | })
45 | export class AppRoutingModule {
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/app/history-page/history-filter/history-filter.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/history-page/history-filter/history-filter.component.css
--------------------------------------------------------------------------------
/client/src/app/history-page/history-filter/history-filter.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Номер заказа
7 |
8 |
9 |
20 |
21 |
22 |
27 | Применить фильтр
28 |
29 |
30 |
--------------------------------------------------------------------------------
/client/src/app/history-page/history-filter/history-filter.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, Output, ViewChild} from '@angular/core'
2 | import {IMaterialDatepicker, MaterialService} from '../../shared/classes/material.service'
3 | import * as moment from 'moment'
4 |
5 | export interface Filter {
6 | start?: Date
7 | end?: Date
8 | order?: number
9 | }
10 |
11 | @Component({
12 | selector: 'app-history-filter',
13 | templateUrl: './history-filter.component.html',
14 | styleUrls: ['./history-filter.component.css']
15 | })
16 | export class HistoryFilterComponent implements AfterViewInit, OnDestroy {
17 |
18 | @ViewChild('dtStart') dtStartRef: ElementRef
19 | @ViewChild('dtEnd') dtEndRef: ElementRef
20 |
21 | @Output() onFilter = new EventEmitter()
22 |
23 | start: IMaterialDatepicker
24 | end: IMaterialDatepicker
25 |
26 | orderNumber
27 |
28 | valid = true
29 |
30 | ngAfterViewInit() {
31 | this.start = MaterialService.initDatepicker(this.dtStartRef, this.validateDate.bind(this))
32 | this.end = MaterialService.initDatepicker(this.dtEndRef, this.validateDate.bind(this))
33 | }
34 |
35 | validateDate() {
36 | if (this.start.date === null || this.end.date === null) {
37 | this.valid = true
38 | return
39 | }
40 |
41 | const s = moment(this.start.date)
42 | const e = moment(this.end.date)
43 |
44 | this.valid = s.isBefore(e)
45 | }
46 |
47 | submit() {
48 | const opts: Filter = {}
49 |
50 | if (this.start.date) {
51 | opts.start = this.start.date
52 | }
53 |
54 | if (this.end.date) {
55 | opts.end = this.end.date
56 | }
57 |
58 | if (this.orderNumber) {
59 | opts.order = this.orderNumber
60 | }
61 |
62 | this.onFilter.emit(opts)
63 | }
64 |
65 | ngOnDestroy() {
66 | this.start.destroy()
67 | this.end.destroy()
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/client/src/app/history-page/history-list/history-list.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/history-page/history-list/history-list.component.css
--------------------------------------------------------------------------------
/client/src/app/history-page/history-list/history-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | №
5 | Дата
6 | Время
7 | Сумма
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{order.order}}
15 | {{getOrderDate(order)}}
16 | {{getOrderTime(order)}}
17 | {{calculatePrice(order)}} руб.
18 |
19 |
20 | open_in_new
21 |
22 |
23 |
24 |
25 |
26 |
27 |
55 |
--------------------------------------------------------------------------------
/client/src/app/history-page/history-list/history-list.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild} from '@angular/core'
2 | import {Order} from '../../shared/interfaces'
3 | import {IMaterialInstance, MaterialService} from '../../shared/classes/material.service'
4 | import * as moment from 'moment'
5 |
6 | @Component({
7 | selector: 'app-history-list',
8 | templateUrl: './history-list.component.html',
9 | styleUrls: ['./history-list.component.css']
10 | })
11 | export class HistoryListComponent implements AfterViewInit, OnDestroy {
12 | @Input() orders: Order[]
13 | @ViewChild('modal') modalRef: ElementRef
14 |
15 | modal: IMaterialInstance
16 | selectedOrder: Order
17 |
18 | calculatePrice(order: Order): number {
19 | return order.list.reduce((total, item) => {
20 | return total += item.quantity * item.cost
21 | }, 0)
22 | }
23 |
24 | getOrderTime(order: Order): string {
25 | return moment(order.date).format('HH:mm:ss')
26 | }
27 |
28 | getOrderDate(order: Order): string {
29 | return moment(order.date).format('DD.MM.YYYY')
30 | }
31 |
32 | ngAfterViewInit() {
33 | this.modal = MaterialService.initModal(this.modalRef)
34 | }
35 |
36 | ngOnDestroy() {
37 | this.modal.destroy()
38 | }
39 |
40 | showOrderList(order: Order) {
41 | this.selectedOrder = order
42 | this.modal.open()
43 | }
44 |
45 | closeListModal() {
46 | this.modal.close()
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/app/history-page/history-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/history-page/history-page.component.css
--------------------------------------------------------------------------------
/client/src/app/history-page/history-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
История заказов
3 |
9 | filter_list
10 |
11 |
12 |
13 |
14 |
15 |
16 |
0; else empty">
17 |
18 |
19 |
20 |
26 | Загрузить еще
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Заказов нет
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/client/src/app/history-page/history-page.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'
2 | import {OrdersService} from '../shared/services/orders.service'
3 | import {Order} from '../shared/interfaces'
4 | import {Filter} from './history-filter/history-filter.component'
5 | import {Subscription} from 'rxjs/Subscription'
6 | import {IMaterialInstance, MaterialService} from '../shared/classes/material.service'
7 |
8 | const STEP = 2
9 | const LIMIT = 2
10 |
11 | @Component({
12 | selector: 'app-history-page',
13 | templateUrl: './history-page.component.html',
14 | styleUrls: ['./history-page.component.css']
15 | })
16 | export class HistoryPageComponent implements OnInit, OnDestroy, AfterViewInit {
17 |
18 | @ViewChild('tooltip') tooltipRef: ElementRef
19 | tooltip: IMaterialInstance
20 |
21 | orders: Order[] = []
22 |
23 | filterVisible = false
24 | reloading = false
25 | loading = false
26 | noMore = false
27 |
28 | limit = LIMIT
29 | offset = 0
30 |
31 | filter: Filter = {}
32 | oSub: Subscription
33 |
34 | constructor(private ordersService: OrdersService) {
35 | }
36 |
37 | ngOnInit() {
38 | this.reloading = true
39 | this.fetch()
40 | }
41 |
42 | ngOnDestroy() {
43 | this.oSub.unsubscribe()
44 | this.tooltip.destroy()
45 | }
46 |
47 | ngAfterViewInit() {
48 | this.tooltip = MaterialService.initTooltip(this.tooltipRef)
49 | }
50 |
51 | private fetch() {
52 | const params = Object.assign({}, this.filter, {
53 | limit: this.limit,
54 | offset: this.offset
55 | })
56 | this.oSub = this.ordersService.fetch(params).subscribe((orders: Order[]) => {
57 | this.orders = this.orders.concat(orders)
58 | this.noMore = orders.length < STEP
59 | this.loading = false
60 | this.reloading = false
61 | })
62 | }
63 |
64 | loadMore() {
65 | this.offset += STEP
66 | this.loading = true
67 | this.fetch()
68 | }
69 |
70 | applyFilter(filter: Filter) {
71 | this.orders = []
72 | this.limit = LIMIT
73 | this.offset = 0
74 | this.filter = filter
75 | this.reloading = true
76 | this.fetch()
77 | }
78 |
79 | isFiltered(): boolean {
80 | return Object.keys(this.filter).length !== 0
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/client/src/app/login-page/login-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/login-page/login-page.component.css
--------------------------------------------------------------------------------
/client/src/app/login-page/login-page.component.html:
--------------------------------------------------------------------------------
1 |
47 |
--------------------------------------------------------------------------------
/client/src/app/login-page/login-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnDestroy, OnInit} from '@angular/core'
2 | import {FormControl, FormGroup, Validators} from '@angular/forms'
3 | import {AuthService} from '../shared/services/auth.service'
4 | import {ActivatedRoute, Params, Router} from '@angular/router'
5 | import {MaterialService} from '../shared/classes/material.service'
6 | import {Subscription} from 'rxjs/Subscription'
7 |
8 | @Component({
9 | selector: 'app-login-page',
10 | templateUrl: './login-page.component.html',
11 | styleUrls: ['./login-page.component.css']
12 | })
13 | export class LoginPageComponent implements OnInit, OnDestroy {
14 | form: FormGroup
15 |
16 | aSub: Subscription
17 |
18 | constructor(private auth: AuthService,
19 | private route: ActivatedRoute,
20 | private router: Router) {
21 | }
22 |
23 | ngOnInit() {
24 | this.form = new FormGroup({
25 | email: new FormControl(null, [Validators.required, Validators.email]),
26 | password: new FormControl(null, [Validators.required, Validators.minLength(6)]),
27 | })
28 |
29 | this.route.queryParams.subscribe((params: Params) => {
30 | if (params['registered']) {
31 | MaterialService.toast('Теперь вы можете зайти использую свои данные')
32 | } else if (params['accessDenied']) {
33 | MaterialService.toast('Для начала авторизуйтесь')
34 | }
35 | })
36 | }
37 |
38 | ngOnDestroy() {
39 | if (this.aSub) {
40 | this.aSub.unsubscribe()
41 | }
42 | }
43 |
44 | onSubmit() {
45 | this.form.disable()
46 | this.aSub = this.auth.login(this.form.value).subscribe(
47 | () => this.router.navigate(['/overview']),
48 | error => {
49 | MaterialService.toast(error.error.message)
50 | this.form.enable()
51 | }
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/new-order-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/new-order-page/new-order-page.component.css
--------------------------------------------------------------------------------
/client/src/app/new-order-page/new-order-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Заказ
4 |
5 | Заказ
6 | keyboard_arrow_right
7 | Добавить продукцию
8 |
9 |
10 |
11 | Завершить
12 |
13 |
14 |
15 |
16 |
17 |
18 |
57 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/new-order-page.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'
2 | import {OrderService} from './order.service'
3 | import {NavigationEnd, Router} from '@angular/router'
4 | import {IMaterialInstance, MaterialService} from '../shared/classes/material.service'
5 | import {OrdersService} from '../shared/services/orders.service'
6 | import {Subscription} from 'rxjs/Subscription'
7 |
8 | @Component({
9 | selector: 'app-new-order-page',
10 | templateUrl: './new-order-page.component.html',
11 | styleUrls: ['./new-order-page.component.css'],
12 | providers: [OrderService]
13 | })
14 | export class NewOrderPageComponent implements OnInit, OnDestroy, AfterViewInit {
15 |
16 | @ViewChild('modal') modalRef: ElementRef
17 | modal: IMaterialInstance
18 | isRoot: boolean
19 | pending = false
20 |
21 | oSub: Subscription
22 |
23 | constructor(private router: Router,
24 | public order: OrderService,
25 | private ordersService: OrdersService) {
26 | }
27 |
28 | ngOnInit() {
29 | this.isRoot = this.router.url === '/order'
30 | this.router.events.subscribe(event => {
31 | if (event instanceof NavigationEnd) {
32 | this.isRoot = this.router.url === '/order'
33 | }
34 | })
35 | }
36 |
37 | ngOnDestroy() {
38 | this.modal.destroy()
39 | if (this.oSub) {
40 | this.oSub.unsubscribe()
41 | }
42 | }
43 |
44 | ngAfterViewInit() {
45 | this.modal = MaterialService.initModal(this.modalRef)
46 | }
47 |
48 | openModal() {
49 | this.modal.open()
50 | }
51 |
52 | cancel() {
53 | this.modal.close()
54 | }
55 |
56 | submit() {
57 | this.pending = true
58 | const list = this.order.list.map(i => {
59 | delete i._id
60 | return i
61 | })
62 | this.oSub = this.ordersService.create({list}).subscribe(
63 | order => {
64 | this.order.clear()
65 | this.pending = false
66 | this.modal.close()
67 | MaterialService.toast(`Заказ №${order.order} добавлен`)
68 | },
69 | error => {
70 | MaterialService.toast(error.error.message)
71 | this.pending = false
72 | }
73 | )
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order-categories/order-categories.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/new-order-page/order-categories/order-categories.component.css
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order-categories/order-categories.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
15 |
{{category.name}}
16 |
17 |
18 |
19 |
20 | Категорий пока нет
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order-categories/order-categories.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core'
2 | import {Observable} from 'rxjs/Observable'
3 | import {CategoriesService} from '../../shared/services/categories.service'
4 | import {Category} from '../../shared/interfaces'
5 |
6 | @Component({
7 | selector: 'app-order-categories',
8 | templateUrl: './order-categories.component.html',
9 | styleUrls: ['./order-categories.component.css']
10 | })
11 | export class OrderCategoriesComponent implements OnInit {
12 |
13 | categories$: Observable
14 |
15 | constructor(private categoriesService: CategoriesService) {
16 | }
17 |
18 | ngOnInit() {
19 | this.categories$ = this.categoriesService.fetch()
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order-production/order-production.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/new-order-page/order-production/order-production.component.css
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order-production/order-production.component.html:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 | Позиций пока нет
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order-production/order-production.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core'
2 | import {PositionsService} from '../../shared/services/positions.service'
3 | import {Observable} from 'rxjs/Observable'
4 | import {OrderListItem, Position} from '../../shared/interfaces'
5 | import {ActivatedRoute, Params} from '@angular/router'
6 | import {switchMap, map} from 'rxjs/operators'
7 | import {OrderService} from '../order.service'
8 | import {MaterialService} from '../../shared/classes/material.service'
9 |
10 | @Component({
11 | selector: 'app-order-production',
12 | templateUrl: './order-production.component.html',
13 | styleUrls: ['./order-production.component.css']
14 | })
15 | export class OrderProductionComponent implements OnInit {
16 |
17 | items$: Observable
18 |
19 | constructor(private positionsService: PositionsService,
20 | private route: ActivatedRoute,
21 | public order: OrderService) {
22 | }
23 |
24 | ngOnInit() {
25 | this.items$ = this.route.params
26 | .pipe(
27 | switchMap((params: Params) => {
28 | return this.positionsService.fetch(params['id'])
29 | }),
30 | map((positions: Position[]) => {
31 | return positions.map(position => {
32 | return {
33 | name: position.name,
34 | cost: position.cost,
35 | _id: position._id,
36 | quantity: 1
37 | }
38 | })
39 | })
40 | )
41 | }
42 |
43 | add(item: OrderListItem) {
44 | this.order.add(Object.assign({}, item))
45 | MaterialService.toast(`Добавлено x${item.quantity}`)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/client/src/app/new-order-page/order.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core'
2 | import {OrderListItem} from '../shared/interfaces'
3 |
4 | @Injectable()
5 | export class OrderService {
6 | public list: OrderListItem[] = []
7 | public price = 0
8 |
9 | add(item: OrderListItem) {
10 | const possible = this.list.find(i => i._id === item._id)
11 | if (possible) {
12 | possible.quantity += item.quantity
13 | } else {
14 | this.list.push(item)
15 | }
16 | this.computePrice()
17 | }
18 |
19 | remove(id: string) {
20 | const idx = this.list.findIndex(i => i._id === id)
21 | this.list.splice(idx, 1)
22 | this.computePrice()
23 | }
24 |
25 | clear() {
26 | this.list = []
27 | this.price = 0
28 | }
29 |
30 | private computePrice() {
31 | this.price = this.list.reduce((total, item) => {
32 | return total += item.quantity * item.cost
33 | }, 0)
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/app/overview-page/overview-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/overview-page/overview-page.component.css
--------------------------------------------------------------------------------
/client/src/app/overview-page/overview-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Обзор за вчера ({{date}})
4 | info_outline
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
Выручка:
16 |
{{data.gain.yesterday}} руб.
17 |
24 |
25 | {{data.gain.isHigher ? 'arrow_upward': 'arrow_downward'}}
26 |
27 | {{data.gain.percent}}%
28 |
29 |
30 | Выручка вашего бизнеса вчера на
31 | {{data.gain.percent}}% {{data.gain.isHigher ? 'выше' : 'ниже'}}
32 | среднего: {{data.gain.compare}} руб. в день
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Заказы:
42 |
{{data.orders.yesterday}} зак.
43 |
50 |
51 | {{data.orders.isHigher ? 'arrow_upward': 'arrow_downward'}}
52 |
53 | {{data.orders.percent}}%
54 |
55 |
56 | Число заказов вчера на
57 | {{data.orders.percent}}% {{data.orders.isHigher ? 'выше' : 'ниже'}}
58 | среднего значения: {{data.orders.compare}} зак. в день
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
Зачем нужна эта страница?
73 |
Страница “Обзор” покажет динамику продаж за предыдущий день. Сравнение со средним значениями поможет вам понять, как идут дела у Вашего бизнеса.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/client/src/app/overview-page/overview-page.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'
2 | import {MaterialService, IMaterialInstance} from '../shared/classes/material.service'
3 | import * as moment from 'moment'
4 | import {AnalyticsService} from '../shared/services/analytics.service'
5 | import {Observable} from 'rxjs/Observable'
6 | import {Overview} from '../shared/interfaces'
7 |
8 | @Component({
9 | selector: 'app-overview-page',
10 | templateUrl: './overview-page.component.html',
11 | styleUrls: ['./overview-page.component.css']
12 | })
13 | export class OverviewPageComponent implements OnInit, AfterViewInit, OnDestroy {
14 | date: string
15 | tapTarget: IMaterialInstance
16 |
17 | @ViewChild('tapTarget') tapTargetEl: ElementRef
18 |
19 | data$: Observable
20 |
21 | constructor(public analyticsService: AnalyticsService) {
22 | }
23 |
24 | ngOnInit() {
25 | this.date = moment().add(-1, 'd').format('DD.MM.YYYY')
26 | this.data$ = this.analyticsService.fetchOverview()
27 | }
28 |
29 | ngAfterViewInit() {
30 | this.tapTarget = MaterialService.tapTarget(this.tapTargetEl)
31 | }
32 |
33 | ngOnDestroy() {
34 | this.tapTarget.destroy()
35 | }
36 |
37 | showInfo() {
38 | this.tapTarget.open()
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/app/products-page/product-form-page/positions-form/positions-form.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/products-page/product-form-page/positions-form/positions-form.component.css
--------------------------------------------------------------------------------
/client/src/app/products-page/product-form-page/positions-form/positions-form.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Позиции:
5 |
9 | Добавить позицию
10 |
11 |
12 |
13 |
14 |
28 |
29 | Позиций пока нет
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
94 |
95 |
--------------------------------------------------------------------------------
/client/src/app/products-page/product-form-page/positions-form/positions-form.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'
2 | import {PositionsService} from '../../../shared/services/positions.service'
3 | import {Position} from '../../../shared/interfaces'
4 | import {IMaterialInstance, MaterialService} from '../../../shared/classes/material.service'
5 | import {FormControl, FormGroup, Validators} from '@angular/forms'
6 | import {Subscription} from 'rxjs/Subscription'
7 |
8 | @Component({
9 | selector: 'app-positions-form',
10 | templateUrl: './positions-form.component.html',
11 | styleUrls: ['./positions-form.component.css']
12 | })
13 | export class PositionsFormComponent implements OnInit, AfterViewInit, OnDestroy {
14 | @ViewChild('modal') modalRef: ElementRef
15 | @Input() categoryId: string
16 |
17 | pSub: Subscription
18 |
19 | modal: IMaterialInstance = null
20 | form: FormGroup
21 |
22 | positions: Position[]
23 | positionId: string = null
24 | loading = true
25 |
26 | constructor(private positionsService: PositionsService) {
27 | }
28 |
29 | ngOnInit() {
30 | this.pSub = this.positionsService.fetch(this.categoryId).subscribe(positions => {
31 | this.positions = positions
32 | this.loading = false
33 | })
34 |
35 | this.form = new FormGroup({
36 | name: new FormControl(null, Validators.required),
37 | cost: new FormControl(null, [Validators.required, Validators.min(1)])
38 | })
39 | }
40 |
41 | ngOnDestroy() {
42 | if (this.pSub) {
43 | this.pSub.unsubscribe()
44 | }
45 | this.modal.destroy()
46 | }
47 |
48 | ngAfterViewInit() {
49 | this.modal = MaterialService.initModal(this.modalRef)
50 | }
51 |
52 | onSelectPosition(position: Position) {
53 | this.positionId = position._id
54 | this.form.patchValue({
55 | name: position.name,
56 | cost: position.cost
57 | })
58 | this.modal.open()
59 | MaterialService.updateTextInput()
60 | }
61 |
62 | addPosition() {
63 | this.positionId = null
64 | this.form.patchValue({
65 | name: null,
66 | cost: null
67 | })
68 | this.modal.open()
69 | MaterialService.updateTextInput()
70 | }
71 |
72 | removePosition(event, position: Position) {
73 | event.stopPropagation()
74 | const decision = window.confirm('Вы уверены, что хотите удалить позицию?')
75 | if (decision) {
76 | this.positionsService.remove(position._id).subscribe(
77 | response => {
78 | const idx = this.positions.findIndex(p => p._id !== position._id)
79 | this.positions.splice(idx, 1)
80 | MaterialService.toast(response.message)
81 | },
82 | error => MaterialService.toast(error.error.message)
83 | )
84 | }
85 | }
86 |
87 | onCancel() {
88 | this.modal.close()
89 | this.form.reset({name: '', cost: 0})
90 | }
91 |
92 | onSubmit() {
93 | this.form.disable()
94 |
95 | const position: Position = {
96 | name: this.form.value.name,
97 | cost: this.form.value.cost,
98 | category: this.categoryId
99 | }
100 |
101 | if (this.positionId) {
102 | position._id = this.positionId
103 | this.positionsService.update(position).subscribe(
104 | pos => {
105 | const idx = this.positions.findIndex(p => p._id === pos._id)
106 | this.positions[idx] = pos
107 | MaterialService.toast('Изменения сохранены')
108 | },
109 | error => {
110 | this.form.enable()
111 | MaterialService.toast(error.error.message)
112 | },
113 | () => {
114 | this.modal.close()
115 | this.form.reset({name: '', cost: 0})
116 | this.form.enable()
117 | }
118 | )
119 | } else {
120 | this.positionsService.create(position).subscribe(
121 | pos => {
122 | this.positions.push(pos)
123 | MaterialService.toast('Изменения сохранены')
124 | },
125 | error => {
126 | this.form.enable()
127 | MaterialService.toast(error.error.message)
128 | },
129 | () => {
130 | this.modal.close()
131 | this.form.reset({name: '', cost: 0})
132 | this.form.enable()
133 | }
134 | )
135 | }
136 | }
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/client/src/app/products-page/product-form-page/product-form-page.component.css:
--------------------------------------------------------------------------------
1 | .h200 {
2 | height: 200px;
3 | }
4 |
5 | .dn {
6 | display: none;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/app/products-page/product-form-page/product-form-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Категории
4 | keyboard_arrow_right
5 | {{isNew ? 'Добавить' : 'Редактировать'}}
6 |
7 |
8 |
13 | delete
14 |
15 |
16 |
17 |
18 |
19 |
66 |
67 |
68 |
73 |
74 |
75 |
76 |
80 |
--------------------------------------------------------------------------------
/client/src/app/products-page/product-form-page/product-form-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'
2 | import {ActivatedRoute, Params, Router} from '@angular/router'
3 | import {FormControl, FormGroup, Validators} from '@angular/forms'
4 | import {CategoriesService} from '../../shared/services/categories.service'
5 | import {switchMap} from 'rxjs/operators/switchMap'
6 | import {of} from 'rxjs/observable/of'
7 | import {MaterialService} from '../../shared/classes/material.service'
8 | import {Category} from '../../shared/interfaces'
9 |
10 | @Component({
11 | selector: 'app-product-form-page',
12 | templateUrl: './product-form-page.component.html',
13 | styleUrls: ['./product-form-page.component.css']
14 | })
15 | export class ProductFormPageComponent implements OnInit {
16 |
17 | @ViewChild('input') input: ElementRef
18 | form: FormGroup
19 | image: File
20 | imagePreview = ''
21 |
22 | isNew = true
23 |
24 | category: Category
25 |
26 | constructor(private route: ActivatedRoute,
27 | private categoriesService: CategoriesService,
28 | private router: Router) {
29 | }
30 |
31 | ngOnInit() {
32 | this.form = new FormGroup({
33 | name: new FormControl(null, Validators.required)
34 | })
35 |
36 | this.form.disable()
37 |
38 | this.route.params
39 | .pipe(switchMap((params: Params) => {
40 | if (params['id']) {
41 | this.isNew = false
42 | return this.categoriesService.getById(params['id'])
43 | }
44 | return of(null)
45 | }))
46 | .subscribe(
47 | category => {
48 | if (category) {
49 | this.category = category
50 | this.form.patchValue({
51 | name: this.category.name
52 | })
53 | this.imagePreview = this.category.imageSrc
54 | MaterialService.updateTextInput()
55 | }
56 | this.form.enable()
57 | },
58 | error => MaterialService.toast(error.error.message)
59 | )
60 | }
61 |
62 | onSubmit() {
63 | let obs$
64 | this.form.disable()
65 | if (this.isNew) {
66 | obs$ = this.categoriesService.create(this.form.value.name, this.image)
67 | } else {
68 | obs$ = this.categoriesService.update(this.category._id, this.form.value.name, this.image)
69 | }
70 |
71 | obs$.subscribe(
72 | category => {
73 | this.category = category
74 | MaterialService.toast('Изменения сохранены')
75 | this.form.enable()
76 | },
77 | error => {
78 | MaterialService.toast(error.error.message)
79 | this.form.enable()
80 | }
81 | )
82 | }
83 |
84 | deleteCategory() {
85 | const decision = window.confirm('Вы уверены, что хотите удалить категорию?')
86 | if (decision) {
87 | this.categoriesService.delete(this.category._id).subscribe(
88 | response => MaterialService.toast(response.message),
89 | error => MaterialService.toast(error.error.message),
90 | () => this.router.navigate(['/categories'])
91 | )
92 | }
93 | }
94 |
95 | onFileSelect(event) {
96 | const file = event.target.files[0]
97 | this.image = file
98 |
99 | const reader = new FileReader()
100 |
101 | reader.onload = () => {
102 | this.imagePreview = reader.result
103 | }
104 |
105 | reader.readAsDataURL(file)
106 | }
107 |
108 | triggerClick() {
109 | this.input.nativeElement.click()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/client/src/app/products-page/products-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/products-page/products-page.component.css
--------------------------------------------------------------------------------
/client/src/app/products-page/products-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Категории
3 | Добавить категорию
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 | У вас нет ни одной категории
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/client/src/app/products-page/products-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core'
2 | import {CategoriesService} from '../shared/services/categories.service'
3 | import {Observable} from 'rxjs/Observable'
4 | import {Category} from '../shared/interfaces'
5 |
6 | @Component({
7 | selector: 'app-products-page',
8 | templateUrl: './products-page.component.html',
9 | styleUrls: ['./products-page.component.css']
10 | })
11 | export class ProductsPageComponent implements OnInit {
12 |
13 | categories$: Observable
14 |
15 | constructor(private categoriesService: CategoriesService) {
16 | }
17 |
18 | ngOnInit() {
19 | this.categories$ = this.categoriesService.fetch()
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/app/registration-page/registration-page.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/registration-page/registration-page.component.css
--------------------------------------------------------------------------------
/client/src/app/registration-page/registration-page.component.html:
--------------------------------------------------------------------------------
1 |
56 |
--------------------------------------------------------------------------------
/client/src/app/registration-page/registration-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnDestroy, OnInit} from '@angular/core'
2 | import {FormControl, FormGroup, Validators} from '@angular/forms'
3 | import {AuthService} from '../shared/services/auth.service'
4 | import {Router} from '@angular/router'
5 | import {MaterialService} from '../shared/classes/material.service'
6 | import {Subscription} from 'rxjs/Subscription'
7 |
8 | @Component({
9 | selector: 'app-registration-page',
10 | templateUrl: './registration-page.component.html',
11 | styleUrls: ['./registration-page.component.css']
12 | })
13 | export class RegistrationPageComponent implements OnInit, OnDestroy {
14 | form: FormGroup
15 | aSub: Subscription
16 |
17 | constructor(private auth: AuthService, private router: Router) {
18 | }
19 |
20 | ngOnInit() {
21 | this.form = new FormGroup({
22 | email: new FormControl(null, [Validators.required, Validators.email]),
23 | password: new FormControl(null, [Validators.required, Validators.minLength(6)]),
24 | })
25 | }
26 |
27 | ngOnDestroy() {
28 | if (this.aSub) {
29 | this.aSub.unsubscribe()
30 | }
31 | }
32 |
33 | onSubmit() {
34 | this.form.disable()
35 | this.aSub = this.auth.register(this.form.value).subscribe(
36 | response => {
37 | this.router.navigate(['/login'], {
38 | queryParams: {
39 | 'registered': true
40 | }
41 | })
42 | },
43 | error => {
44 | MaterialService.toast(error.error.message)
45 | this.form.enable()
46 | }
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/app/shared/classes/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot} from '@angular/router'
2 | import {Injectable} from '@angular/core'
3 | import {Observable} from 'rxjs/Observable'
4 | import {AuthService} from '../services/auth.service'
5 | import {of} from 'rxjs/observable/of'
6 |
7 | @Injectable()
8 | export class AuthGuard implements CanActivate, CanActivateChild {
9 | constructor(private auth: AuthService, private router: Router) {
10 | }
11 |
12 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean {
13 | if (this.auth.isAuthenticated()) {
14 | return of(true)
15 | } else {
16 | this.router.navigate(['/login'], {
17 | queryParams: {
18 | accessDenied: true
19 | }
20 | })
21 | return of(false)
22 | }
23 | }
24 |
25 | canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean {
26 | return this.canActivate(route, state)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/app/shared/classes/material.service.ts:
--------------------------------------------------------------------------------
1 | import {ElementRef} from '@angular/core'
2 |
3 | declare var M
4 |
5 | export interface IMaterialInstance {
6 | open?()
7 | destroy?()
8 | close?()
9 | }
10 |
11 | export interface IMaterialDatepicker extends IMaterialInstance {
12 | toString?()
13 | setDate?(value: Date)
14 | date?: Date
15 | }
16 |
17 | export class MaterialService {
18 | static toast(message: string) {
19 | M.toast({html: message})
20 | }
21 |
22 | static tapTarget(ref: ElementRef): IMaterialInstance {
23 | return M.TapTarget.init(ref.nativeElement)
24 | }
25 |
26 | static initializeFloatingButton(ref: ElementRef) {
27 | M.FloatingActionButton.init(ref.nativeElement)
28 | }
29 |
30 | static updateTextInput() {
31 | M.updateTextFields()
32 | }
33 |
34 | static initModal(ref: ElementRef): IMaterialInstance {
35 | return M.Modal.init(ref.nativeElement)
36 | }
37 |
38 | static initDatepicker(ref: ElementRef, onClose?: () => void): IMaterialDatepicker {
39 | return M.Datepicker.init(ref.nativeElement, {
40 | showClearBtn: true,
41 | format: 'dd.mm.yyyy',
42 | onClose
43 | })
44 | }
45 |
46 | static initTooltip(ref: ElementRef): IMaterialInstance {
47 | return M.Tooltip.init(ref.nativeElement)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/app/shared/classes/token.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'
2 | import {Injectable} from '@angular/core'
3 | import {Observable} from 'rxjs/Observable'
4 | import {AuthService} from '../services/auth.service'
5 |
6 | @Injectable()
7 | export class TokenInterceptor implements HttpInterceptor {
8 | constructor(private auth: AuthService) {}
9 |
10 | intercept(req: HttpRequest, next: HttpHandler): Observable> {
11 | if (this.auth.isAuthenticated()) {
12 | req = req.clone({
13 | setHeaders: {
14 | Authorization: this.auth.getToken()
15 | }
16 | })
17 | }
18 |
19 | return next.handle(req)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/app/shared/components/loader/loader.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/shared/components/loader/loader.component.css
--------------------------------------------------------------------------------
/client/src/app/shared/components/loader/loader.component.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/client/src/app/shared/components/loader/loader.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-loader',
5 | templateUrl: './loader.component.html',
6 | styleUrls: ['./loader.component.css']
7 | })
8 | export class LoaderComponent implements OnInit {
9 |
10 | constructor() { }
11 |
12 | ngOnInit() {
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/app/shared/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | _id?: string
3 | password: string
4 | email: string
5 | }
6 |
7 | export interface Category {
8 | _id?: string
9 | imageSrc?: string
10 | name: string
11 | user?: string
12 | }
13 |
14 | export interface Position {
15 | _id?: string
16 | cost: number
17 | name: string
18 | category: string
19 | user?: string
20 | }
21 |
22 | export interface Message {
23 | message: string
24 | }
25 |
26 | export interface Order {
27 | _id?: string
28 | date?: Date
29 | order?: number
30 | user?: string
31 | list: OrderListItem[]
32 | }
33 |
34 | export interface OrderListItem {
35 | _id?: string
36 | name: string
37 | quantity: number
38 | cost: number
39 | }
40 |
41 | export interface Overview {
42 | gain: OverviewItem
43 | orders: OverviewItem
44 | }
45 |
46 | export interface Analytics {
47 | chart: AnalyticsChart[]
48 | average: number
49 | }
50 |
51 | export interface AnalyticsChart {
52 | gain: number
53 | order: number
54 | label: string
55 | }
56 |
57 | export interface OverviewItem {
58 | percent: number
59 | compare: number
60 | yesterday: number
61 | isHigher: boolean
62 | }
63 |
--------------------------------------------------------------------------------
/client/src/app/shared/layouts/empty-layout/empty-layout.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/shared/layouts/empty-layout/empty-layout.component.css
--------------------------------------------------------------------------------
/client/src/app/shared/layouts/empty-layout/empty-layout.component.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/src/app/shared/layouts/empty-layout/empty-layout.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit, ViewEncapsulation} from '@angular/core'
2 |
3 | @Component({
4 | selector: 'app-empty-layout',
5 | templateUrl: './empty-layout.component.html',
6 | styleUrls: ['./empty-layout.component.css'],
7 | encapsulation: ViewEncapsulation.None
8 | })
9 | export class EmptyLayoutComponent implements OnInit {
10 |
11 | constructor() { }
12 |
13 | ngOnInit() {
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/app/shared/layouts/site-layout/site-layout.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/app/shared/layouts/site-layout/site-layout.component.css
--------------------------------------------------------------------------------
/client/src/app/shared/layouts/site-layout/site-layout.component.html:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
29 | info
30 |
31 |
--------------------------------------------------------------------------------
/client/src/app/shared/layouts/site-layout/site-layout.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, ViewChild, ViewEncapsulation} from '@angular/core'
2 | import {AuthService} from '../../services/auth.service'
3 | import {Router} from '@angular/router'
4 | import {MaterialService} from '../../classes/material.service'
5 |
6 | @Component({
7 | selector: 'app-site-layout',
8 | templateUrl: './site-layout.component.html',
9 | styleUrls: ['./site-layout.component.css'],
10 | encapsulation: ViewEncapsulation.None
11 | })
12 | export class SiteLayoutComponent implements AfterViewInit {
13 |
14 | @ViewChild('actionBtn') actionBtnEl: ElementRef
15 |
16 | links = [
17 | {url: '/overview', name: 'Обзор'},
18 | {url: '/analytics', name: 'Аналитика'},
19 | {url: '/history', name: 'История'},
20 | {url: '/order', name: 'Новый заказ'},
21 | {url: '/categories', name: 'Ассортимент'}
22 | ]
23 |
24 | constructor(private auth: AuthService, private router: Router) {
25 | }
26 |
27 | logout(event) {
28 | event.preventDefault()
29 | this.auth.logout()
30 | this.router.navigate(['/login'])
31 | }
32 |
33 | ngAfterViewInit() {
34 | MaterialService.initializeFloatingButton(this.actionBtnEl)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/analytics.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core'
2 | import {HttpClient} from '@angular/common/http'
3 | import {Observable} from 'rxjs/Observable'
4 | import {Analytics, Overview} from '../interfaces'
5 |
6 | @Injectable()
7 | export class AnalyticsService {
8 | constructor(private http: HttpClient) {}
9 |
10 | fetchOverview(): Observable {
11 | return this.http.get('/api/analytics/overview')
12 | }
13 |
14 | fetchAnalytics(): Observable {
15 | return this.http.get('/api/analytics/analytics')
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/auth.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core'
2 | import {User} from '../interfaces'
3 | import {Observable} from 'rxjs/Observable'
4 | import {tap} from 'rxjs/operators'
5 | import {HttpClient} from '@angular/common/http'
6 |
7 | @Injectable()
8 | export class AuthService {
9 |
10 | private token = null
11 |
12 | constructor(private http: HttpClient) {
13 | }
14 |
15 | login(user: User): Observable<{token: string}> {
16 | return this.http.post<{token: string}>('/api/auth/login', user)
17 | .pipe(tap(({token}) => {
18 | localStorage.setItem('auth-token', token)
19 | this.setToken(token)
20 | }))
21 | }
22 |
23 | register(user: User): Observable {
24 | return this.http.post('/api/auth/register', user)
25 | }
26 |
27 | isAuthenticated(): boolean {
28 | return !!this.token
29 | }
30 |
31 | logout() {
32 | this.token = null
33 | localStorage.clear()
34 | }
35 |
36 | setToken(token: string) {
37 | this.token = token
38 | }
39 |
40 | getToken(): string {
41 | return this.token
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/categories.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core'
2 | import {Observable} from 'rxjs/Observable'
3 | import {HttpClient} from '@angular/common/http'
4 | import {Category, Message} from '../interfaces'
5 |
6 | @Injectable()
7 | export class CategoriesService {
8 | constructor(private http: HttpClient) {}
9 |
10 | create(name: string, image?: File): Observable {
11 | const fd = new FormData()
12 |
13 | if (image) {
14 | fd.append('image', image, image.name)
15 | }
16 | fd.append('name', name)
17 |
18 | return this.http.post('/api/category', fd)
19 | }
20 |
21 | update(id: string, name: string, image?: File): Observable {
22 | const fd = new FormData()
23 |
24 | if (image) {
25 | fd.append('image', image, image.name)
26 | }
27 | fd.append('name', name)
28 | return this.http.patch(`/api/category/${id}`, fd)
29 | }
30 |
31 | getById(id: string): Observable {
32 | return this.http.get(`/api/category/${id}`)
33 | }
34 |
35 | fetch(): Observable {
36 | return this.http.get('/api/category')
37 | }
38 |
39 | delete(id: string): Observable {
40 | return this.http.delete(`/api/category/${id}`)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/orders.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core'
2 | import {Observable} from 'rxjs/Observable'
3 | import {HttpClient, HttpParams} from '@angular/common/http'
4 | import {Order} from '../interfaces'
5 |
6 | @Injectable()
7 | export class OrdersService {
8 | constructor(private http: HttpClient) {}
9 |
10 | create(order: Order): Observable {
11 | return this.http.post('/api/order', order)
12 | }
13 |
14 | fetch(params: any = {}): Observable {
15 | return this.http.get(`/api/order`, {
16 | params: new HttpParams({fromObject: params})
17 | })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/positions.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core'
2 | import {HttpClient} from '@angular/common/http'
3 | import {Message, Position} from '../interfaces'
4 | import {Observable} from 'rxjs/Observable'
5 |
6 | @Injectable()
7 | export class PositionsService {
8 | constructor(private http: HttpClient) {}
9 |
10 | fetch(categoryId: string): Observable {
11 | return this.http.get(`/api/position/${categoryId}`)
12 | }
13 |
14 | create(position: Position): Observable {
15 | return this.http.post('/api/position', position)
16 | }
17 |
18 | remove(id: string): Observable {
19 | return this.http.delete(`/api/position/${id}`)
20 | }
21 |
22 | update(position: Position): Observable {
23 | return this.http.patch(`/api/position/${position._id}`, {
24 | name: position.name,
25 | cost: position.cost
26 | })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/assets/.gitkeep
--------------------------------------------------------------------------------
/client/src/assets/cake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/assets/cake.jpg
--------------------------------------------------------------------------------
/client/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/client/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/client/src/favicon.ico
--------------------------------------------------------------------------------
/client/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FullstackNgFrontend
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import {enableProdMode} from '@angular/core'
2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'
3 |
4 | import {AppModule} from './app/app.module'
5 | import {environment} from './environments/environment'
6 |
7 | import 'materialize-css/dist/js/materialize.js'
8 |
9 | if (environment.production) {
10 | enableProdMode()
11 | }
12 |
13 | platformBrowserDynamic().bootstrapModule(AppModule)
14 | .catch(err => console.log(err))
15 |
--------------------------------------------------------------------------------
/client/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Required to support Web Animations `@angular/platform-browser/animations`.
51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
52 | **/
53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
54 |
55 | /**
56 | * By default, zone.js will patch all possible macroTask and DomEvents
57 | * user can disable parts of macroTask/DomEvents patch by setting following flags
58 | */
59 |
60 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
61 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
62 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
63 |
64 | /*
65 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
66 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
67 | */
68 | // (window as any).__Zone_enable_cross_context_check = true;
69 |
70 | /***************************************************************************************************
71 | * Zone JS is required by default for Angular itself.
72 | */
73 | import 'zone.js/dist/zone'; // Included with Angular CLI.
74 |
75 |
76 |
77 | /***************************************************************************************************
78 | * APPLICATION IMPORTS
79 | */
80 |
--------------------------------------------------------------------------------
/client/src/styles.css:
--------------------------------------------------------------------------------
1 | /*@import "theme/materialize.min.css";*/
2 | @import "~materialize-css/dist/css/materialize.min.css";
3 | @import "theme/styles.css";
4 |
--------------------------------------------------------------------------------
/client/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/client/src/theme/styles.css:
--------------------------------------------------------------------------------
1 | .a-sidenav{transform:translateX(0)!important;width:250px!important}.a-sidenav h4{padding:0 30px;opacity:.8;font-size:2rem}.a-sidenav .bold.last{position:absolute;bottom:62px;width:100%}main,footer{margin-left:250px}.content{padding:0 30px}.m0{margin:0}.mr1{margin-right:10px}.mb2{margin-bottom:20px}.mb1{margin-bottom:10px}.p10{padding:10px!important}.pointer{cursor:pointer}.order-row{flex-wrap:wrap}.order-row .card{width:30%;min-width:175px;margin-right:20px}.order-img{height:100px!important}.frow{display:flex;margin-bottom:20px;margin-left:auto;margin-right:auto}.page-title{display:flex;justify-content:space-between;align-items:center;padding:25px 0}.page-title a{color:black;opacity:.8}#create-modal{max-width:500px}.page-subtitle{display:flex;justify-content:space-between;align-items:center;padding:15px 0}.page-title h3,.page-title h4{margin:0;font-size:2.3rem}.page-subtitle h4{margin:0;font-size:2rem}.order-summary{display:flex;justify-content:flex-end;font-size:20px;padding-right:30px}.order-summary p{margin-bottom:0!important}.order-position-input{margin-top:0;margin-bottom:0}.order-position-input input{margin-bottom:0!important}.fr{display:flex;margin-bottom:15px}.fr .col.order{width:120px}.fr .col.filter-pickers{padding:0 20px;width:40%;display:flex}.fr .col.filter-pickers .input-field{width:45%!important;min-width:120px;margin-right:20px!important;margin-top:0!important;margin-bottom:0!important}.fr .col.range{width:40%;padding-top:44px}.filter{margin-bottom:15px}.hide{display:none!important}.tap-target{background-color:#e0e0e0!important}.btn-floating.tap-target-origin{background-color:#bdbdbd!important}.auth-block{display:flex;justify-content:center;align-items:center;padding-top:50px}.auth-block .card{width:400px}.nav-wrapper{padding-left:20px}.collection-item-icon{display:flex!important;justify-content:space-between;align-items:center;cursor:pointer;z-index:10}.collection-item-icon i.material-icons:hover{color:#f44336!important;transition:.3s color}.pl0{padding-left:0!important}.average-price{padding:15px 0;font-size:20px}.analytics-block{width:100%;height:300px}.pb3{padding-bottom:30px}
2 |
--------------------------------------------------------------------------------
/client/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "es2015",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "types": [
8 | "jasmine",
9 | "node"
10 | ]
11 | },
12 | "files": [
13 | "test.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs",
22 | "rxjs/Rx"
23 | ],
24 | "import-spacing": true,
25 | "indent": [
26 | true,
27 | "spaces"
28 | ],
29 | "interface-over-type-literal": true,
30 | "label-position": true,
31 | "max-line-length": [
32 | true,
33 | 140
34 | ],
35 | "member-access": false,
36 | "member-ordering": [
37 | true,
38 | {
39 | "order": [
40 | "static-field",
41 | "instance-field",
42 | "static-method",
43 | "instance-method"
44 | ]
45 | }
46 | ],
47 | "no-arg": true,
48 | "no-bitwise": true,
49 | "no-console": [
50 | true,
51 | "debug",
52 | "info",
53 | "time",
54 | "timeEnd",
55 | "trace"
56 | ],
57 | "no-construct": true,
58 | "no-debugger": true,
59 | "no-duplicate-super": true,
60 | "no-empty": false,
61 | "no-empty-interface": true,
62 | "no-eval": true,
63 | "no-inferrable-types": [
64 | true,
65 | "ignore-params"
66 | ],
67 | "no-misused-new": true,
68 | "no-non-null-assertion": true,
69 | "no-shadowed-variable": true,
70 | "no-string-literal": false,
71 | "no-string-throw": true,
72 | "no-switch-case-fall-through": true,
73 | "no-trailing-whitespace": true,
74 | "no-unnecessary-initializer": true,
75 | "no-unused-expression": true,
76 | "no-use-before-declare": true,
77 | "no-var-keyword": true,
78 | "object-literal-sort-keys": false,
79 | "one-line": [
80 | true,
81 | "check-open-brace",
82 | "check-catch",
83 | "check-else",
84 | "check-whitespace"
85 | ],
86 | "prefer-const": true,
87 | "quotemark": [
88 | true,
89 | "single"
90 | ],
91 | "radix": true,
92 | "semicolon": [
93 | true,
94 | "always"
95 | ],
96 | "triple-equals": [
97 | true,
98 | "allow-null-check"
99 | ],
100 | "typedef-whitespace": [
101 | true,
102 | {
103 | "call-signature": "nospace",
104 | "index-signature": "nospace",
105 | "parameter": "nospace",
106 | "property-declaration": "nospace",
107 | "variable-declaration": "nospace"
108 | }
109 | ],
110 | "unified-signatures": true,
111 | "variable-name": false,
112 | "whitespace": [
113 | true,
114 | "check-branch",
115 | "check-decl",
116 | "check-operator",
117 | "check-separator",
118 | "check-type"
119 | ],
120 | "directive-selector": [
121 | true,
122 | "attribute",
123 | "app",
124 | "camelCase"
125 | ],
126 | "component-selector": [
127 | true,
128 | "element",
129 | "app",
130 | "kebab-case"
131 | ],
132 | "no-output-on-prefix": true,
133 | "use-input-property-decorator": true,
134 | "use-output-property-decorator": true,
135 | "use-host-property-decorator": true,
136 | "no-input-rename": true,
137 | "no-output-rename": true,
138 | "use-life-cycle-interface": true,
139 | "use-pipe-transform-interface": true,
140 | "component-class-suffix": true,
141 | "directive-class-suffix": true
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/config/keys.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./keys.prod')
3 | } else {
4 | module.exports = require('./keys.dev')
5 | }
--------------------------------------------------------------------------------
/config/keys.prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | jwt: process.env.JWT,
3 | mongoURI: process.env.MONGO_URI
4 | }
--------------------------------------------------------------------------------
/controllers/analytics.js:
--------------------------------------------------------------------------------
1 | const Order = require('../models/order')
2 | const moment = require('moment')
3 | const errorHandler = require('../utils/error')
4 |
5 | module.exports.getOverview = async function(req, res) {
6 | try {
7 | const allOrders = await Order.find({user: req.user.id}).sort({date: 1})
8 | const ordersMap = getOrdersMap(allOrders)
9 | const yesterdayOrders = ordersMap[moment().add(-1, 'd').format('DD.MM.YYYY')] || []
10 |
11 | // Количество заказов
12 | const totalOrdersNumber = allOrders.length
13 |
14 | // console.log(JSON.stringify(ordersMap, null, 2))
15 | // Всего количество дней
16 | const daysNumber = Object.keys(ordersMap).length
17 | // Количество заказов вчера
18 | const yesterdayOrdersNumber = yesterdayOrders.length
19 | // Заказов в день
20 | const ordersPerDay = (totalOrdersNumber / daysNumber).toFixed(0)
21 | // Процент для количества заказов
22 | const ordersPercent = (((yesterdayOrdersNumber / ordersPerDay) - 1) * 100).toFixed(2)
23 | // Общая выручка
24 | const totalGain = calculatePrice(allOrders)
25 | // Выручка в день
26 | const gainPerDay = totalGain / daysNumber
27 | // Выручка за вчера
28 | const yesterdayGain = calculatePrice(yesterdayOrders).toFixed(2)
29 | // Процент выручка
30 | const gainPercent = (((yesterdayGain / gainPerDay) - 1) * 100).toFixed(2)
31 | // Сравнение выручки
32 | const compareGain = (yesterdayGain - gainPerDay).toFixed(2)
33 | // Сравнение количетсво заказов
34 | const compareNumber = (yesterdayOrdersNumber - ordersPerDay).toFixed(0)
35 |
36 | res.status(200).json({
37 | gain: {
38 | percent: Math.abs(+gainPercent),
39 | compare: Math.abs(+compareGain),
40 | yesterday: +yesterdayGain,
41 | isHigher: +gainPercent > 0
42 | },
43 | orders: {
44 | percent: Math.abs(+ordersPercent),
45 | compare: Math.abs(+compareNumber),
46 | yesterday: +yesterdayOrdersNumber,
47 | isHigher: +ordersPercent > 0
48 | }
49 | })
50 |
51 | } catch (e) {
52 | errorHandler(res, e)
53 | }
54 | }
55 |
56 | module.exports.getAnalytics = async function(req, res) {
57 | try {
58 | const allOrders = await Order.find({user: req.user.id}).sort({date: 1})
59 | const ordersMap = getOrdersMap(allOrders)
60 |
61 | const average = calculatePrice(allOrders) / Object.keys(ordersMap).length
62 |
63 | const chart = Object.keys(ordersMap).map(label => {
64 | const gain = calculatePrice(ordersMap[label])
65 | const order = ordersMap[label].length
66 | return {
67 | label, gain, order
68 | }
69 | })
70 |
71 | res.status(200).json({
72 | chart,
73 | average: +average.toFixed(2)
74 | })
75 | } catch (e) {
76 | errorHandler(res, e)
77 | }
78 | }
79 |
80 |
81 | function calculatePrice(orders = []) {
82 | return orders.reduce((total, order) => {
83 | const orderPrice = order.list.reduce((orderTotal, item) => {
84 | return orderTotal += item.cost * item.quantity
85 | }, 0)
86 | return total += orderPrice
87 | }, 0)
88 | }
89 |
90 | function getOrdersMap(orders = []) {
91 | const daysOrder = {}
92 | orders.forEach(order => {
93 | const date = moment(order.date).format('DD.MM.YYYY')
94 |
95 | // Не счиаем текущий день
96 | if (date === moment().format('DD.MM.YYYY')) {
97 | return
98 | }
99 |
100 | if (!daysOrder[date]) {
101 | daysOrder[date] = []
102 | }
103 |
104 | daysOrder[date].push(order)
105 | })
106 | return daysOrder
107 | }
--------------------------------------------------------------------------------
/controllers/auth.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt-nodejs')
2 | const jwt = require('jsonwebtoken')
3 | const keys = require('../config/keys')
4 | const error = require('../utils/error')
5 | const User = require('../models/user')
6 |
7 | module.exports.login = async function (req, res) {
8 | const candidate = await User.findOne({email: req.body.email})
9 |
10 | if (candidate) {
11 | const result = bcrypt.compareSync(req.body.password, candidate.password)
12 | if (result) {
13 | const token = jwt.sign({
14 | email: candidate.email,
15 | userId: candidate._id
16 | }, keys.jwt, {expiresIn: 60 * 60})
17 | res.status(200).json({token: `Bearer ${token}`})
18 | } else {
19 | res.status(401).json({message: 'Пароль неверный'})
20 | }
21 | } else {
22 | res.status(404).json({message: 'Пользователь не найден'})
23 | }
24 |
25 | }
26 |
27 | module.exports.register = async function (req, res) {
28 | const candidate = await User.findOne({email: req.body.email})
29 |
30 | if (candidate) {
31 | res.status(409).json({message: 'Такой email уже занят'})
32 | } else {
33 | const salt = bcrypt.genSaltSync(10)
34 | const user = new User({
35 | email: req.body.email,
36 | password: bcrypt.hashSync(req.body.password, salt)
37 | })
38 |
39 | try {
40 | await user.save()
41 | res.status(201).json(user)
42 | } catch(e) {
43 | error(res, e)
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/controllers/category.js:
--------------------------------------------------------------------------------
1 | const Category = require('../models/category')
2 | const Position = require('../models/position')
3 | const errorHandler = require('../utils/error')
4 |
5 | module.exports.getAll = async function(req, res) {
6 | try {
7 | const categories = await Category.find({user: req.user.id})
8 | res.status(200).json(categories)
9 | } catch (e) {
10 | errorHandler(res, e)
11 | }
12 | }
13 |
14 | module.exports.create = async function(req, res) {
15 | const category = new Category({
16 | name: req.body.name,
17 | imageSrc: req.file ? req.file.path : '',
18 | user: req.user.id
19 | })
20 |
21 | try {
22 | await category.save()
23 | res.status(201).json(category)
24 | } catch (e) {
25 | errorHandler(res, e)
26 | }
27 | }
28 |
29 | module.exports.getById = async function(req, res) {
30 | try {
31 | const category = await Category.findById(req.params.id)
32 | res.status(200).json(category)
33 | } catch (e) {
34 | errorHandler(res, e)
35 | }
36 | }
37 |
38 | module.exports.delete = async function(req, res) {
39 | try {
40 | await Category.remove({_id: req.params.id})
41 | await Position.find({category: req.params.id})
42 | res.status(200).json({message: 'Удалено'})
43 | } catch (e) {
44 | errorHandler(res, e)
45 | }
46 | }
47 |
48 | module.exports.update = async function(req, res) {
49 | const updated = {
50 | name: req.body.name
51 | }
52 | if (req.file) {
53 | updated.imageSrc = req.file.path
54 | }
55 | try {
56 | const category = await Category.findOneAndUpdate({_id: req.params.id}, {$set: updated}, {new: true})
57 | res.status(200).json(category)
58 | } catch (e) {
59 | errorHandler(res, e)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/controllers/order.js:
--------------------------------------------------------------------------------
1 | const Order = require('../models/order')
2 | const errorHandler = require('../utils/error')
3 |
4 | module.exports.getAll = async function(req, res) {
5 | try {
6 | const query = {user: req.user.id}
7 |
8 | if (req.query.order) {
9 | query.order = +req.query.order
10 | }
11 |
12 | if (req.query.start) {
13 | query.date = {
14 | $gte: req.query.start
15 | }
16 | }
17 |
18 | if (req.query.end) {
19 | if (!query.date) {
20 | query.date = {}
21 | }
22 |
23 | query.date['$lte'] = req.query.end
24 | }
25 |
26 | const orders = await Order
27 | .find(query)
28 | .sort({date: -1})
29 | .skip(+req.query.offset)
30 | .limit(+req.query.limit)
31 |
32 | res.status(200).json(orders)
33 | } catch (e) {
34 | errorHandler(res, e)
35 | }
36 | }
37 |
38 | module.exports.create = async function(req, res) {
39 | try {
40 | const lastOrder = await Order.findOne({user: req.user.id}).sort({date: -1})
41 | const maxNumber = lastOrder ? lastOrder.order : 0
42 |
43 | const order = await new Order({
44 | list: req.body.list,
45 | order: maxNumber + 1,
46 | user: req.user.id
47 | }).save()
48 |
49 | res.status(201).json(order)
50 | } catch (e) {
51 | errorHandler(res, e)
52 | }
53 | }
--------------------------------------------------------------------------------
/controllers/position.js:
--------------------------------------------------------------------------------
1 | const Position = require('../models/position')
2 | const errorHandler = require('../utils/error')
3 |
4 | module.exports.getAll = async function(req, res) {
5 | try {
6 | const positions = await Position.find({category: req.params.category, user: req.user.id})
7 | res.status(200).json(positions)
8 | } catch (e) {
9 | errorHandler(res, e)
10 | }
11 | }
12 |
13 | module.exports.create = async function(req, res) {
14 | try {
15 | const position = await new Position({
16 | name: req.body.name,
17 | cost: req.body.cost,
18 | category: req.body.category,
19 | user: req.user.id
20 | }).save()
21 | res.status(201).json(position)
22 | } catch (e) {
23 | errorHandler(res, e)
24 | }
25 | }
26 |
27 | module.exports.update = async function(req, res) {
28 | try {
29 | const position = await Position.findOneAndUpdate({_id: req.params.id}, {$set: req.body}, {new: true})
30 | res.status(200).json(position)
31 | } catch (e) {
32 | errorHandler(res, e)
33 | }
34 | }
35 | module.exports.delete = async function(req, res) {
36 | try {
37 | await Position.remove({_id: req.params.id})
38 | res.status(200).json({message: 'Позиция удалена'})
39 | } catch (e) {
40 | errorHandler(res, e)
41 | }
42 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const app = require('./app')
2 | const port = process.env.PORT || 5000
3 |
4 | app.listen(port, () => console.log(`Server has been started on port ${port}`))
--------------------------------------------------------------------------------
/middleware/passport.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy
2 | const ExtractJwt = require('passport-jwt').ExtractJwt
3 | const mongoose = require('mongoose')
4 | const keys = require('../config/keys')
5 | const User = mongoose.model('users')
6 |
7 | const opts = {
8 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
9 | secretOrKey: keys.jwt
10 | }
11 |
12 | module.exports = passport => {
13 | passport.use(new JwtStrategy(opts, async (payload, done) => {
14 | try {
15 | const user = await User.findById(payload.userId).select('email id')
16 | if (user) {
17 | done(null, user)
18 | } else {
19 | done(null, false)
20 | }
21 | } catch (e) {
22 | console.log(e)
23 | }
24 | }))
25 | }
--------------------------------------------------------------------------------
/middleware/upload.js:
--------------------------------------------------------------------------------
1 | const multer = require('multer')
2 | const moment = require('moment')
3 |
4 | const storage = multer.diskStorage({
5 | destination(req, file, cb) {
6 | cb(null, 'uploads/')
7 | },
8 | filename(req, file, cb) {
9 | cb(null, `${moment().format('DDMMYYYY-HHmmss_SSS')}-${file.originalname}`)
10 | }
11 | })
12 | const fileFilter = (req, file, cb) => {
13 | if (file.mimetype === 'image/png' || file.mimetype === 'image/jpeg') {
14 | cb(null, true)
15 | } else {
16 | cb(null, false)
17 | }
18 | }
19 |
20 | module.exports = multer({storage, fileFilter, limits: {fileSize: 1024 * 1024 * 5}})
--------------------------------------------------------------------------------
/models/category.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const Schema = mongoose.Schema
3 |
4 | const categorySchema = new Schema({
5 | name: {
6 | type: String,
7 | required: true
8 | },
9 | imageSrc: {
10 | type: String,
11 | default: ''
12 | },
13 | user: {
14 | ref: 'users',
15 | type: Schema.Types.ObjectId
16 | }
17 | })
18 |
19 | module.exports = mongoose.model('categories', categorySchema)
--------------------------------------------------------------------------------
/models/order.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const Schema = mongoose.Schema
3 |
4 | const orderSchema = new Schema({
5 | date: {
6 | type: Date,
7 | default: Date.now
8 | },
9 | order: {
10 | type: Number,
11 | required: true
12 | },
13 | list: [
14 | {
15 | name: {
16 | type: String
17 | },
18 | quantity: {
19 | type: Number
20 | },
21 | cost: {
22 | type: Number
23 | }
24 | }
25 | ],
26 | user: {
27 | ref: 'users',
28 | type: Schema.Types.ObjectId
29 | }
30 | })
31 |
32 | module.exports = mongoose.model('orders', orderSchema)
--------------------------------------------------------------------------------
/models/position.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const Schema = mongoose.Schema
3 |
4 | const categorySchema = new Schema({
5 | name: {
6 | type: String,
7 | required: true
8 | },
9 | cost: {
10 | type: Number,
11 | required: true
12 | },
13 | category: {
14 | ref: 'categories',
15 | type: Schema.Types.ObjectId
16 | },
17 | user: {
18 | ref: 'users',
19 | type: Schema.Types.ObjectId
20 | }
21 | })
22 |
23 | module.exports = mongoose.model('positions', categorySchema)
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const Schema = mongoose.Schema
3 |
4 | const userSchema = new Schema({
5 | email: {
6 | type: String,
7 | unique: true,
8 | required: true
9 | },
10 | password: {
11 | type: String,
12 | required: true
13 | }
14 | })
15 |
16 | module.exports = mongoose.model('users', userSchema)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fullstack-backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "engines": {
7 | "node": "9.9.0",
8 | "npm": "5.6.0"
9 | },
10 | "scripts": {
11 | "start": "node index.js",
12 | "server": "nodemon index.js",
13 | "client-install": "npm install --prefix client",
14 | "client": "npm run start --prefix client",
15 | "dev": "concurrently \"npm run server\" \"npm run client\"",
16 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm run client-install && npm run build --prefix client"
17 | },
18 | "author": "Vladilen Minin",
19 | "license": "ISC",
20 | "dependencies": {
21 | "bcrypt-nodejs": "0.0.3",
22 | "body-parser": "^1.18.2",
23 | "concurrently": "^3.5.1",
24 | "cors": "^2.8.4",
25 | "express": "^4.16.3",
26 | "jsonwebtoken": "^8.2.1",
27 | "moment": "^2.22.1",
28 | "mongoose": "^5.0.14",
29 | "morgan": "^1.9.0",
30 | "multer": "^1.3.0",
31 | "passport": "^0.4.0",
32 | "passport-jwt": "^4.0.0"
33 | },
34 | "devDependencies": {
35 | "nodemon": "^1.17.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/routes/analytics.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const controller = require('../controllers/analytics')
3 | const passport = require('passport')
4 | const router = express.Router()
5 |
6 | router.get('/overview', passport.authenticate('jwt', {session: false}), controller.getOverview)
7 | router.get('/analytics', passport.authenticate('jwt', {session: false}), controller.getAnalytics)
8 |
9 | module.exports = router
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const controller = require('../controllers/auth')
3 | const router = express.Router()
4 |
5 | router.post('/login', controller.login)
6 | router.post('/register', controller.register)
7 |
8 | module.exports = router
--------------------------------------------------------------------------------
/routes/category.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const controller = require('../controllers/category')
3 | const upload = require('../middleware/upload')
4 | const passport = require('passport')
5 | const router = express.Router()
6 |
7 | router.get('/', passport.authenticate('jwt', {session: false}), controller.getAll)
8 | router.get('/:id', passport.authenticate('jwt', {session: false}), controller.getById)
9 | router.delete('/:id', passport.authenticate('jwt', {session: false}), controller.delete)
10 | router.post('/', passport.authenticate('jwt', {session: false}), upload.single('image'), controller.create)
11 | router.patch('/:id', passport.authenticate('jwt', {session: false}), upload.single('image'), controller.update)
12 |
13 | module.exports = router
--------------------------------------------------------------------------------
/routes/order.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const controller = require('../controllers/order')
3 | const passport = require('passport')
4 | const router = express.Router()
5 |
6 | router.get('/', passport.authenticate('jwt', {session: false}), controller.getAll)
7 | router.post('/', passport.authenticate('jwt', {session: false}), controller.create)
8 |
9 | module.exports = router
--------------------------------------------------------------------------------
/routes/position.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const controller = require('../controllers/position')
3 | const passport = require('passport')
4 | const router = express.Router()
5 |
6 | router.get('/:category', passport.authenticate('jwt', {session: false}), controller.getAll)
7 | router.post('/', passport.authenticate('jwt', {session: false}), controller.create)
8 | router.patch('/:id', passport.authenticate('jwt', {session: false}), controller.update)
9 | router.delete('/:id', passport.authenticate('jwt', {session: false}), controller.delete)
10 |
11 | module.exports = router
--------------------------------------------------------------------------------
/uploads/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/.gitkeep
--------------------------------------------------------------------------------
/uploads/17042018-174213_055-coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/17042018-174213_055-coffee.png
--------------------------------------------------------------------------------
/uploads/17042018-174233_978-cake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/17042018-174233_978-cake.jpg
--------------------------------------------------------------------------------
/uploads/17042018-214450_614-cake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/17042018-214450_614-cake.jpg
--------------------------------------------------------------------------------
/uploads/19042018-145815_822-cake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/19042018-145815_822-cake.jpg
--------------------------------------------------------------------------------
/uploads/20042018-143422_242-cake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/20042018-143422_242-cake.jpg
--------------------------------------------------------------------------------
/uploads/20042018-143459_550-coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/20042018-143459_550-coffee.png
--------------------------------------------------------------------------------
/uploads/21042018-122114_479-cake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vladilenm/fullstack/72465941d15015d873fab7eaf595e9f3135c264f/uploads/21042018-122114_479-cake.jpg
--------------------------------------------------------------------------------
/utils/error.js:
--------------------------------------------------------------------------------
1 | module.exports = function errorHandler(res, message) {
2 | res.status(500).json({
3 | success: false,
4 | message: message.message ? message.message : message
5 | })
6 | }
--------------------------------------------------------------------------------