first push
parent
c3f651da36
commit
d61cb6e403
|
|
@ -0,0 +1,17 @@
|
|||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -1,50 +1,46 @@
|
|||
# These are some examples of commonly ignored file patterns.
|
||||
# You should customize this list as applicable to your project.
|
||||
# Learn more about .gitignore:
|
||||
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Node artifact files
|
||||
node_modules/
|
||||
dist/
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# Compiled Java class files
|
||||
*.class
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# Compiled Python bytecode
|
||||
*.py[cod]
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Package files
|
||||
*.jar
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Maven
|
||||
target/
|
||||
dist/
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
/.angular
|
||||
|
||||
# JetBrains IDE
|
||||
.idea/
|
||||
|
||||
# Unit test reports
|
||||
TEST*.xml
|
||||
|
||||
# Generated by MacOS
|
||||
# System Files
|
||||
.DS_Store
|
||||
|
||||
# Generated by Windows
|
||||
Thumbs.db
|
||||
|
||||
# Applications
|
||||
*.app
|
||||
*.exe
|
||||
*.war
|
||||
|
||||
# Large media files
|
||||
*.mp4
|
||||
*.tiff
|
||||
*.avi
|
||||
*.flv
|
||||
*.mov
|
||||
*.wmv
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
46
README.md
46
README.md
|
|
@ -1,45 +1,27 @@
|
|||
**Edit a file, create a new file, and clone from Bitbucket in under 2 minutes**
|
||||
# Front
|
||||
|
||||
When you're done, you can delete the content in this README and update the file with details for others getting started with your repository.
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.10.
|
||||
|
||||
*We recommend that you open this README in another tab as you perform the tasks below. You can [watch our video](https://youtu.be/0ocf7u76WSo) for a full demo of all the steps in this tutorial. Open the video in a new tab to avoid leaving Bitbucket.*
|
||||
## Development server
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
## Edit a file
|
||||
## Code scaffolding
|
||||
|
||||
You’ll start by editing this README file to learn how to edit a file in Bitbucket.
|
||||
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`.
|
||||
|
||||
1. Click **Source** on the left side.
|
||||
2. Click the README.md link from the list of files.
|
||||
3. Click the **Edit** button.
|
||||
4. Delete the following text: *Delete this line to make a change to the README from Bitbucket.*
|
||||
5. After making your change, click **Commit** and then **Commit** again in the dialog. The commit page will open and you’ll see the change you just made.
|
||||
6. Go back to the **Source** page.
|
||||
## Build
|
||||
|
||||
---
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Create a file
|
||||
## Running unit tests
|
||||
|
||||
Next, you’ll add a new file to this repository.
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
1. Click the **New file** button at the top of the **Source** page.
|
||||
2. Give the file a filename of **contributors.txt**.
|
||||
3. Enter your name in the empty file space.
|
||||
4. Click **Commit** and then **Commit** again in the dialog.
|
||||
5. Go back to the **Source** page.
|
||||
## Running end-to-end tests
|
||||
|
||||
Before you move on, go ahead and explore the repository. You've already seen the **Source** page, but check out the **Commits**, **Branches**, and **Settings** pages.
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
---
|
||||
## Further help
|
||||
|
||||
## Clone a repository
|
||||
|
||||
Use these steps to clone from SourceTree, our client for using the repository command-line free. Cloning allows you to work on your files locally. If you don't yet have SourceTree, [download and install first](https://www.sourcetreeapp.com/). If you prefer to clone from the command line, see [Clone a repository](https://confluence.atlassian.com/x/4whODQ).
|
||||
|
||||
1. You’ll see the clone button under the **Source** heading. Click that button.
|
||||
2. Now click **Check out in SourceTree**. You may need to create a SourceTree account or log in.
|
||||
3. When you see the **Clone New** dialog in SourceTree, update the destination path and name if you’d like to and then click **Clone**.
|
||||
4. Open the directory you just created to see your repository’s files.
|
||||
|
||||
Now that you're more familiar with your Bitbucket repository, go ahead and add a new file locally. You can [push your change back to Bitbucket with SourceTree](https://confluence.atlassian.com/x/iqyBMg), or you can [add, commit,](https://confluence.atlassian.com/x/8QhODQ) and [push from the command line](https://confluence.atlassian.com/x/NQ0zDQ).
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
},
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"front": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
},
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/front",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{"glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/"},
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/assets/css/fonts.scss",
|
||||
"src/assets/css/basics.scss",
|
||||
"src/assets/css/main-menu.scss",
|
||||
"src/assets/css/registry.scss",
|
||||
"src/assets/css/page-top-panel.scss",
|
||||
"src/assets/css/buttons.scss",
|
||||
"src/assets/css/dropdown.scss",
|
||||
"src/assets/css/forms.scss",
|
||||
"src/assets/css/tables.scss",
|
||||
"src/assets/css/tabs.scss",
|
||||
"src/assets/css/list-items.scss",
|
||||
"src/assets/css/documents-lists.scss",
|
||||
"src/assets/css/slider.scss",
|
||||
"src/styles.scss",
|
||||
"node_modules/swiper/swiper-bundle.css"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src/styles"
|
||||
]
|
||||
},
|
||||
"scripts": [
|
||||
"node_modules/tinymce/tinymce.min.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1500kb",
|
||||
"maximumError": "10mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "32kb",
|
||||
"maximumError": "64kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "front:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "front:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "front:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "front"
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/front'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "front",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~15.1.0",
|
||||
"@angular/cdk": "^15.0.0",
|
||||
"@angular/common": "~15.1.0",
|
||||
"@angular/compiler": "~15.1.0",
|
||||
"@angular/core": "~15.1.0",
|
||||
"@angular/forms": "~15.1.0",
|
||||
"@angular/material": "^15.0.0",
|
||||
"@angular/platform-browser": "~15.1.0",
|
||||
"@angular/platform-browser-dynamic": "~15.1.0",
|
||||
"@angular/router": "~15.1.0",
|
||||
"@tinymce/tinymce-angular": "^7.0.0",
|
||||
"echarts": "^5.4.0",
|
||||
"ngx-echarts": "^8.0.1",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"particles.js": "^2.0.0",
|
||||
"rxjs": "~6.6.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"swiper": "^8.4.7",
|
||||
"tinymce": "^6.4.2",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^15.1.0",
|
||||
"@angular/cli": "^15.2.9",
|
||||
"@angular/compiler-cli": "~15.1.0",
|
||||
"@types/echarts": "^4.9.12",
|
||||
"@types/jasmine": "^4.0.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/sortablejs": "^1.15.1",
|
||||
"jasmine-core": "^4.0.0",
|
||||
"karma": "^6.4.1",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-coverage": "^2.0.3",
|
||||
"karma-jasmine": "^4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.7.0",
|
||||
"typescript": "~4.9.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,468 @@
|
|||
import { Directive, ElementRef, Input, OnDestroy, HostListener, OnInit } from "@angular/core";
|
||||
|
||||
/*
|
||||
Variables to be used outside of directive scope
|
||||
To improve performance.
|
||||
*/
|
||||
const TAU: number = Math.PI * 2;
|
||||
const QUADTREE_CAPACITY: number = 4;
|
||||
let linkBatches: number = 10;
|
||||
let mouse: {x: number,y: number} = {x: 0, y: 0};
|
||||
|
||||
|
||||
/*
|
||||
Variables to be initiated
|
||||
*/
|
||||
let linkDistance: number;
|
||||
let linkDistance2: number;
|
||||
let repulseDistance: number;
|
||||
let particleSpeed: number;
|
||||
let particleSize: number;
|
||||
let bounce: boolean;
|
||||
let quadTree: QuadTree;
|
||||
let canvas: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: "[repulse-particles]"
|
||||
})
|
||||
export class ParticlesDirective implements OnDestroy, OnInit {
|
||||
|
||||
@Input() number: number = 80;
|
||||
@Input() speed: number = 6;
|
||||
@Input() linkWidth: number = .5;
|
||||
@Input() linkDistance: number = 140;
|
||||
@Input() size: number = 3;
|
||||
@Input() repulseDistance: number = 140;
|
||||
@Input() particleHex: string = "#FFF";
|
||||
@Input() linkHex: string = "#FFF";
|
||||
@Input() bounce: boolean = true;
|
||||
@Input() densityArea: number = 800;
|
||||
|
||||
|
||||
particlesNumber: number;
|
||||
particlesList: Particle[] = [];
|
||||
links: Link[][] = [];
|
||||
linkBatchAlphas: number[] = [];
|
||||
linkPool: Link[] = [];
|
||||
candidates: Particle[] = [];
|
||||
boundary: Bounds;
|
||||
|
||||
animationFrame;
|
||||
|
||||
constructor(
|
||||
public el: ElementRef,
|
||||
) {
|
||||
canvas = this.el.nativeElement;
|
||||
canvas.style.height = "100%";
|
||||
canvas.style.width = "100%";
|
||||
ctx = canvas.getContext("2d");
|
||||
for (var i = 1/(linkBatches + 1); i < 1; i += 1/(linkBatches + 1)) {
|
||||
this.links.push([]);
|
||||
this.linkBatchAlphas.push(i);
|
||||
}
|
||||
this.initVariables();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.setCanvasSize();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
@HostListener("window:resize") onResize() {
|
||||
this.setCanvasSize();
|
||||
}
|
||||
|
||||
@HostListener("mouseleave") onMouseLeave() {
|
||||
this.stopMouse()
|
||||
}
|
||||
|
||||
@HostListener("touchend") onTouchEnd() {
|
||||
this.stopMouse()
|
||||
}
|
||||
|
||||
@HostListener("mousemove", ["$event"]) onMouseMove(e) {
|
||||
this.setMousePos(e.offsetX, e.offsetY);
|
||||
}
|
||||
|
||||
@HostListener("touchmove", ["$event"]) onTouchMove(e) {
|
||||
this.setMousePos(e.touches[0].clientX, e.touches[0].clientY);
|
||||
}
|
||||
|
||||
@HostListener("change") ngOnChanges() {
|
||||
this.initVariables();
|
||||
this.resetParticles();
|
||||
}
|
||||
|
||||
setMousePos(x, y) {
|
||||
mouse.x = x;
|
||||
mouse.y = y;
|
||||
}
|
||||
|
||||
stopMouse() {
|
||||
mouse.x = null;
|
||||
}
|
||||
|
||||
initVariables() {
|
||||
linkDistance = this.linkDistance;
|
||||
linkDistance2 = (0.7 * linkDistance) ** 2;
|
||||
repulseDistance = this.repulseDistance;
|
||||
particleSpeed = this.speed;
|
||||
particleSize = this.size;
|
||||
bounce = this.bounce;
|
||||
if (this.densityArea) this.scaleDensity();
|
||||
}
|
||||
|
||||
|
||||
animate() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.updateParticles();
|
||||
this.updateLinks();
|
||||
this.animationFrame = requestAnimationFrame(this.animate.bind(this));
|
||||
}
|
||||
|
||||
updateParticles() {
|
||||
quadTree.close();
|
||||
ctx.fillStyle = this.particleHex;
|
||||
ctx.beginPath();
|
||||
for (const p of this.particlesList) p.update(ctx, true);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
updateLinks() {
|
||||
let i: number;
|
||||
let link: Link;
|
||||
let alphaIdx = 0;
|
||||
|
||||
for (const p1 of this.particlesList) {
|
||||
p1.explored = true;
|
||||
const count = quadTree.query(p1, 0, this.candidates);
|
||||
for (i = 0; i < count; i++) {
|
||||
const p2 = this.candidates[i];
|
||||
if (!p2.explored) {
|
||||
link = this.linkPool.length ? this.linkPool.pop() : new Link();
|
||||
link.init(p1, p2);
|
||||
this.links[link.batchId].push(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.lineWidth = this.linkWidth;
|
||||
ctx.strokeStyle = this.linkHex;
|
||||
for (const l of this.links) {
|
||||
ctx.globalAlpha = this.linkBatchAlphas[alphaIdx++];
|
||||
ctx.beginPath();
|
||||
while (l.length) this.linkPool.push(l.pop().addPath(ctx));
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
resetParticles() {
|
||||
this.particlesList = [];
|
||||
for (let i = 0; i < this.particlesNumber; i++) {
|
||||
this.particlesList.push(new Particle(canvas, particleSize))
|
||||
}
|
||||
quadTree = new QuadTree();
|
||||
for (const p of this.particlesList) p.reset(canvas);
|
||||
}
|
||||
|
||||
scaleDensity() {
|
||||
var area = canvas.width * canvas.height / 1000;
|
||||
this.particlesNumber = (area * this.number / this.densityArea) | 0;
|
||||
}
|
||||
|
||||
setCanvasSize() {
|
||||
canvas.height = canvas.offsetHeight;
|
||||
canvas.width = canvas.offsetWidth;
|
||||
if (this.densityArea) this.scaleDensity();
|
||||
this.resetParticles();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
}
|
||||
|
||||
class Link {
|
||||
p1: Particle;
|
||||
p2: Particle;
|
||||
alpha: number;
|
||||
batchId: number;
|
||||
constructor() { }
|
||||
init(p1: Particle, p2: Particle) {
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
const dx = p1.x - p2.x;
|
||||
const dy = p1.y - p2.y;
|
||||
this.alpha = 1 - (dx * dx + dy * dy) / linkDistance2;
|
||||
this.batchId = this.alpha * linkBatches | 0;
|
||||
this.batchId = this.batchId >= linkBatches ? linkBatches : this.batchId;
|
||||
}
|
||||
addPath(ctx) {
|
||||
ctx.moveTo(this.p1.x, this.p1.y);
|
||||
ctx.lineTo(this.p2.x, this.p2.y);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Particle {
|
||||
r: number;
|
||||
speedScale: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
quad: QuadTree;
|
||||
explored: boolean;
|
||||
constructor (canvas, r) {
|
||||
this.r = r;
|
||||
this.speedScale = particleSpeed / 2;
|
||||
this.reset(canvas, r);
|
||||
}
|
||||
reset(canvas, r = this.r) {
|
||||
const W = canvas.width - r * 2;
|
||||
const H = canvas.height - r * 2;
|
||||
this.x = Math.random() * W + r;
|
||||
this.y = Math.random() * H + r;
|
||||
this.vx = Math.random() - 0.5;
|
||||
this.vy = Math.random() - 0.5;
|
||||
this.quad = undefined;
|
||||
this.explored = false;
|
||||
|
||||
}
|
||||
addPath(ctx) {
|
||||
ctx.moveTo(this.x + this.r, this.y);
|
||||
ctx.arc(this.x, this.y, this.r, 0, TAU);
|
||||
}
|
||||
near(p) {
|
||||
return ((p.x - this.x) ** 2 + (p.y - this.y) ** 2) <= linkDistance2;
|
||||
}
|
||||
intersects(range) {
|
||||
const xd = Math.abs(range.x - this.x);
|
||||
const yd = Math.abs(range.y - this.y);
|
||||
const r = linkDistance;
|
||||
const w = range.w;
|
||||
const h = range.h;
|
||||
if (xd > r + w || yd > r + h) { return false }
|
||||
if (xd <= w || yd <= h) { return true }
|
||||
return ((xd - w) ** 2 + (yd - h) ** 2) <= linkDistance2;
|
||||
|
||||
}
|
||||
update(ctx, repulse = true) {
|
||||
this.explored = false;
|
||||
const r = this.r;
|
||||
let W, H;
|
||||
this.x += this.vx * this.speedScale;
|
||||
this.y += this.vy * this.speedScale;
|
||||
|
||||
if (bounce) {
|
||||
W = ctx.canvas.width - r;
|
||||
H = ctx.canvas.height - r;
|
||||
if (this.x > W || this.x < 0) {
|
||||
this.vx = -this.vx;
|
||||
}
|
||||
if (this.y > H || this.y < 0) {
|
||||
this.vy = -this.vy;
|
||||
}
|
||||
} else {
|
||||
W = ctx.canvas.width + r;
|
||||
H = ctx.canvas.height + r;
|
||||
if (this.x > W) {
|
||||
this.x = 0;
|
||||
this.y = Math.random() * (H - r);
|
||||
} else if (this.x < -r) {
|
||||
this.x = W - r;
|
||||
this.y = Math.random() * (H - r);
|
||||
}
|
||||
if (this.y > H) {
|
||||
this.y = 0
|
||||
this.x = Math.random() * (W - r);
|
||||
} else if (this.y < -r) {
|
||||
this.y = H - r;
|
||||
this.x = Math.random() * (W - r);
|
||||
}
|
||||
}
|
||||
repulse && mouse.x && this.repulse();
|
||||
this.addPath(ctx);
|
||||
quadTree.insert(this);
|
||||
this.quad && (this.quad.drawn = false)
|
||||
}
|
||||
repulse() {
|
||||
var dx = this.x - mouse.x;
|
||||
var dy = this.y - mouse.y;
|
||||
|
||||
const dist = (dx * dx + dy * dy) ** 0.5;
|
||||
var rf = ((1 - (dist / repulseDistance) ** 2) * 100);
|
||||
rf = (rf < 0 ? 0 : rf > 50 ? 50 : rf) / dist;
|
||||
|
||||
var posX = this.x + dx * rf;
|
||||
var posY = this.y + dy * rf;
|
||||
|
||||
if (bounce) {
|
||||
if (posX - particleSize > 0 && posX + particleSize < canvas.width) this.x = posX;
|
||||
if (posY - particleSize > 0 && posY + particleSize < canvas.height) this.y = posY;
|
||||
} else {
|
||||
this.x = posX;
|
||||
this.y = posY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Bounds {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
diagonal: number;
|
||||
constructor(x, y, w, h) { this.init(x, y, w, h) }
|
||||
init(x,y,w,h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
this.left = x - w;
|
||||
this.right = x + w;
|
||||
this.top = y - h;
|
||||
this.bottom = y + h;
|
||||
this.diagonal = (w * w + h * h);
|
||||
}
|
||||
|
||||
contains(p) {
|
||||
return (p.x >= this.left && p.x <= this.right && p.y >= this.top && p.y <= this.bottom);
|
||||
}
|
||||
|
||||
near(p) {
|
||||
if (!this.contains(p)) {
|
||||
const dx = p.x - this.x;
|
||||
const dy = p.y - this.y;
|
||||
const dist = (dx * dx + dy * dy) - this.diagonal - linkDistance2;
|
||||
return dist < 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class QuadTree {
|
||||
boundary: Bounds;
|
||||
divided: boolean;
|
||||
points: Particle[];
|
||||
pointCount: number;
|
||||
drawn: boolean;
|
||||
depth: number;
|
||||
|
||||
NE: QuadTree;
|
||||
NW: QuadTree;
|
||||
SE: QuadTree;
|
||||
SW: QuadTree;
|
||||
constructor(boundary: Bounds = new Bounds(canvas.width / 2,canvas.height / 2,canvas.width / 2 ,canvas.height / 2), depth = 0) {
|
||||
this.boundary = boundary;
|
||||
this.divided = false;
|
||||
this.points = depth > 1 ? [] : null;
|
||||
this.pointCount = 0
|
||||
this.drawn = false;
|
||||
this.depth = depth;
|
||||
if(depth === 0) { // BM67 Fix on resize
|
||||
this.subdivide();
|
||||
this.NE.subdivide();
|
||||
this.NW.subdivide();
|
||||
this.SE.subdivide();
|
||||
this.SW.subdivide();
|
||||
}
|
||||
}
|
||||
|
||||
addPath() {
|
||||
const b = this.boundary;
|
||||
ctx.rect(b.left, b.top, b.w * 2, b.h * 2);
|
||||
this.drawn = true;
|
||||
}
|
||||
addToSubQuad(particle) {
|
||||
if (this.NE.insert(particle)) { return true }
|
||||
if (this.NW.insert(particle)) { return true }
|
||||
if (this.SE.insert(particle)) { return true }
|
||||
if (this.SW.insert(particle)) { return true }
|
||||
particle.quad = undefined;
|
||||
}
|
||||
insert(particle) {
|
||||
if (this.depth > 0 && !this.boundary.contains(particle)) { return false }
|
||||
|
||||
if (this.depth > 1 && this.pointCount < QUADTREE_CAPACITY) {
|
||||
this.points[this.pointCount++] = particle;
|
||||
particle.quad = this;
|
||||
return true;
|
||||
}
|
||||
if (!this.divided) { this.subdivide() }
|
||||
return this.addToSubQuad(particle);
|
||||
}
|
||||
|
||||
subdivide() {
|
||||
if (!this.NW) {
|
||||
const x = this.boundary.x;
|
||||
const y = this.boundary.y;
|
||||
const w = this.boundary.w / 2;
|
||||
const h = this.boundary.h / 2;
|
||||
const depth = this.depth + 1;
|
||||
|
||||
this.NE = new QuadTree(new Bounds(x + w, y - h, w, h), depth);
|
||||
this.NW = new QuadTree(new Bounds(x - w, y - h, w, h), depth);
|
||||
this.SE = new QuadTree(new Bounds(x + w, y + h, w, h), depth);
|
||||
this.SW = new QuadTree(new Bounds(x - w, y + h, w, h), depth);
|
||||
} else {
|
||||
this.NE.pointCount = 0;
|
||||
this.NW.pointCount = 0;
|
||||
this.SE.pointCount = 0;
|
||||
this.SW.pointCount = 0;
|
||||
}
|
||||
|
||||
this.divided = true;
|
||||
}
|
||||
query(part, fc, found) {
|
||||
var i = this.pointCount;
|
||||
if (this.depth === 0 || this.boundary.near(part)) {
|
||||
if (this.depth > 1) {
|
||||
while (i--) {
|
||||
const p = this.points[i];
|
||||
if (!p.explored && part.near(p)) { found[fc++] = p }
|
||||
}
|
||||
if (this.divided) {
|
||||
fc = this.NE.pointCount ? this.NE.query(part, fc, found) : fc;
|
||||
fc = this.NW.pointCount ? this.NW.query(part, fc, found) : fc;
|
||||
fc = this.SE.pointCount ? this.SE.query(part, fc, found) : fc;
|
||||
fc = this.SW.pointCount ? this.SW.query(part, fc, found) : fc;
|
||||
}
|
||||
} else if(this.divided) {
|
||||
fc = this.NE.query(part, fc, found);
|
||||
fc = this.NW.query(part, fc, found);
|
||||
fc = this.SE.query(part, fc, found);
|
||||
fc = this.SW.query(part, fc, found);
|
||||
}
|
||||
}
|
||||
return fc;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.divided) {
|
||||
this.NE.close();
|
||||
this.NW.close();
|
||||
this.SE.close();
|
||||
this.SW.close();
|
||||
}
|
||||
|
||||
if (this.depth === 2 && this.divided) {
|
||||
this.NE.pointCount = 0;
|
||||
this.NW.pointCount = 0;
|
||||
this.SE.pointCount = 0;
|
||||
this.SW.pointCount = 0;
|
||||
} else if (this.depth > 2) {
|
||||
this.divided = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { AuthenticationService } from '@app/_services';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private router: Router, private authenticationService: AuthenticationService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const oauthToken = this.authenticationService.oauthTokenValue;
|
||||
if (oauthToken) {
|
||||
// logged in so return true
|
||||
return true;
|
||||
}
|
||||
|
||||
// not logged in so redirect to login page with the return url
|
||||
this.authenticationService.popup('login');
|
||||
|
||||
//this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { AuthenticationService } from '@app/_services';
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Injectable()
|
||||
export class ErrorInterceptor implements HttpInterceptor {
|
||||
constructor(private authenticationService: AuthenticationService, private router: Router) {
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(catchError(err => {
|
||||
if (err.status === 401) {
|
||||
this.authenticationService.logout();
|
||||
this.authenticationService.popup('login');
|
||||
}
|
||||
|
||||
const error = err.error || err.statusText;
|
||||
return throwError(error);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';
|
||||
|
||||
const users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];
|
||||
|
||||
@Injectable()
|
||||
export class FakeBackendInterceptor implements HttpInterceptor {
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
const { url, method, headers, body } = request;
|
||||
|
||||
// wrap in delayed observable to simulate server api call
|
||||
return of(null)
|
||||
.pipe(mergeMap(handleRoute))
|
||||
.pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
|
||||
.pipe(delay(500))
|
||||
.pipe(dematerialize());
|
||||
|
||||
function handleRoute() {
|
||||
switch (true) {
|
||||
case url.endsWith('/users/authenticate') && method === 'POST':
|
||||
return authenticate();
|
||||
case url.endsWith('/users') && method === 'GET':
|
||||
return getUsers();
|
||||
default:
|
||||
// pass through any requests not handled above
|
||||
return next.handle(request);
|
||||
}
|
||||
}
|
||||
|
||||
// route functions
|
||||
|
||||
function authenticate() {
|
||||
const { username, password } = body;
|
||||
const user = users.find(x => x.username === username && x.password === password);
|
||||
if (!user) return error('Username or password is incorrect');
|
||||
return ok({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
token: 'fake-jwt-token'
|
||||
})
|
||||
}
|
||||
|
||||
function getUsers() {
|
||||
if (!isLoggedIn()) return unauthorized();
|
||||
return ok(users);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
function ok(body?) {
|
||||
return of(new HttpResponse({ status: 200, body }))
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
return throwError({ error: { message } });
|
||||
}
|
||||
|
||||
function unauthorized() {
|
||||
return throwError({ status: 401, error: { message: 'Unauthorised' } });
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return headers.get('Authorization') === 'Bearer fake-jwt-token';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let fakeBackendProvider = {
|
||||
// use fake backend in place of Http service for backend-less development
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: FakeBackendInterceptor,
|
||||
multi: true
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './auth.guard';
|
||||
export * from './error.interceptor';
|
||||
export * from './fake-backend';
|
||||
export * from './jwt.interceptor';
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { environment } from '@environments/environment';
|
||||
import { AuthenticationService } from '@app/_services';
|
||||
|
||||
@Injectable()
|
||||
export class JwtInterceptor implements HttpInterceptor {
|
||||
constructor(private authenticationService: AuthenticationService) { }
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
const oauthToken = this.authenticationService.token.value;
|
||||
const isLoggedIn = oauthToken && oauthToken.access_token;
|
||||
const isApiUrl = request.url.startsWith(environment.apiUrl);
|
||||
if (isLoggedIn && isApiUrl) {
|
||||
request = request.clone({
|
||||
setHeaders: {
|
||||
Authorization: `${oauthToken.token_type} ${oauthToken.access_token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next.handle(request);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import {UserData} from "@app/_models/user";
|
||||
|
||||
export class Asset {
|
||||
id: string;
|
||||
type: string;
|
||||
path: string;
|
||||
mime: string;
|
||||
filename: string;
|
||||
extension: string;
|
||||
createdAt: string;
|
||||
coordinates?: any;
|
||||
links?: AssetLinks;
|
||||
user?: UserData;
|
||||
}
|
||||
|
||||
export class AssetsList {
|
||||
data: Asset[];
|
||||
}
|
||||
|
||||
export class AssetData {
|
||||
data: Asset;
|
||||
}
|
||||
|
||||
export class AssetLinks {
|
||||
open?: string;
|
||||
download?: string;
|
||||
full?: string;
|
||||
thumb?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './asset';
|
||||
export * from './oauthToken';
|
||||
export * from './user';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export class OauthToken {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
export class User {
|
||||
id: string;
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
initials: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
avatar?: any;
|
||||
isPrivileged: boolean;
|
||||
roles?: any;
|
||||
membership?: any;
|
||||
privileges?: any;
|
||||
}
|
||||
|
||||
export class UsersList {
|
||||
data: User[];
|
||||
}
|
||||
|
||||
export class UserData {
|
||||
data: User;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterModule} from "@angular/router";
|
||||
import {AuthGuard} from "@app/_helpers";
|
||||
import {AdministrationPageComponent} from "@app/_modules/administration/page/administration-page.component";
|
||||
import {BrowserModule} from "@angular/platform-browser";
|
||||
import {AdministrateCompanyComponent} from "@app/_modules/administration/company/administrate-company.component";
|
||||
import {AdministrateCommitteeComponent} from "@app/_modules/administration/committee/administrate-committee.component";
|
||||
import {AdvisoriesModule} from "@app/_modules/advisories/advisories.module";
|
||||
import {CompaniesModule} from "@app/_modules/companies/companies.module";
|
||||
|
||||
type PathMatch = "full" | "prefix" | undefined;
|
||||
const routes = [
|
||||
{path: 'administrate', redirectTo: 'administrate/company', pathMatch: 'full' as PathMatch},
|
||||
{path: 'administrate/:tab', component: AdministrationPageComponent, canActivate: [AuthGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes),
|
||||
AdvisoriesModule,
|
||||
CompaniesModule
|
||||
],
|
||||
declarations: [
|
||||
AdministrationPageComponent,
|
||||
AdministrateCompanyComponent,
|
||||
AdministrateCommitteeComponent
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AdministrationModule {}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<advisories-tree></advisories-tree>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'administrate-committee',
|
||||
templateUrl: 'administrate-committee.component.html',
|
||||
styleUrls: ['administrate-committee.component.scss']
|
||||
})
|
||||
export class AdministrateCommitteeComponent {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<company *ngIf="company" [company]="company"></company>
|
||||
<company-structure [companyId]="companyId" [editable]="isEditable"></company-structure>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
company {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {CompaniesService} from "@app/_services/companies.service";
|
||||
import {Subscription} from "rxjs";
|
||||
import {ListsService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
selector: 'administrate-company',
|
||||
templateUrl: 'administrate-company.component.html',
|
||||
styleUrls: ['administrate-company.component.scss']
|
||||
})
|
||||
export class AdministrateCompanyComponent {
|
||||
@Input() companyId: string;
|
||||
public company: any;
|
||||
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(private companiesService: CompaniesService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
|
||||
get permissions() {
|
||||
return this.company?.permissions;
|
||||
}
|
||||
get isEditable() {
|
||||
return this.permissions?.edit || this.permissions?.anything;
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.subscription = this.listsService.controls('company-main-info').subscribe(res => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
fetch() {
|
||||
let include = ['phones', 'emails', 'legalAddress', 'logo', 'permissions'];
|
||||
this.companiesService.fetch(this.company?.id || this.companyId, {include: include.join(',')}).subscribe(res => {
|
||||
this.company = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<div class="container">
|
||||
<h1>Панель управления</h1>
|
||||
<div class="tabs default">
|
||||
<button type="button" *ngFor="let tab of tabs" [class.active]="tab.active"
|
||||
routerLink="/administrate/{{tab.name}}">{{tab.title}}</button>
|
||||
</div>
|
||||
<div class="container" [ngSwitch]="tab.name">
|
||||
<administrate-company *ngSwitchCase="'company'" [companyId]="'main'"></administrate-company>
|
||||
<administrate-committee *ngSwitchCase="'committee'"></administrate-committee>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.container {
|
||||
h1 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {ActivatedRoute, NavigationEnd, Router} from "@angular/router";
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
templateUrl: 'administration-page.component.html',
|
||||
styleUrls: ['administration-page.component.scss']
|
||||
})
|
||||
export class AdministrationPageComponent {
|
||||
public tabs = <any>[{name: 'company', title: 'Структура ФАУ «ФЦС»'}, {name: 'committee', title: 'Структура ТК 465'}];
|
||||
routeSubscription: Subscription;
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router) {
|
||||
this.routeSubscription = this.router.events.subscribe(event => {
|
||||
if (event instanceof NavigationEnd && (this.route.snapshot.paramMap.get('tab') !== this.tab?.name)) this.switchTab(this.route.snapshot.paramMap.get('tab'));
|
||||
});
|
||||
}
|
||||
|
||||
get tab() {
|
||||
return this.tabs.filter(tab => {return tab.active})[0];
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.routeSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
switchTab(name: string) {
|
||||
this.tabs.map(tab => {tab.active = tab.name === name});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import {NgModule} from '@angular/core'
|
||||
import {CommonModule} from '@angular/common'
|
||||
import {AdvisoriesTreeComponent} from "@app/_modules/advisories/tree/advisories-tree.component";
|
||||
import {AdvisoriesTreeAdvisoryComponent} from "@app/_modules/advisories/tree/advisory/advisories-tree-advisory.component";
|
||||
import {AdvisoriesTreeCompanyComponent} from "@app/_modules/advisories/tree/company/advisories-tree-company.component";
|
||||
import {AdvisoriesTreeMemberComponent} from "@app/_modules/advisories/tree/member/advisories-tree-member.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
declarations: [
|
||||
AdvisoriesTreeComponent,
|
||||
AdvisoriesTreeAdvisoryComponent,
|
||||
AdvisoriesTreeCompanyComponent,
|
||||
AdvisoriesTreeMemberComponent
|
||||
],
|
||||
exports: [
|
||||
AdvisoriesTreeComponent,
|
||||
AdvisoriesTreeCompanyComponent
|
||||
]
|
||||
})
|
||||
export class AdvisoriesModule {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<div class="tree default" *ngIf="advisories?.length">
|
||||
<div class="items">
|
||||
<advisories-tree-advisory [advisory]="advisory" [active]="true" *ngFor="let advisory of advisories"></advisories-tree-advisory>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
.item {
|
||||
&.committee {
|
||||
>.bar .mid .info .logo {
|
||||
border: #F9B417 solid 1px;
|
||||
color: #F9B417;
|
||||
background-color: transparent;
|
||||
}
|
||||
&.main {
|
||||
>.bar .mid .info .logo {
|
||||
background: #3E3D40 none;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.workgroup {
|
||||
>.bar .mid .info .logo {
|
||||
border: #86898E solid 1px;
|
||||
color: #86898E;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: #E0E0E0 solid 1px;
|
||||
.left {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
&:before {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
background: transparent url("~src/assets/images/icons/circle_plus_24.svg") 50% 50% no-repeat;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
.mid {
|
||||
flex-grow: 1;
|
||||
padding: 0 16px;
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 16px;
|
||||
border-radius: 100px;
|
||||
background: #F9B417 url('~src/assets/images/icons/architecture_24.svg') 50% 50% no-repeat;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 100px;
|
||||
object-fit: cover;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
&.voter:before {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: #ffffff solid 1px;
|
||||
border-radius: 100px;
|
||||
//background: transparent url('~src/assets/images/icons/star_sign.svg') 50% 50% no-repeat;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
.name {
|
||||
p {
|
||||
margin: 0;
|
||||
&.sub {
|
||||
color: #7f7f7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
button {
|
||||
margin-left: 24px;
|
||||
&.edit {
|
||||
background-image: url('~src/assets/images/icons/edit_24dp.svg');
|
||||
}
|
||||
&.add-company {
|
||||
background-image: url('~src/assets/images/icons/note_add_20.svg');
|
||||
}
|
||||
&.add-person {
|
||||
background-image: url('~src/assets/images/icons/note_add_20.svg');
|
||||
}
|
||||
&.add-group {
|
||||
background-image: url('~src/assets/images/icons/add_folder_24.svg');
|
||||
}
|
||||
&.add-child {
|
||||
background-image: url('~src/assets/images/icons/add_library_dark_24.svg');
|
||||
}
|
||||
&.delete {
|
||||
background-image: url('~src/assets/images/icons/close_24dp.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.items {
|
||||
display: none;
|
||||
padding-left: 56px;
|
||||
}
|
||||
&.active {
|
||||
>.bar {
|
||||
.left {
|
||||
&:before {
|
||||
background-image: url('~src/assets/images/icons/circle_minus_24.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
>.items {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.item {
|
||||
.bar {
|
||||
.mid {
|
||||
padding: 0 12px;
|
||||
.info .logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.items {
|
||||
padding-left: 16px;
|
||||
}
|
||||
&.company {
|
||||
.items {
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {AdvisoriesService} from "@app/_services/advisories.service";
|
||||
import {Subscription} from "rxjs";
|
||||
import {ListsService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
selector: 'advisories-tree',
|
||||
templateUrl: 'advisories-tree.component.html',
|
||||
styleUrls: ['advisories-tree.component.scss']
|
||||
})
|
||||
export class AdvisoriesTreeComponent {
|
||||
public advisories: any[];
|
||||
public controlsSubscription?: Subscription;
|
||||
|
||||
constructor(private advisoriesService: AdvisoriesService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.controlsSubscription = this.listsService.controls().subscribe(val => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
fetch() {
|
||||
this.advisoriesService.show('main', {include: 'logo,permissions'}).subscribe(res => {
|
||||
this.advisories = [res.data];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<div class="item {{advisory?.type?.name}}" [class.main]="advisory.isMain" [class.active]="active" (click)="touched = true">
|
||||
<div class="bar">
|
||||
<div class="left" (click)="toggle()"></div>
|
||||
<div class="mid">
|
||||
<div class="info">
|
||||
<div class="logo">
|
||||
<img *ngIf="logo" src="{{logo}}" alt="" />
|
||||
<span *ngIf="!logo">{{noLogoLetters}}</span>
|
||||
</div>
|
||||
<div class="name">
|
||||
<p>{{advisory?.caption}}<span *ngIf="type?.name">. <span class="link" (click)="info()">Посмотреть</span></span></p>
|
||||
<p *ngIf="secretary" class="sub">Секретарь - {{secretary?.user?.data.name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="menu" *ngIf="isEditable">
|
||||
<button *ngIf="advisory.isMain" type="button" class="btn icon add-group" (click)="addGroup()"></button>
|
||||
<button type="button" class="btn icon add-child" (click)="addAdvisory()"></button>
|
||||
<!--button type="button" class="btn icon add-company" (click)="addCompanies()"></button-->
|
||||
<button type="button" class="btn icon edit" (click)="edit()"></button>
|
||||
<button *ngIf="isDeletable" type="button" class="btn icon delete" (click)="delete()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="items">
|
||||
<advisories-tree-company [advisoryCompany]="advisoryCompany" [parent]="advisory" *ngFor="let advisoryCompany of advisoryCompanies"></advisories-tree-company>
|
||||
<advisories-tree-advisory [advisory]="child" [parent]="advisory" *ngFor="let child of children"></advisories-tree-advisory>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {AdvisoriesService} from "@app/_services/advisories.service";
|
||||
import {FormsService, ListsService} from "@app/_services";
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'advisories-tree-advisory',
|
||||
templateUrl: 'advisories-tree-advisory.component.html',
|
||||
styleUrls: ['../advisories-tree.component.scss', 'advisories-tree-advisory.component.scss']
|
||||
})
|
||||
export class AdvisoriesTreeAdvisoryComponent {
|
||||
@Input() advisory: any;
|
||||
@Input() parent: any;
|
||||
@Input() active = false;
|
||||
public controlsSubscription?: Subscription;
|
||||
|
||||
public touched = false;
|
||||
|
||||
|
||||
constructor(private advisoriesService: AdvisoriesService, private listsService: ListsService, private formsService: FormsService) {
|
||||
}
|
||||
|
||||
|
||||
get type() {
|
||||
return this.advisory?.type;
|
||||
}
|
||||
get logo() {
|
||||
return this.advisory?.logo?.data.links?.full;
|
||||
}
|
||||
get noLogoLetters() {
|
||||
let result = '';
|
||||
if (this.type?.name === 'committee') result = this.advisory.isMain ? 'ТК' : 'ПК';
|
||||
if (this.type?.name === 'workgroup') result = 'РГ';
|
||||
return result;
|
||||
}
|
||||
get companies() {
|
||||
return this.advisoryCompanies.map(item => {return item.company?.data}) || [];
|
||||
}
|
||||
get advisoryCompanies() {
|
||||
return this.advisory?.advisoryCompanies?.data || [];
|
||||
}
|
||||
get children() {
|
||||
return this.advisory?.children?.data || [];
|
||||
}
|
||||
get secretary() {
|
||||
return this.advisory?.secretary?.data;
|
||||
}
|
||||
get permissions() {
|
||||
return this.advisory?.permissions;
|
||||
}
|
||||
get isEditable() {
|
||||
return this.permissions?.edit || this.permissions?.anything;
|
||||
}
|
||||
get isDeletable() {
|
||||
return this.isEditable && !this.advisory.isMain;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.controlsSubscription = this.listsService.controls(this.advisory.id).subscribe(val => {
|
||||
if (this.active || this.touched) this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.controlsSubscription) this.controlsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
fetch() {
|
||||
let include = ['logo', 'children.logo' ,'advisoryCompanies.company.logo', 'advisoryCompanies.voter.companyMember.user', 'secretary.user', 'children.secretary.user',
|
||||
'permissions', 'children.permissions', 'advisoryCompanies.permissions', 'advisoryCompanies.company.permissions'];
|
||||
//let include = 'logo,children.logo,advisoryCompanies.company.logo,permissions,children.permissions,advisoryCompanies.company.permissions';
|
||||
this.advisoriesService.show(this.advisory.id, {include: include.join(',')}).subscribe(res => {
|
||||
this.advisory = res.data;
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
edit() {
|
||||
this.formsService.editModel(this.type?.name ? 'advisory' : 'advisoryGroup', this.advisory.id, null, this.advisory.id);
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (confirm(`Удалить ${this.advisory.caption}`)) this.advisoriesService.delete(this.advisory.id).subscribe(res => {
|
||||
this.listsService.refresh(this.parent.id);
|
||||
});
|
||||
}
|
||||
|
||||
addAdvisory() {
|
||||
this.formsService.createModel('advisory', {extraProps: {parent: this.advisory.id}}, this.advisory.id);
|
||||
}
|
||||
|
||||
addGroup() {
|
||||
this.formsService.createModel('advisoryGroup', {extraProps: {parent: this.advisory.id}}, this.advisory.id);
|
||||
}
|
||||
|
||||
|
||||
info() {
|
||||
this.formsService.editModel('advisoryInfo', this.advisory.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
toggle() {
|
||||
if (!this.advisory.children) this.fetch();
|
||||
else this.active = !this.active;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<div class="item {{advisory?.type?.name || 'company'}}" [class.active]="active" (click)="touched = true">
|
||||
<div class="bar">
|
||||
<div class="left" (click)="toggle()"></div>
|
||||
<div class="mid">
|
||||
<div class="info">
|
||||
<div class="logo"><img *ngIf="logo" src="{{logo}}" alt="" /></div>
|
||||
<div class="name">
|
||||
<p>{{company?.name || advisory?.caption}}. <span class="link" *ngIf="isViewable" (click)="open()">{{company ? 'Перейти' : 'Посмотреть'}}</span></p>
|
||||
<p class="sub" *ngIf="voter">С правом голоса - {{voter?.user?.data.name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right" *ngIf="isEditable">
|
||||
<button type="button" class="btn icon add-person" (click)="addMembers()"></button>
|
||||
<button type="button" class="btn icon delete" (click)="delete()"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="items">
|
||||
<advisories-tree-member [advisoryMember]="member" [parent]="advisoryCompany" *ngFor="let member of members"></advisories-tree-member>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {Router} from "@angular/router";
|
||||
import {AdvisoryCompaniesService} from "@app/_services/advisory-companies.service";
|
||||
import {FormsService, ListsService} from "@app/_services";
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'advisories-tree-company',
|
||||
templateUrl: 'advisories-tree-company.component.html',
|
||||
styleUrls: ['../advisories-tree.component.scss', 'advisories-tree-company.component.scss']
|
||||
})
|
||||
export class AdvisoriesTreeCompanyComponent {
|
||||
@Input() advisoryCompany: any;
|
||||
@Input() parent: any;
|
||||
@Input() active = false;
|
||||
public controlsSubscription?: Subscription;
|
||||
|
||||
public touched = false;
|
||||
|
||||
|
||||
constructor(private advisoryCompaniesService: AdvisoryCompaniesService, private listsService: ListsService, private formsService: FormsService, private router: Router) {
|
||||
}
|
||||
|
||||
|
||||
get logo() {
|
||||
return this.advisory?.logo?.data.links?.full || this.company?.logo?.data.links?.full;
|
||||
}
|
||||
get company() {
|
||||
return this.advisoryCompany?.company?.data;
|
||||
}
|
||||
get advisory() {
|
||||
return this.advisoryCompany?.advisory?.data;
|
||||
}
|
||||
get members() {
|
||||
return this.advisoryCompany?.advisoryMembers?.data;
|
||||
}
|
||||
get voter() {
|
||||
return this.advisoryCompany?.voter?.data.companyMember?.data;
|
||||
}
|
||||
|
||||
get parentPermissions() {
|
||||
return this.parent?.permissions;
|
||||
}
|
||||
get isEditable() {
|
||||
return this.parentPermissions?.edit || this.parentPermissions?.anything;
|
||||
}
|
||||
get permissions() {
|
||||
return this.company?.permissions;
|
||||
}
|
||||
get isViewable() {
|
||||
return this.permissions?.view || this.permissions?.anything || this.isEditable;
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.controlsSubscription = this.listsService.controls(this.advisoryCompany.id).subscribe(val => {
|
||||
if (this.active || this.touched) this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.controlsSubscription) this.controlsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
|
||||
fetch() {
|
||||
this.advisoryCompaniesService.show(this.advisoryCompany.id, {include: 'advisoryMembers.companyMember.user.avatar,voter.companyMember.user'}).subscribe(res => {
|
||||
this.advisoryCompany.voter = res.data.voter;
|
||||
this.advisoryCompany.advisoryMembers = res.data.advisoryMembers;
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
toggle() {
|
||||
if (!this.members) this.fetch();
|
||||
else this.active = !this.active;
|
||||
}
|
||||
|
||||
open() {
|
||||
if (this.company) this.router.navigate(['companies', this.company?.id]).then();
|
||||
else this.formsService.editModel('advisoryInfo', this.advisory.id);
|
||||
}
|
||||
|
||||
addMembers() {
|
||||
this.formsService.editModel('advisoryCompanyMembers', this.advisoryCompany.id, null, this.advisoryCompany.id);
|
||||
}
|
||||
|
||||
|
||||
delete() {
|
||||
if (confirm('Исключить организацию из списка участников комитета?')) {
|
||||
this.advisoryCompaniesService.delete(this.advisoryCompany.id).subscribe(res => {
|
||||
this.listsService.refresh(this.parent?.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="item member">
|
||||
<div class="bar">
|
||||
<div class="mid">
|
||||
<div class="info">
|
||||
<div class="logo" [class.voter]="isVoter">
|
||||
<img *ngIf="avatar" src="{{avatar}}" alt="" />
|
||||
<div *ngIf="!avatar" class="initials">{{user?.abbreviationName}}</div>
|
||||
</div>
|
||||
<div class="name"><p>{{user?.name}}</p><p class="sub">{{member.position}}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right" *ngIf="isEditable">
|
||||
<button type="button" class="btn icon delete" (click)="delete()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {AdvisoryMembersService} from "@app/_services/advisory-members.service";
|
||||
import {ListsService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
selector: 'advisories-tree-member',
|
||||
templateUrl: 'advisories-tree-member.component.html',
|
||||
styleUrls: ['../advisories-tree.component.scss', 'advisories-tree-member.component.scss']
|
||||
})
|
||||
export class AdvisoriesTreeMemberComponent {
|
||||
@Input() advisoryMember: any;
|
||||
@Input() parent: any;
|
||||
|
||||
constructor(private advisoryMembersService: AdvisoryMembersService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
|
||||
get member() {
|
||||
return this.advisoryMember?.companyMember?.data;
|
||||
}
|
||||
get user() {
|
||||
return this.member?.user?.data;
|
||||
}
|
||||
get avatar() {
|
||||
return this.user.avatar?.data.links?.full;
|
||||
}
|
||||
get isVoter() {
|
||||
return this.advisoryMember?.rank.name === 'voter';
|
||||
}
|
||||
get parentPermissions() {
|
||||
return this.parent?.permissions;
|
||||
}
|
||||
get isEditable() {
|
||||
return this.parentPermissions?.edit || this.parentPermissions?.anything;
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
delete() {
|
||||
if (confirm('Исключить сотрудника из участников комитета?')) {
|
||||
this.advisoryMembersService.delete(this.advisoryMember.id).subscribe(res => {
|
||||
this.listsService.refresh(this.parent?.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterModule} from "@angular/router";
|
||||
import {AuthGuard} from "@app/_helpers";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {PaginationModule} from "@app/_modules/pagination/pagination.module";
|
||||
import {ApplicationsPageComponent} from "@app/_modules/applications/page/applications-page.component";
|
||||
import {ApplicationsListComponent} from "@app/_modules/applications/list/applications-list.component";
|
||||
import {ApplicationsListItemComponent} from "@app/_modules/applications/list/item/applications-list-item.component";
|
||||
|
||||
const routes = [
|
||||
{path: 'applications', component: ApplicationsPageComponent, canActivate: [AuthGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule.forRoot(routes),
|
||||
PaginationModule,
|
||||
],
|
||||
declarations: [
|
||||
ApplicationsPageComponent,
|
||||
ApplicationsListComponent,
|
||||
ApplicationsListItemComponent
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class ApplicationsModule {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<pagination></pagination>
|
||||
<div class="items">
|
||||
<applications-list-item *ngFor="let application of items" [application]="application"></applications-list-item>
|
||||
</div>
|
||||
<pagination></pagination>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
.items {
|
||||
margin: 24px 0;
|
||||
}
|
||||
applications-list-item {
|
||||
display: block;
|
||||
padding: 12px 24px;
|
||||
border-bottom: #E8E8E8 solid 1px;
|
||||
&:first-child {
|
||||
border-top: #E8E8E8 solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 526px) {
|
||||
applications-list-item {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {Subscription} from "rxjs";
|
||||
import {ListsService} from "@app/_services";
|
||||
import {ApplicationsService} from "@app/_services/applications.service";
|
||||
|
||||
@Component({
|
||||
selector: 'applications-list',
|
||||
templateUrl: 'applications-list.component.html',
|
||||
styleUrls: ['applications-list.component.scss']
|
||||
})
|
||||
export class ApplicationsListComponent {
|
||||
public items = <any>[];
|
||||
|
||||
private controlsSubscription: Subscription;
|
||||
private resultSubscription: Subscription;
|
||||
|
||||
constructor(private applicationsService: ApplicationsService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.controlsSubscription = this.listsService.controls().subscribe(controls => {
|
||||
this.fetch(controls);
|
||||
});
|
||||
this.resultSubscription = this.listsService.result().subscribe(res => {
|
||||
this.items = res?.data || [];
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.controlsSubscription?.unsubscribe();
|
||||
this.resultSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
fetch(controls: any) {
|
||||
let include = ['submitter', 'product', 'permissions'];
|
||||
this.applicationsService.list({page: controls.page || 0, filters: JSON.stringify(controls.filters), include: include.join(',')}).subscribe(res => {
|
||||
this.listsService.result().next(res);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<div class="main">
|
||||
<div class="left">
|
||||
<div class="title">{{application.title}}</div>
|
||||
<div class="status {{status?.name}}">{{status?.title}}</div>
|
||||
<table class="default">
|
||||
<tr><td class="caption">Hаименование продукции</td><td class="value">{{product?.name}}</td></tr>
|
||||
<tr><td class="caption">Hазначение продукции</td><td class="value">{{product?.purpose}}</td></tr>
|
||||
<tr><td class="caption">Заявитель</td><td class="value">{{application?.applicant}}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="right"><button type="button" class="toggle" [class.active]="active" (click)="toggle()"></button></div>
|
||||
</div>
|
||||
<div class="details" *ngIf="active">
|
||||
<table class="default">
|
||||
<tr><td class="caption">Дата формирования заявки</td><td class="value">{{application?.createdAt | date: 'dd.MM.yyyy HH:mm'}}</td></tr>
|
||||
<tr><td class="caption">Область применения продукции</td><td class="value">{{product?.usage || '—'}}</td></tr>
|
||||
<tr><td class="caption">Нормативно-технический документ</td><td class="value">{{product?.normative || '—'}}</td></tr>
|
||||
<tr><td class="caption">Изготовитель / разработчик</td><td class="value">{{product?.producer || '—'}}</td></tr>
|
||||
<tr><td class="caption">Ф.И.О. контактного лица</td><td class="value">{{submitter?.name || '—'}}</td></tr>
|
||||
<tr>
|
||||
<td class="caption">Документы</td>
|
||||
<td class="value">
|
||||
<div class="documents list default" *ngIf="documents?.length">
|
||||
<div class="items">
|
||||
<div class="item" *ngFor="let document of documents"><a [href]="document.links?.open" target="_blank">{{document.id}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<span *ngIf="!documents?.length">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="default" *ngIf="conclusion">
|
||||
<tr><td class="caption">Эксперт</td><td class="value">{{expert?.name}}</td></tr>
|
||||
<tr><td class="caption">Дата ответа</td><td class="value">{{conclusion?.createdAt | date: 'dd.MM.yyyy HH:mm'}}</td></tr>
|
||||
<tr><td class="caption">Мнение эксперта</td><td class="value" [innerHTML]="conclusion?.message"></td></tr>
|
||||
</table>
|
||||
<div class="reply" *ngIf="permissions?.reply && !conclusion"><button type="button" class="btn" (click)="reply()">Ответить</button></div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.right {
|
||||
margin-left: 24px;
|
||||
flex-shrink: 0;
|
||||
.toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent url('~src/assets/images/icons/expand_less_20.svg') 50% 50% no-repeat;
|
||||
transform: rotateZ(180deg);
|
||||
transition: transform 0.3s;
|
||||
&.active {
|
||||
transform: rotateZ(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status {
|
||||
margin-bottom: 8px;
|
||||
color: #86898E;
|
||||
&.completed {color: #28A814;}
|
||||
&.processing {color: #7714A8;}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
.reply {
|
||||
margin-top: 12px;
|
||||
.btn {
|
||||
padding: 7px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.documents.list.default {
|
||||
.items {
|
||||
.item {
|
||||
margin: 0 0 8px;
|
||||
&:last-child {margin: 0;}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {FormsService, ListsService, ObjectsService} from "@app/_services";
|
||||
import {ApplicationsService} from "@app/_services/applications.service";
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'applications-list-item',
|
||||
templateUrl: 'applications-list-item.component.html',
|
||||
styleUrls: ['applications-list-item.component.scss']
|
||||
})
|
||||
export class ApplicationsListItemComponent {
|
||||
@Input() application: any;
|
||||
public active = false;
|
||||
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(private applicationsService: ApplicationsService, private formsService: FormsService, private objectsService: ObjectsService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this.application?.status;
|
||||
}
|
||||
get submitter() {
|
||||
return this.application?.submitter?.data;
|
||||
}
|
||||
get product() {
|
||||
return this.application?.product?.data;
|
||||
}
|
||||
get conclusion() {
|
||||
return this.application?.conclusions?.data[0];
|
||||
}
|
||||
get expert() {
|
||||
return this.conclusion?.author?.data;
|
||||
}
|
||||
get properties() {
|
||||
return this.application?.properties?.data;
|
||||
}
|
||||
get documents() {
|
||||
return this.objectsService.getValue(this.properties, 'documents');
|
||||
}
|
||||
|
||||
get permissions() {
|
||||
return this.application?.permissions;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.subscription = this.listsService.controls(this.application.id).subscribe(controls => {
|
||||
if (this.active) this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
fetch() {
|
||||
let include = ['submitter', 'product', 'permissions', 'properties.groups.fields.value', 'conclusions.author'];
|
||||
this.applicationsService.show(this.application.id, {include: include.join(',')}).subscribe(res => {
|
||||
this.application = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
reply() {
|
||||
this.formsService.createModel('applicationConclusion', {extraProps: {application: this.application.id}}, this.application.id);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.active = !this.active;
|
||||
if (this.active) this.listsService.refresh(this.application.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<div class="top-panel pt-40">
|
||||
<h1>Журнал заявок</h1>
|
||||
<div class="buttons" *ngIf="!authService.isMainCompanyMember">
|
||||
<button type="button" class="btn create" (click)="create()">Сформировать заявку</button>
|
||||
</div>
|
||||
<div class="filters fullwidth" [formGroup]="filters">
|
||||
<div class="limiter">
|
||||
<div class="search">
|
||||
<label>Поиск</label>
|
||||
<input type="text" formControlName="search" />
|
||||
</div>
|
||||
<div class="advanced">
|
||||
<button type="button" (click)="showFilters()">Фильтр</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<applications-list></applications-list>
|
||||
<!--phonebook-list [companyId]="'main'"></phonebook-list-->
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {FormGroup, FormControl} from "@angular/forms";
|
||||
import {debounceTime} from "rxjs/operators";
|
||||
import {AuthenticationService, FiltersService, FormsService, ListsService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
templateUrl: 'applications-page.component.html',
|
||||
styleUrls: ['applications-page.component.scss']
|
||||
})
|
||||
export class ApplicationsPageComponent {
|
||||
public filters: FormGroup;
|
||||
|
||||
constructor(private listsService: ListsService, private formsService: FormsService, private filtersService: FiltersService, public authService: AuthenticationService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.filters = new FormGroup({search: new FormControl('')});
|
||||
this.filters.valueChanges.pipe(debounceTime(200)).subscribe(val => {
|
||||
this.listsService.addFilters(val);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
|
||||
create() {
|
||||
this.formsService.createModel('application');
|
||||
}
|
||||
|
||||
|
||||
showFilters() {
|
||||
this.filtersService.slider('model', 'applications');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import {NgModule} from '@angular/core'
|
||||
import {CommonModule} from '@angular/common'
|
||||
import {RouterModule} from "@angular/router";
|
||||
import {LoginComponent} from "@app/_modules/auth/login/login.component";
|
||||
import {BrowserModule} from "@angular/platform-browser";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {ForgetComponent} from "@app/_modules/auth/forget/forget.component";
|
||||
import {PasswordResetComponent} from "@app/_modules/auth/reset/password-reset.component";
|
||||
import {SignupComponent} from "@app/_modules/auth/signup/signup.component";
|
||||
import {AuthFormComponent} from "@app/_modules/auth/form/form.component";
|
||||
|
||||
const routes = [
|
||||
{path: 'login', component: AuthFormComponent, outlet: 'auth'},
|
||||
{path: 'signup', component: AuthFormComponent, outlet: 'auth'},
|
||||
{path: 'password/forget', component: AuthFormComponent, outlet: 'auth'},
|
||||
{path: 'password/reset/:token/:email', component: PasswordResetComponent}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes),
|
||||
],
|
||||
exports: [
|
||||
RouterModule,
|
||||
],
|
||||
declarations: [
|
||||
AuthFormComponent,
|
||||
LoginComponent,
|
||||
SignupComponent,
|
||||
ForgetComponent,
|
||||
PasswordResetComponent,
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<div class="popup">
|
||||
<form [formGroup]="form" (submit)="onSubmit()">
|
||||
<div class="header">
|
||||
<h2>Восстановление пароля</h2>
|
||||
<button type="button" class="close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="body" *ngIf="!success">
|
||||
<div class="field" [class.invalid]="email.invalid && email.touched">
|
||||
<label for="username">Электронная почта*</label>
|
||||
<input id="username" formControlName="email" type="email" />
|
||||
</div>
|
||||
<div *ngIf="error" class="error">{{error}}</div>
|
||||
<div class="description">Если указанный адрес зарегистрирован, то на него будет выслан новый пароль</div>
|
||||
</div>
|
||||
<div class="body success" *ngIf="success">
|
||||
<h3>Письмо с инструкциями было отправлено на указанный адрес.</h3>
|
||||
<p>Пожалуйста, проверьте почту и выполните указанные в письме инструкции.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="btn secondary" (click)="login()">Авторизоваться</button>
|
||||
<button *ngIf="!success" [disabled]="loading" type="submit" class="btn">Запросить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.success {
|
||||
h3, p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router, ActivatedRoute} from '@angular/router';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
|
||||
import {AuthenticationService} from '@app/_services';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-forget',
|
||||
templateUrl: 'forget.component.html',
|
||||
styleUrls: ['forget.component.scss', '../login/login.component.scss']
|
||||
})
|
||||
export class ForgetComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
loading = false;
|
||||
success = false;
|
||||
error = '';
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private route: ActivatedRoute, private router: Router, private authenticationService: AuthenticationService) {
|
||||
//if (this.authenticationService.oauthTokenValue) this.router.navigate(['']).then();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', [Validators.required, Validators.email]]
|
||||
});
|
||||
}
|
||||
|
||||
get f() {
|
||||
return this.form.controls;
|
||||
}
|
||||
get email() {
|
||||
return this.f?.email?.value;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.form.invalid) return;
|
||||
this.loading = true;
|
||||
this.authenticationService.forget(this.email).subscribe(res => {
|
||||
this.success = true;
|
||||
this.loading = false;
|
||||
}, error => {
|
||||
this.setError(error.message);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
setError(error: string) {
|
||||
let trans = {'The given data was invalid.': 'Указанный адрес почты не найден'};
|
||||
this.error = trans[error] || error;
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authenticationService.popup('login');
|
||||
}
|
||||
|
||||
close() {
|
||||
this.authenticationService.popup(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<div class="auth">
|
||||
<div class="form">
|
||||
<auth-login *ngIf="path==='login'"></auth-login>
|
||||
<auth-signup *ngIf="path==='signup'"></auth-signup>
|
||||
<auth-forget *ngIf="path==='password/forget'"></auth-forget>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
.auth {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
background-color: rgba(62, 61, 64, 70%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
.form {
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
max-height: 632px;
|
||||
height: 100%;
|
||||
border-radius: 24px;
|
||||
background-color: var(--white);
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.auth {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {Subscription} from "rxjs";
|
||||
import {AuthenticationService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
templateUrl: 'form.component.html',
|
||||
styleUrls: ['form.component.scss']
|
||||
})
|
||||
export class AuthFormComponent implements OnInit {
|
||||
public subscription: Subscription;
|
||||
|
||||
constructor(private authService: AuthenticationService, private route: ActivatedRoute) {
|
||||
this.subscription = this.authService.user.subscribe(user => {
|
||||
if (user) window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
|
||||
get path() {
|
||||
return this.route?.snapshot?.routeConfig?.path;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<div class="popup">
|
||||
<form [formGroup]="form" (submit)="onSubmit()">
|
||||
<div class="header">
|
||||
<h2>Вход</h2>
|
||||
<button type="button" class="close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field" [class.invalid]="username.invalid && username.touched">
|
||||
<label for="username">Логин</label>
|
||||
<input id="username" formControlName="username" type="email" />
|
||||
</div>
|
||||
<div class="field" [class.invalid]="password.invalid && password.touched">
|
||||
<label for="password">Пароль</label>
|
||||
<input id="password" formControlName="password" [type]="type" autocomplete="off" />
|
||||
<div class="eye {{type==='password'?'show':'hide'}}" (click)="type=type==='password'?'text':'password'"></div>
|
||||
</div>
|
||||
<div *ngIf="error" class="error">{{error}}</div>
|
||||
<div class="bar">
|
||||
<div class="remember">
|
||||
<input id="remember" type="checkbox" />
|
||||
<label for="remember">Запомнить меня</label>
|
||||
</div>
|
||||
<div class="forget" (click)="forget()">Забыли пароль?</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="btn secondary" (click)="signup()">Зарегистрироваться</button>
|
||||
<button [disabled]="loading" type="submit" class="btn">Войти</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
.popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.header {
|
||||
flex-grow: 0;
|
||||
border-radius: 24px 24px 0 0;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #F5F4F4;
|
||||
border-bottom: 1px solid #3E3D40;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
line-height: 30px;
|
||||
color: #000000;
|
||||
}
|
||||
.close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: auto;
|
||||
background: transparent url(/assets/images/icons/close_24dp.svg) 50% 50% no-repeat;
|
||||
}
|
||||
}
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px 24px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
row-gap: 24px;
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
position: relative;
|
||||
label {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: normal;
|
||||
}
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #BFBFBF;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.eye {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
&.show {background-image: url("~src/assets/images/icons/visibility_on_24dp.svg");}
|
||||
&.hide {background-image: url("~src/assets/images/icons/visibility_off_24dp.svg");}
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 18px;
|
||||
.remember {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #86898E;
|
||||
}
|
||||
}
|
||||
.forget {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
border-top: 1px solid #3E3D40;
|
||||
border-radius: 0 0 24px 24px;
|
||||
padding: 20px 24px;
|
||||
justify-content: flex-end;
|
||||
background-color: #F5F4F4;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
.btn {
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
.authentication {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 76px;
|
||||
margin-bottom: 50px;
|
||||
background-color: transparent;
|
||||
background-size: contain;
|
||||
}
|
||||
.block {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.popup {
|
||||
form {
|
||||
.header, .footer {
|
||||
border-radius: 0;
|
||||
}
|
||||
.footer {
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router, ActivatedRoute} from '@angular/router';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
|
||||
import {AuthenticationService} from '@app/_services';
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'auth-login',
|
||||
templateUrl: 'login.component.html',
|
||||
styleUrls: ['login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
loading = false;
|
||||
error = '';
|
||||
subscription: Subscription;
|
||||
public type: string = 'password';
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private router: Router, private route: ActivatedRoute, private authenticationService: AuthenticationService) {
|
||||
//this.subscription = this.authenticationService.user.subscribe(user => {
|
||||
// if (user) this.router.navigate([this.route.snapshot.queryParamMap.get('returnUrl')?.split('(')[0] || '']).then();
|
||||
//});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
username: ['', [Validators.required, Validators.email]],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
get f() {
|
||||
return this.form.controls;
|
||||
}
|
||||
get username() {
|
||||
return this.f.username.value;
|
||||
}
|
||||
get password() {
|
||||
return this.f.password.value;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.form.invalid) return;
|
||||
this.loading = true;
|
||||
this.authenticationService.getToken(this.username, this.password).subscribe(res => {
|
||||
this.authenticationService.saveToken(res);
|
||||
}, error => {
|
||||
this.setError(error.message);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
setError(error: string) {
|
||||
let trans = {'The user credentials were incorrect.': 'Имя пользователя или пароль указаны неверно.'};
|
||||
this.error = trans[error] || error;
|
||||
}
|
||||
|
||||
forget() {
|
||||
this.authenticationService.popup(['password','forget']);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.authenticationService.popup(null);
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.authenticationService.popup('signup');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<div class="authentication">
|
||||
<div class="logo"></div>
|
||||
<div class="block">
|
||||
<form [formGroup]="form" (submit)="onSubmit()">
|
||||
<div class="form-title">Сброс пароля</div>
|
||||
<div class="field password">
|
||||
<label for="password">Укажите новый пароль</label>
|
||||
<input id="password" formControlName="password" type="password" />
|
||||
</div>
|
||||
<div class="field confirmation">
|
||||
<label for="confirmation">Новый пароль еще раз</label>
|
||||
<input id="confirmation" formControlName="confirmation" type="password" />
|
||||
</div>
|
||||
<div *ngIf="error" class="error">{{error}}</div>
|
||||
<div class="bar">
|
||||
<div class="forget"><a routerLink="/login">Авторизация</a></div>
|
||||
</div>
|
||||
<div class="submit">
|
||||
<button [disabled]="loading" class="btn">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router, ActivatedRoute} from '@angular/router';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
|
||||
import {AuthenticationService} from '@app/_services';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'password-reset.component.html',
|
||||
styleUrls: ['password-reset.component.scss', '../login/login.component.scss']
|
||||
})
|
||||
export class PasswordResetComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private route: ActivatedRoute, private router: Router, private authenticationService: AuthenticationService) {
|
||||
//if (this.authenticationService.oauthTokenValue) this.router.navigate(['']).then();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
password: ['', [Validators.required, Validators.minLength(8)]],
|
||||
confirmation: ['', [Validators.required]]
|
||||
});
|
||||
}
|
||||
|
||||
get f() {
|
||||
return this.form.controls;
|
||||
}
|
||||
get password() {
|
||||
return this.f?.password?.value;
|
||||
}
|
||||
get confirmation() {
|
||||
return this.f?.confirmation?.value;
|
||||
}
|
||||
get token() {
|
||||
return this.route.snapshot.paramMap.get('token');
|
||||
}
|
||||
get email() {
|
||||
return this.route.snapshot.paramMap.get('email');
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.error = '';
|
||||
if (this.form.invalid) return;
|
||||
if (this.password !== this.confirmation) {
|
||||
this.setError('Пароль и подтверждение пароля не совпадают');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.authenticationService.restore({email: this.email, token: this.token, password: this.password}).subscribe(res => {
|
||||
this.router.navigate(['login']).then();
|
||||
}, error => {
|
||||
this.setError(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
setError(error: string) {
|
||||
let trans = {'The user credentials were incorrect.': 'Имя пользователя или пароль указаны неверно.'};
|
||||
this.error = trans[error] || error;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<div class="popup">
|
||||
<form [formGroup]="form" (submit)="onSubmit()">
|
||||
<div class="header">
|
||||
<h2>Регистрация</h2>
|
||||
<button type="button" class="close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field" [class.invalid]="email.invalid && email.touched">
|
||||
<label for="email">Электронная почта*</label>
|
||||
<input id="email" formControlName="email" type="email" />
|
||||
<!--p *ngIf="email.errors?.required">Поле обязательно для заполнения</p-->
|
||||
<p *ngIf="email.errors?.email">Адрес почты указан не корректно</p>
|
||||
<p *ngFor="let err of asyncErrors.email">{{err}}</p>
|
||||
</div>
|
||||
<div class="field" [class.invalid]="name.invalid && name.touched">
|
||||
<label for="name">Фамилия, имя, отчество*</label>
|
||||
<input id="name" formControlName="name" type="text" />
|
||||
<!--p *ngIf="name.errors?.required">Поле обязательно для заполнения</p-->
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="phone">Телефон</label>
|
||||
<input id="phone" formControlName="phone" type="text" />
|
||||
</div>
|
||||
<div class="field" [class.invalid]="password.invalid && password.touched">
|
||||
<label for="password">Пароль</label>
|
||||
<input id="password" formControlName="password" type="password" />
|
||||
<!--p *ngIf="password.errors?.required">Поле обязательно для заполнения</p-->
|
||||
<p *ngFor="let err of asyncErrors.password">{{err}}</p>
|
||||
</div>
|
||||
<div class="field" [class.invalid]="confirmation.invalid && confirmation.touched">
|
||||
<label for="confirmation">Подтверждение пароля*</label>
|
||||
<input id="confirmation" formControlName="passwordConfirmation" type="password" />
|
||||
<!--p *ngIf="confirmation.errors?.required">Поле обязательно для заполнения</p-->
|
||||
</div>
|
||||
<div *ngIf="error" class="error">{{error}}</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="btn secondary" (click)="login()">Авторизоваться</button>
|
||||
<button type="submit" [disabled]="loading" class="btn">Зарегистрироваться</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router, ActivatedRoute} from '@angular/router';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
|
||||
import {AuthenticationService} from '@app/_services';
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'auth-signup',
|
||||
templateUrl: 'signup.component.html',
|
||||
styleUrls: ['signup.component.scss', '../login/login.component.scss']
|
||||
})
|
||||
export class SignupComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
loading = false;
|
||||
error = '';
|
||||
asyncErrors: any = {};
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private router: Router, private route: ActivatedRoute, private authenticationService: AuthenticationService) {
|
||||
//this.subscription = this.authenticationService.user.subscribe(user => {
|
||||
// if (user) this.router.navigate([this.route.snapshot.queryParamMap.get('returnUrl')?.split('(')[0] || 'applications']).then();
|
||||
//});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
name: ['', Validators.required],
|
||||
phone: [''],
|
||||
password: ['', Validators.required],
|
||||
passwordConfirmation: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
get f() {
|
||||
return this.form.controls;
|
||||
}
|
||||
get email() {
|
||||
return this.f.email;
|
||||
}
|
||||
get name() {
|
||||
return this.f.name;
|
||||
}
|
||||
get password() {
|
||||
return this.f.password;
|
||||
}
|
||||
get confirmation() {
|
||||
return this.f.passwordConfirmation;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.form.markAllAsTouched();
|
||||
if (this.form.invalid) return;
|
||||
this.loading = true;
|
||||
this.authenticationService.signup(this.form.value).subscribe(res => {
|
||||
this.authenticationService.getToken(this.email.value, this.password.value).subscribe(res => {
|
||||
this.authenticationService.saveToken(res);
|
||||
this.close();
|
||||
});
|
||||
}, error => {
|
||||
this.setError(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
setError(error: any) {
|
||||
this.asyncErrors = error.errors;
|
||||
for (let prop in error.errors) {
|
||||
if (error.errors.hasOwnProperty(prop)) this.f[prop].setErrors({custom: true});
|
||||
}
|
||||
let trans = {'The given data was invalid.': 'Проверьте правильность заполнения формы'};
|
||||
this.error = trans[error.message] || error.message;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.authenticationService.popup(null);
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authenticationService.popup('login');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<div class="tree default" *ngIf="advisoryCompanies">
|
||||
<div class="items">
|
||||
<advisories-tree-company [advisoryCompany]="advisoryCompany" *ngFor="let advisoryCompany of advisoryCompanies"></advisories-tree-company>
|
||||
<div class="none" *ngIf="!advisoryCompanies.length">Организация не является членом ТК или ПК</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {CompaniesService} from "@app/_services/companies.service";
|
||||
|
||||
@Component({
|
||||
selector: 'company-advisories',
|
||||
templateUrl: 'company-advisories.component.html',
|
||||
styleUrls: ['company-advisories.component.scss']
|
||||
})
|
||||
export class CompanyAdvisoriesComponent {
|
||||
@Input() companyId: string;
|
||||
@Input() editable = false;
|
||||
public company: any;
|
||||
|
||||
constructor(private companiesService: CompaniesService) {
|
||||
}
|
||||
|
||||
|
||||
get advisoryCompanies() {
|
||||
return this.company?.advisoryCompanies?.data;
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
|
||||
fetch() {
|
||||
this.companiesService.fetch(this.companyId, {include: 'advisoryCompanies.advisory.logo'}).subscribe(res => {
|
||||
this.company = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import {NgModule} from '@angular/core'
|
||||
import {CommonModule} from '@angular/common'
|
||||
import {CompanyMembersComponent} from "@app/_modules/companies/members/company-members.component";
|
||||
import {CompanyStructureComponent} from "@app/_modules/companies/structure/company-structure.component";
|
||||
import {CompanyDepartmentComponent} from "@app/_modules/companies/structure/department/company-department.component";
|
||||
import {CompanyMemberComponent} from "@app/_modules/companies/members/member/company-member.component";
|
||||
import {CompanyAdvisoriesComponent} from "@app/_modules/companies/advisories/company-advisories.component";
|
||||
import {CompanyComponent} from "@app/_modules/companies/company/company.component";
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
declarations: [
|
||||
CompanyComponent,
|
||||
CompanyMembersComponent,
|
||||
CompanyMemberComponent,
|
||||
CompanyStructureComponent,
|
||||
CompanyDepartmentComponent,
|
||||
CompanyAdvisoriesComponent
|
||||
],
|
||||
exports: [
|
||||
CompanyComponent,
|
||||
CompanyMembersComponent,
|
||||
CompanyStructureComponent,
|
||||
CompanyAdvisoriesComponent
|
||||
]
|
||||
})
|
||||
export class CompaniesModule {}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<div class="main">
|
||||
<div class="left"><div class="logo"><img *ngIf="logo" src="{{logo}}" alt="" /></div></div>
|
||||
<div class="mid"><div class="name">{{company?.name}}</div></div>
|
||||
<div class="right"><div class="menu" *ngIf="isEditable"><button type="button" title="Редактировать параметры организации" class="edit" (click)="edit()"></button></div></div>
|
||||
</div>
|
||||
<table class="default">
|
||||
<tr><td class="caption">Юридический адрес</td><td class="value">{{company?.legalAddress?.data.full}}</td></tr>
|
||||
<tr><td class="caption">Электронная почта</td><td class="value">{{emails}}</td></tr>
|
||||
<tr><td class="caption">Телефон</td><td class="value">{{phones}}</td></tr>
|
||||
<!--tr *ngIf="isEditable"><td colspan="2"><span class="link" (click)="edit()">Изменить</span></td></tr-->
|
||||
</table>
|
||||
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
.left {
|
||||
margin-right: 16px;
|
||||
.logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 100%;
|
||||
border: #E8E8E8 solid 1px;
|
||||
background-size: 60%;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
.mid {
|
||||
.name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-left: auto;
|
||||
.menu {
|
||||
button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent url('~src/assets/images/icons/edit_24dp.svg') 50% 50% no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.intro {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.left {
|
||||
.logo {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
margin-right: 16px;
|
||||
border-radius: 100%;
|
||||
border: #0033661F solid 2px;
|
||||
//background: #ffffff url('~src/assets/images/icons/factory_96dp.svg') 50% 50% no-repeat;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
.line {
|
||||
margin-bottom: 6px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 40px;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: #6c6c6c solid 1px;
|
||||
.properties {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16px;
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
.title {
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-left: 24px;
|
||||
flex-shrink: 0;
|
||||
.edit {
|
||||
background-image: url('~src/assets/images/icons/edit_24dp.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 983px) {
|
||||
.content {
|
||||
padding: 24px;
|
||||
.properties {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.intro {
|
||||
.left {
|
||||
.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-size: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
.properties {
|
||||
.left {
|
||||
.default {
|
||||
tr {
|
||||
td {
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 12px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
.main {
|
||||
.left {
|
||||
margin-right: 0;
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {FormsService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
selector: 'company',
|
||||
templateUrl: 'company.component.html',
|
||||
styleUrls: ['company.component.scss']
|
||||
})
|
||||
export class CompanyComponent {
|
||||
@Input() company: any;
|
||||
|
||||
constructor(private formsService: FormsService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
get logo() {
|
||||
return this.company?.logo?.data.links?.full;
|
||||
}
|
||||
get phones() {
|
||||
return this.company?.phones?.data.map(item => {
|
||||
return item.value;
|
||||
}).join('; ') || 'не указан';
|
||||
}
|
||||
get emails() {
|
||||
return this.company?.emails?.data.map(item => {
|
||||
return item.value;
|
||||
}).join('; ') || 'не указана';
|
||||
}
|
||||
get types() {
|
||||
return this.company?.types?.data.map(item => {
|
||||
return item.title;
|
||||
}).join(', ');
|
||||
}
|
||||
get permissions() {
|
||||
return this.company?.permissions;
|
||||
}
|
||||
get isViewable() {
|
||||
return this.permissions?.view || this.permissions?.anything;
|
||||
}
|
||||
get isEditable() {
|
||||
return this.permissions?.edit || this.permissions?.anything;
|
||||
}
|
||||
|
||||
|
||||
|
||||
edit() {
|
||||
this.formsService.editModel('company', this.company.id, null, 'company-main-info');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<company-member [member]="member" [editable]="editable" *ngFor="let member of members"></company-member>
|
||||
<div class="none" *ngIf="!members?.length">Сотрудники отсутствуют</div>
|
||||
<!--div class="buttons"><button type="button" class="btn">ДОБАВИТЬ СОТРУДНИКА</button></div-->
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.buttons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {CompaniesService} from "@app/_services/companies.service";
|
||||
import {Subscription} from "rxjs";
|
||||
import {ListsService} from "@app/_services";
|
||||
|
||||
@Component({
|
||||
selector: 'company-members',
|
||||
templateUrl: 'company-members.component.html',
|
||||
styleUrls: ['company-members.component.scss']
|
||||
})
|
||||
export class CompanyMembersComponent {
|
||||
@Input() companyId: string;
|
||||
@Input() editable = false;
|
||||
public company: any;
|
||||
public controlsSubscription?: Subscription;
|
||||
|
||||
constructor(private companiesService: CompaniesService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
|
||||
get members() {
|
||||
return this.company?.members?.data;
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.controlsSubscription = this.listsService.controls().subscribe(val => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.controlsSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
fetch() {
|
||||
this.companiesService.fetch(this.companyId, {include: 'members.user.avatar'}).subscribe(res => {
|
||||
this.company = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="member">
|
||||
<div class="left">
|
||||
<div class="avatar">
|
||||
<img *ngIf="avatar" [src]="avatar" alt="" />
|
||||
<div class="initials" *ngIf="!avatar">{{user?.initials}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mid">
|
||||
<div class="name">{{user?.name}}. {{member.position}}</div>
|
||||
<div class="sub">{{role?.title}}</div>
|
||||
</div>
|
||||
<div class="right" *ngIf="editable">
|
||||
<button type="button" title="Редактировать" class="btn icon edit" (click)="edit()"></button>
|
||||
<button type="button" title="Удалить" class="btn icon delete" (click)="delete()"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
.member {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
border-bottom: #E0E0E0 solid 1px;
|
||||
overflow: hidden;
|
||||
.left {
|
||||
flex-shrink: 0;
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 100px;
|
||||
border: #86898E solid 1px;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
.initials {
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
color: #86898E;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
.mid {
|
||||
padding: 0 16px;
|
||||
flex-grow: 1;
|
||||
.name {
|
||||
}
|
||||
.sub {
|
||||
color: #86898E;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
flex-shrink: 0;
|
||||
button {
|
||||
margin-left: 20px;
|
||||
&.edit {background-image: url('~src/assets/images/icons/edit_24dp.svg');}
|
||||
&.delete {background-image: url('~src/assets/images/icons/close_24dp.svg');}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
.member {
|
||||
.left {
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {FormsService, ListsService} from "@app/_services";
|
||||
import {CompanyMembersService} from "@app/_services/company-members.service";
|
||||
|
||||
@Component({
|
||||
selector: 'company-member',
|
||||
templateUrl: 'company-member.component.html',
|
||||
styleUrls: ['company-member.component.scss']
|
||||
})
|
||||
export class CompanyMemberComponent {
|
||||
@Input() member: any;
|
||||
@Input() parent: any;
|
||||
|
||||
@Input() editable = false;
|
||||
@Input() draggable = false;
|
||||
|
||||
constructor(private membersService: CompanyMembersService, private formsService: FormsService, private listsService: ListsService) {
|
||||
}
|
||||
|
||||
get user() {
|
||||
return this.member?.user?.data;
|
||||
}
|
||||
get phone() {
|
||||
return this.user?.phone;
|
||||
}
|
||||
get email() {
|
||||
return this.user?.email;
|
||||
}
|
||||
get avatar() {
|
||||
return this.user?.avatar?.data.links?.thumb;
|
||||
}
|
||||
get role() {
|
||||
return this.member?.role;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
|
||||
edit() {
|
||||
this.formsService.editModel('companyMember', this.member.id, null, this.parent?.id || null);
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (confirm(`Удалить сотрудника «${this.user.name}»?`)) this.membersService.delete(this.member.id).subscribe(res => {
|
||||
this.listsService.refresh(this.parent.id);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<div class="structure" *ngIf="company">
|
||||
<company-department [department]="department" [editable]="editable" [active]="true"></company-department>
|
||||
<company-member [member]="member" [editable]="editable" *ngFor="let member of members"></company-member>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {CompaniesService} from "@app/_services/companies.service";
|
||||
|
||||
@Component({
|
||||
selector: 'company-structure',
|
||||
templateUrl: 'company-structure.component.html',
|
||||
styleUrls: ['company-structure.component.scss']
|
||||
})
|
||||
export class CompanyStructureComponent {
|
||||
@Input() companyId: string;
|
||||
@Input() editable = false;
|
||||
public company: any;
|
||||
|
||||
constructor(private companiesService: CompaniesService) {
|
||||
}
|
||||
|
||||
|
||||
get department() {
|
||||
return this.company?.rootDepartment?.data;
|
||||
}
|
||||
|
||||
get members() {
|
||||
return this.company?.rootMembers?.data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
fetch() {
|
||||
this.companiesService.fetch(this.companyId, {include: 'rootDepartment.children,rootDepartment.members.user.avatar,rootMembers.user'}).subscribe(res => {
|
||||
this.company = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<div class="department" [class.active]="active" (click)="touched = true">
|
||||
<div class="bar">
|
||||
<div class="left" (click)="toggle()"></div>
|
||||
<div class="logo" (click)="toggle()">{{ftl}}</div>
|
||||
<div class="mid" (click)="toggle()"><div class="name">{{department?.title}}</div></div>
|
||||
<div class="right" *ngIf="editable">
|
||||
<button type="button" title="Создать подразделение" class="btn icon add-department" (click)="addDepartment()"></button>
|
||||
<button type="button" title="Добавить сотрудника" class="btn icon add-member" (click)="addMember()"></button>
|
||||
<button type="button" title="Редактировать" class="btn icon edit" (click)="edit()"></button>
|
||||
<button *ngIf="isDeletable" title="Удалить" type="button" class="btn icon delete" (click)="delete()"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="items">
|
||||
<company-member [member]="member" [parent]="department" [editable]="editable" *ngFor="let member of members"></company-member>
|
||||
<company-department [department]="dep" [parent]="department" [editable]="editable" *ngFor="let dep of departments"></company-department>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
.department {
|
||||
.bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: #E8E8E8 solid 1px;
|
||||
cursor: pointer;
|
||||
.left {
|
||||
flex-shrink: 0;
|
||||
&:before {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
background: transparent url("~src/assets/images/icons/circle_plus_24.svg") 50% 50% no-repeat;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 16px;
|
||||
border-radius: 200px;
|
||||
background-color: #F9B417;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.mid {
|
||||
flex-grow: 1;
|
||||
padding: 0 16px;
|
||||
.name {
|
||||
}
|
||||
}
|
||||
.right {
|
||||
flex-shrink: 0;
|
||||
button {
|
||||
margin-left: 20px;
|
||||
&.edit {
|
||||
background-image: url('~src/assets/images/icons/edit_24dp.svg');
|
||||
}
|
||||
&.add-member {
|
||||
background-image: url('~src/assets/images/icons/add_person_24.svg');
|
||||
}
|
||||
&.add-department {
|
||||
background-image: url('~src/assets/images/icons/add_library_dark_24.svg');
|
||||
}
|
||||
&.delete {
|
||||
background-image: url('~src/assets/images/icons/close_24dp.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.items {
|
||||
display: none;
|
||||
padding-left: 56px;
|
||||
}
|
||||
&.active {
|
||||
>.bar {
|
||||
.left {
|
||||
&:before {
|
||||
background-image: url("~src/assets/images/icons/circle_minus_24.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
>.items {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.department {
|
||||
.bar {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
//.right {display: none;}
|
||||
}
|
||||
.items {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {DepartmentsService} from "@app/_services/departments.service";
|
||||
import {FormsService, ListsService} from "@app/_services";
|
||||
import {Subscription} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'company-department',
|
||||
templateUrl: 'company-department.component.html',
|
||||
styleUrls: ['company-department.component.scss']
|
||||
})
|
||||
export class CompanyDepartmentComponent {
|
||||
@Input() department: any;
|
||||
@Input() active = false;
|
||||
@Input() parent: any;
|
||||
@Input() editable = false;
|
||||
public controlsSubscription?: Subscription;
|
||||
|
||||
public touched = false;
|
||||
|
||||
constructor(private departmentsService: DepartmentsService, private listsService: ListsService, private formsService: FormsService) {
|
||||
}
|
||||
|
||||
|
||||
get ftl() {
|
||||
return this.department.title.substring(0, 2);
|
||||
}
|
||||
get isRoot() {
|
||||
return this.department.name === 'root';
|
||||
}
|
||||
get isDeletable() {
|
||||
return !this.isRoot;
|
||||
}
|
||||
|
||||
get departments() {
|
||||
return this.department?.children?.data || [];
|
||||
}
|
||||
get members() {
|
||||
return this.department?.members?.data || [];
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.controlsSubscription = this.listsService.controls(this.department?.id).subscribe(val => {
|
||||
if (this.active || this.touched) this.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.controlsSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
fetch() {
|
||||
this.departmentsService.fetch(this.department?.id, {include: 'members.user.avatar,children'}).subscribe(res => {
|
||||
this.department = res.data;
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
edit() {
|
||||
this.formsService.editModel('department', this.department.id, null, this.department.id);
|
||||
}
|
||||
|
||||
addMember() {
|
||||
this.formsService.createModel('companyMember', {extraProps: {department: this.department.id}}, this.department.id);
|
||||
}
|
||||
|
||||
addDepartment() {
|
||||
this.formsService.createModel('department', {extraProps: {department: this.department.id}}, this.department.id);
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (confirm(`Удалить подразделение «${this.department.title}»?`)) this.departmentsService.delete(this.department.id).subscribe(res => {
|
||||
this.listsService.refresh(this.parent.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
toggle() {
|
||||
if (!this.department.children) this.fetch();
|
||||
else this.active = !this.active;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div class="field" [class.hidden]="field.hidden">
|
||||
<div class="caption">
|
||||
<label for="{{field.name}}">{{field.title}}</label>
|
||||
</div>
|
||||
<div class="value">
|
||||
<div class="control" [ngSwitch]="field.type">
|
||||
<filter-field-boolean *ngSwitchCase="'boolean'" [field]="field" [formGroup]="formGroup"></filter-field-boolean>
|
||||
<filter-field-date *ngSwitchCase="'date'" [field]="field" [formGroup]="formGroup"></filter-field-date>
|
||||
<filter-field-datetime *ngSwitchCase="'datetime'" [field]="field" [formGroup]="formGroup"></filter-field-datetime>
|
||||
<filter-field-float *ngSwitchCase="'float'" [field]="field" [formGroup]="formGroup"></filter-field-float>
|
||||
<filter-field-integer *ngSwitchCase="'integer'" [field]="field" [formGroup]="formGroup"></filter-field-integer>
|
||||
<filter-field-relation *ngSwitchCase="'relation'" [field]="field" [formGroup]="formGroup"></filter-field-relation>
|
||||
<filter-field-string *ngSwitchCase="'string'" [field]="field" [formGroup]="formGroup"></filter-field-string>
|
||||
<filter-field-text *ngSwitchCase="'text'" [field]="field" [formGroup]="formGroup"></filter-field-text>
|
||||
<p *ngSwitchDefault>field type {{field.type}} filter is undefined</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
margin: 0 0 24px;
|
||||
.caption {
|
||||
width: 256px;
|
||||
margin-right: 16px;
|
||||
padding: 10px 0 2px;
|
||||
flex-shrink: 0;
|
||||
color: #6c6c6c;
|
||||
}
|
||||
.value {
|
||||
flex-grow: 1;
|
||||
}
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 983px) {
|
||||
.field {
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {FormControl, FormGroup} from "@angular/forms";
|
||||
import {FormFieldsService} from "@app/_services/form-fields.service";
|
||||
|
||||
@Component({
|
||||
selector: 'filter-field',
|
||||
templateUrl: 'filter-field.component.html',
|
||||
styleUrls: ['filter-field.component.scss']
|
||||
})
|
||||
export class FilterFieldComponent {
|
||||
@Input() field!: any;
|
||||
@Input() formGroup!: FormGroup;
|
||||
|
||||
constructor(private fieldsService: FormFieldsService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.field.value = {data: this.fieldsService.prepareFieldValue(this.field)};
|
||||
this.formGroup.addControl(this.field.name, new FormControl(), {emitEvent: false});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {
|
||||
FilterFieldBooleanModule,
|
||||
FilterFieldDateModule,
|
||||
FilterFieldDatetimeModule,
|
||||
FilterFieldFloatModule,
|
||||
FilterFieldIntegerModule,
|
||||
FilterFieldRelationModule,
|
||||
FilterFieldStringModule,
|
||||
FilterFieldTextModule,
|
||||
} from "@app/_modules/filter-fields/types";
|
||||
import {FilterFieldComponent} from "@app/_modules/filter-fields/field/filter-field.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FilterFieldStringModule,
|
||||
FilterFieldTextModule,
|
||||
FilterFieldIntegerModule,
|
||||
FilterFieldFloatModule,
|
||||
FilterFieldBooleanModule,
|
||||
FilterFieldDateModule,
|
||||
FilterFieldDatetimeModule,
|
||||
FilterFieldRelationModule,
|
||||
],
|
||||
declarations: [
|
||||
FilterFieldComponent
|
||||
],
|
||||
exports: [
|
||||
FilterFieldComponent
|
||||
]
|
||||
})
|
||||
export class FilterFieldsModule {}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<div class="radio" [formGroup]="formGroup">
|
||||
<label><input type="radio" [formControlName]="field.name" value=""> Не важно</label>
|
||||
<label><input type="radio" [formControlName]="field.name" value="yes"> Есть</label>
|
||||
<label><input type="radio" [formControlName]="field.name" value="no"> Нет</label>
|
||||
<!--select [formControlName]="field.name">
|
||||
<option value="">Не важно</option>
|
||||
<option value="yes">Присутствует</option>
|
||||
<option value="no">Отсутствует</option>
|
||||
</select-->
|
||||
</div>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
.radio {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 36px;
|
||||
}
|
||||
input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {FormGroup} from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: 'filter-field-boolean',
|
||||
templateUrl: 'filter-field-boolean.component.html',
|
||||
styleUrls: ['filter-field-boolean.component.scss']
|
||||
})
|
||||
export class FilterFieldBooleanComponent {
|
||||
@Input() field: any;
|
||||
@Input() formGroup: FormGroup;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.control.setValue(this.initialValue, {emitEvent: false});
|
||||
}
|
||||
|
||||
get initialValue() {
|
||||
return this.field.value?.data[0] || '';
|
||||
}
|
||||
get control() {
|
||||
return this.formGroup.controls[this.field.name];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FilterFieldBooleanComponent} from "@app/_modules/filter-fields/types/boolean/filter-field-boolean.component";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
declarations: [
|
||||
FilterFieldBooleanComponent
|
||||
],
|
||||
exports: [
|
||||
FilterFieldBooleanComponent
|
||||
]
|
||||
})
|
||||
export class FilterFieldBooleanModule {}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<div class="row" [formGroup]="controlGroup">
|
||||
<input formControlName="gt" [matDatepicker]="gt" (click)="gt.open()" placeholder="дд.мм.гггг" [min]="min" [max]="max">
|
||||
<mat-datepicker #gt></mat-datepicker>
|
||||
<p>-</p>
|
||||
<input formControlName="lt" [matDatepicker]="lt" (click)="lt.open()" placeholder="дд.мм.гггг" [min]="min" [max]="max">
|
||||
<mat-datepicker #lt></mat-datepicker>
|
||||
</div>
|
||||
|
||||
<!--div class="row" [formGroup]="controlGroup">
|
||||
<input type="date" formControlName="gt" placeholder="от {{min}}" [min]="min" [max]="max" step="0.1" />
|
||||
<p>-</p>
|
||||
<input type="date" formControlName="lt" placeholder="до {{max}}" [min]="min" [max]="max" step="0.1" />
|
||||
</div-->
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
input {
|
||||
padding-right: 50px;
|
||||
//background: transparent url('~src/assets/images/icons/notification_date.svg') calc(100% - 20px) 50% no-repeat;
|
||||
background-size: 16px;
|
||||
}
|
||||
p {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {FormControl, FormGroup} from "@angular/forms";
|
||||
import {debounceTime} from "rxjs/operators";
|
||||
import {MatDatepickerInputEvent} from "@angular/material/datepicker";
|
||||
import {DatePipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'filter-field-date',
|
||||
templateUrl: 'filter-field-date.component.html',
|
||||
styleUrls: ['filter-field-date.component.scss']
|
||||
})
|
||||
export class FilterFieldDateComponent {
|
||||
@Input() field: any;
|
||||
@Input() formGroup: FormGroup;
|
||||
public controlGroup: FormGroup;
|
||||
|
||||
constructor(private datePipe: DatePipe) {
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.controlGroup = new FormGroup({gt: new FormControl(this.initialValue?.gt || null), lt: new FormControl(this.initialValue?.lt || null)});
|
||||
this.control.setValue(this.controlGroup.value, {emitEvent: false});
|
||||
this.controlGroup.valueChanges.pipe(debounceTime(700)).subscribe(res => {this.value = res});
|
||||
}
|
||||
|
||||
|
||||
get initialValue() {
|
||||
return this.field.value?.data[0] || this.field.value?.data || {};
|
||||
}
|
||||
get control() {
|
||||
return this.formGroup.controls[this.field.name];
|
||||
}
|
||||
get value() {
|
||||
return this.control.value;
|
||||
}
|
||||
set value(val: any) {
|
||||
val.gt = val.gt ? this.datePipe.transform(val.gt, 'yyyy-MM-dd') : null;
|
||||
val.lt = val.lt ? this.datePipe.transform(val.lt, 'yyyy-MM-dd') : null;
|
||||
this.control?.setValue(val);
|
||||
}
|
||||
|
||||
get min() {
|
||||
return this.field?.range?.min || null;
|
||||
}
|
||||
get max() {
|
||||
return this.field?.range?.max || null;
|
||||
}
|
||||
|
||||
|
||||
onChange(prop: string, event: MatDatepickerInputEvent<Date>) {
|
||||
this.value = event.value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FilterFieldDateComponent} from "@app/_modules/filter-fields/types/date/filter-field-date.component";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||
import {MatNativeDateModule} from "@angular/material/core";
|
||||
import {NoopAnimationsModule} from "@angular/platform-browser/animations";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
FilterFieldDateComponent
|
||||
],
|
||||
exports: [
|
||||
FilterFieldDateComponent
|
||||
]
|
||||
})
|
||||
export class FilterFieldDateModule {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<div class="row" [formGroup]="controlGroup">
|
||||
<input type="datetime-local" formControlName="gt" />
|
||||
<p>-</p>
|
||||
<input type="datetime-local" formControlName="lt" />
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue