first push

master
Boris Voropaev 2023-08-25 17:17:11 +03:00
parent c3f651da36
commit d61cb6e403
774 changed files with 44069 additions and 73 deletions

17
.browserslistrc Normal file
View File

@ -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.

16
.editorconfig Normal file
View File

@ -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

78
.gitignore vendored
View File

@ -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

15
.vscode/launch.json vendored Normal file
View File

@ -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}"
}
]
}

View File

@ -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
Youll 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 youll 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, youll 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. Youll 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 youd like to and then click **Clone**.
4. Open the directory you just created to see your repositorys 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.

136
angular.json Normal file
View File

@ -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"
}

44
karma.conf.js Normal file
View File

@ -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
});
};

21605
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@ -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"
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}));
}
}

View File

@ -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
};

View File

@ -0,0 +1,4 @@
export * from './auth.guard';
export * from './error.interceptor';
export * from './fake-backend';
export * from './jwt.interceptor';

View File

@ -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);
}
}

29
src/app/_models/asset.ts Normal file
View File

@ -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;
}

3
src/app/_models/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './asset';
export * from './oauthToken';
export * from './user';

View File

@ -0,0 +1,6 @@
export class OauthToken {
access_token: string;
expires_in: number;
refresh_token: string;
token_type: string;
}

23
src/app/_models/user.ts Normal file
View File

@ -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;
}

View File

@ -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 {}

View File

@ -0,0 +1 @@
<advisories-tree></advisories-tree>

View File

@ -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() {
}
}

View File

@ -0,0 +1,2 @@
<company *ngIf="company" [company]="company"></company>
<company-structure [companyId]="companyId" [editable]="isEditable"></company-structure>

View File

@ -0,0 +1,4 @@
company {
display: block;
margin-bottom: 24px;
}

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -0,0 +1,5 @@
.container {
h1 {
margin-top: 40px;
}
}

View File

@ -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});
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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];
});
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
});
}
}
}

View File

@ -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>

View File

@ -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);
});
}
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
});
}
}

View File

@ -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>

View File

@ -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;}
}
}
}

View File

@ -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);
}
}

View File

@ -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-->

View File

@ -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');
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -0,0 +1,5 @@
.success {
h3, p {
margin: 0;
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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%;
}
}
}
}
}

View File

@ -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');
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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');
}
}

View File

@ -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>

View File

@ -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;
});
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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');
}
}

View File

@ -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-->

View File

@ -0,0 +1,3 @@
.buttons {
margin-top: 24px;
}

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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);
});
}
}

View File

@ -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>

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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});
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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];
}
}

View File

@ -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 {}

View File

@ -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-->

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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