Compare commits
142 Commits
2fe2b11659
...
master
Author | SHA1 | Date | |
---|---|---|---|
52b2af097f | |||
ea5e731bd2 | |||
74a7fe7eb6 | |||
2f9d552e43 | |||
004671c006 | |||
0f6a1e7a45 | |||
437a3fcc58 | |||
0002371265 | |||
0f25d5404c | |||
e98a0db7ca | |||
324c7630ea | |||
f1f1ed16e1 | |||
6fcd68b627 | |||
d50da4db3e | |||
066b1444af | |||
df4ea723b3 | |||
434dec492d | |||
24d6b91553 | |||
2b988db70d | |||
a3a19be5a4 | |||
9f742cab78 | |||
c8bcda8da2 | |||
1bf2868d00 | |||
5b9b67d50c | |||
061307447e | |||
cf09738447 | |||
79a992dc69 | |||
612da04cbb | |||
3d38b49839 | |||
fcd179166e | |||
224d7a3443 | |||
2370a2051b | |||
1d691ccc09 | |||
a7542eaf32 | |||
a8b1485b0e | |||
90fca336f5 | |||
a7b8c15e3a | |||
135570d384 | |||
7830c5f21d | |||
6e914caabc | |||
f26d74aae5 | |||
3aefee124a | |||
eda6ca4b1a | |||
10bf53adec | |||
e10075dfed | |||
2b482d2b2d | |||
9017e87175 | |||
16e25905dc | |||
8138a63324 | |||
1ffbfad37a | |||
c04c457211 | |||
fba28b6bbe | |||
86e6f59567 | |||
a2d4151cc3 | |||
3af8c43cd9 | |||
21f89132ff | |||
99958a2383 | |||
38b877608f | |||
7c66f31bac | |||
72a5f37404 | |||
e92927addb | |||
924c75ea79 | |||
5d265e4b48 | |||
a8159b4f27 | |||
9231bd0d4a | |||
0bbed93df2 | |||
2b09086902 | |||
9209b31db2 | |||
3ca6f56fec | |||
eded639cc3 | |||
844d91de7d | |||
380b2efa0d | |||
6211dd8889 | |||
a86e88e087 | |||
eb4b5d31df | |||
1f901f0612 | |||
85b8bab530 | |||
80a7e71b84 | |||
6450acf6a3 | |||
207f54ae17 | |||
916aa2fd9c | |||
42d831892a | |||
5ee350b66c | |||
578fdff6ca | |||
4bfd919bbc | |||
9d9302525b | |||
b341d66f08 | |||
79393a39c3 | |||
60218a73f2 | |||
42e454c4d6 | |||
c6059a7a60 | |||
6a3a6a8d47 | |||
1fa1e864da | |||
fc828f3008 | |||
49179d2a8a | |||
ebf1066610 | |||
8c9b798bff | |||
660f251b40 | |||
80ab5c9b50 | |||
dba0d3cd62 | |||
7a9bca86bc | |||
f24c1fd9c8 | |||
c945a1016b | |||
8a584fd28a | |||
60d306f9c9 | |||
5d79d86c44 | |||
eada16110b | |||
b215d8909c | |||
1f03c2a9c3 | |||
48a74ecbf5 | |||
fd5a1cb14f | |||
2871505591 | |||
93929633d6 | |||
9b7b4aba50 | |||
ef0de7f709 | |||
35217726b0 | |||
52432fd00f | |||
8acb30c2c3 | |||
1d79860df3 | |||
48bc7ca005 | |||
3cea5f7982 | |||
e0a2ba257c | |||
2e36b06aea | |||
95a593bdb6 | |||
f4b25f428d | |||
b764f2a77b | |||
95165a0940 | |||
e9735a4e99 | |||
e82a0ecb5e | |||
1327131d69 | |||
8fa39614b7 | |||
458cab9888 | |||
0c28514a8d | |||
d5074eb0f3 | |||
c8f2c608b8 | |||
ece19a663a | |||
28804e6f06 | |||
b5cc4cd06f | |||
a71223b951 | |||
8cb05a7895 | |||
dedc8b258a | |||
748421580a |
@ -30,7 +30,6 @@ jobs:
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
ssh-passphrase: ${{ secrets.SSH_PASSPHRASE }}
|
||||
|
||||
- name: Deploy to Server
|
||||
env:
|
||||
@ -40,5 +39,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts
|
||||
scp -r ./dist/frontend/* $SSH_USER@$SSH_HOST:$TARGET_DIR
|
||||
ssh $SSH_USER@$SSH_HOST "sudo chown -R www-data:www-data $TARGET_DIR"
|
||||
sudo apt update
|
||||
sudo apt install rsync -y
|
||||
rsync -avr -p --chmod=770 --no-times --delete ./dist/frontend/browser/ $SSH_USER@$SSH_HOST:$TARGET_DIR
|
||||
ssh $SSH_USER@$SSH_HOST "chown -R :www-data $TARGET_DIR"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# MIREA schedule by Winsomnia
|
||||
|
||||
[](https://github.com/angular/angular-cli)
|
||||
[](https://github.com/angular/angular-cli)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
This project provides a Web interface for working with the MIREA schedule.
|
||||
|
10251
package-lock.json
generated
10251
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0-b0",
|
||||
"version": "1.0.0-rc0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
@ -10,33 +10,34 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.0.5",
|
||||
"@angular/cdk": "~18.0.5",
|
||||
"@angular/cdk-experimental": "^18.0.5",
|
||||
"@angular/common": "^18.0.5",
|
||||
"@angular/compiler": "^18.0.5",
|
||||
"@angular/core": "^18.0.5",
|
||||
"@angular/forms": "^18.0.5",
|
||||
"@angular/material": "~18.0.5",
|
||||
"@angular/platform-browser": "^18.0.5",
|
||||
"@angular/platform-browser-dynamic": "^18.0.5",
|
||||
"@angular/router": "^18.0.5",
|
||||
"@progress/kendo-date-math": "^1.5.13",
|
||||
"@angular/animations": "^19.1.4",
|
||||
"@angular/cdk": "~19.1.2",
|
||||
"@angular/cdk-experimental": "^19.1.2",
|
||||
"@angular/common": "^19.1.4",
|
||||
"@angular/compiler": "^19.1.4",
|
||||
"@angular/core": "^19.1.4",
|
||||
"@angular/forms": "^19.1.4",
|
||||
"@angular/material": "~19.1.2",
|
||||
"@angular/platform-browser": "^19.1.4",
|
||||
"@angular/platform-browser-dynamic": "^19.1.4",
|
||||
"@angular/router": "^19.1.4",
|
||||
"@progress/kendo-date-math": "^1.5.14",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.6.3",
|
||||
"zone.js": "~0.14.7"
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.0.6",
|
||||
"@angular/cli": "^18.0.6",
|
||||
"@angular/compiler-cli": "^18.0.5",
|
||||
"@types/jasmine": "~5.1.4",
|
||||
"jasmine-core": "~5.1.2",
|
||||
"karma": "~6.4.3",
|
||||
"@angular-devkit/build-angular": "^19.1.5",
|
||||
"@angular/cli": "^19.1.5",
|
||||
"@angular/compiler-cli": "^19.1.4",
|
||||
"@types/jasmine": "~5.1.5",
|
||||
"jasmine-core": "~5.5.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import {HttpHeaders} from "@angular/common/http";
|
||||
|
||||
export interface SetRequestBuilderAfterBuild {
|
||||
setRequestBuilder(request: RequestData): void;
|
||||
}
|
||||
|
||||
export interface RequestData {
|
||||
endpoint: string;
|
||||
queryParams: Record<string, string | number | boolean | Array<any> | null> | null;
|
||||
@ -11,102 +7,48 @@ export interface RequestData {
|
||||
data: any;
|
||||
silenceMode: boolean;
|
||||
withCredentials: boolean;
|
||||
needAuth: boolean;
|
||||
}
|
||||
|
||||
export class RequestBuilder {
|
||||
private endpoint: string = '';
|
||||
private queryParams: Record<string, string | number | boolean | Array<any> | null> | null = null;
|
||||
private httpHeaders: HttpHeaders = new HttpHeaders();
|
||||
private data: any = null;
|
||||
private silenceMode: boolean = false;
|
||||
private withCredentials: boolean = false;
|
||||
private readonly object: any;
|
||||
private result: RequestData = Object.create({});
|
||||
|
||||
constructor(obj: any) {
|
||||
this.object = obj;
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public setEndpoint(endpoint: string): this {
|
||||
this.endpoint = endpoint;
|
||||
this.result.endpoint = endpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setQueryParams(queryParams: Record<string, string | number | boolean | Array<any> | null>): RequestBuilder {
|
||||
this.queryParams = queryParams;
|
||||
this.result.queryParams = queryParams;
|
||||
return this;
|
||||
}
|
||||
|
||||
public addHeaders(headers: Record<string, string>): RequestBuilder {
|
||||
Object.keys(headers).forEach(key => {
|
||||
this.httpHeaders = this.httpHeaders.set(key, headers[key]);
|
||||
this.result.httpHeaders = this.result.httpHeaders.set(key, headers[key]);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public setData(data: any): RequestBuilder {
|
||||
this.data = data;
|
||||
this.result.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setSilenceMode(silence: boolean = true): RequestBuilder {
|
||||
this.silenceMode = silence;
|
||||
this.result.silenceMode = silence;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setWithCredentials(credentials: boolean = true): RequestBuilder {
|
||||
this.withCredentials = credentials;
|
||||
this.result.withCredentials = credentials;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build<Type>(): Type {
|
||||
(this.object as SetRequestBuilderAfterBuild).setRequestBuilder({
|
||||
endpoint: this.endpoint,
|
||||
queryParams: this.queryParams,
|
||||
httpHeaders: this.httpHeaders,
|
||||
data: this.data,
|
||||
silenceMode: this.silenceMode,
|
||||
withCredentials: this.withCredentials
|
||||
});
|
||||
return this.object as Type;
|
||||
}
|
||||
|
||||
public getEndpoint(): string {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
public getQueryParams(): Record<string, string | number | boolean | Array<any> | null> | null {
|
||||
return this.queryParams;
|
||||
}
|
||||
|
||||
public getHttpHeaders(): HttpHeaders {
|
||||
return this.httpHeaders;
|
||||
}
|
||||
|
||||
public getData(): any {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public getSilenceMode(): boolean {
|
||||
return this.silenceMode;
|
||||
}
|
||||
|
||||
public static getStandardRequestData(): RequestData {
|
||||
return {
|
||||
endpoint: '',
|
||||
queryParams: null,
|
||||
httpHeaders: new HttpHeaders(),
|
||||
data: null,
|
||||
silenceMode: false,
|
||||
withCredentials: false
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.endpoint = '';
|
||||
this.queryParams = null;
|
||||
this.httpHeaders = new HttpHeaders();
|
||||
this.data = null;
|
||||
this.silenceMode = false;
|
||||
this.withCredentials = false;
|
||||
public get build(): RequestData {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +1,37 @@
|
||||
import {catchError, filter, mergeMap, Observable, retryWhen, switchMap, take, tap, timer} from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
first,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
switchMap
|
||||
} from "rxjs";
|
||||
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
|
||||
import {NotifyColor, OpenNotifyService} from "@service/open-notify.service";
|
||||
import {environment} from "@environment";
|
||||
import {Router} from "@angular/router";
|
||||
import {Injectable} from "@angular/core";
|
||||
import {RequestBuilder, RequestData, SetRequestBuilderAfterBuild} from "@api/RequestBuilder";
|
||||
import {TokenRefreshService} from "@service/token-refresh.service";
|
||||
import {AuthToken} from "@service/auth.service";
|
||||
|
||||
export function retryWithInterval<T>(): (source: Observable<T>) => Observable<T> {
|
||||
return (source: Observable<T>) =>
|
||||
source.pipe(
|
||||
retryWhen((errors: Observable<any>) =>
|
||||
errors.pipe(
|
||||
mergeMap((error, index) => {
|
||||
if (index < (environment.maxRetry < 0 ? Infinity : environment.maxRetry - 1) && !error.status.toString().startsWith('4') && !error.status.toString().startsWith('5')) {
|
||||
console.log(`Retrying after ${environment.retryDelay}ms...`);
|
||||
return timer(environment.retryDelay);
|
||||
} else {
|
||||
if (error.status.toString().startsWith('4'))
|
||||
console.error(`Server returned a client code error`);
|
||||
else
|
||||
console.error(`Exceeded maximum retries (${environment.maxRetry})`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
import {RequestBuilder, RequestData} from "@api/RequestBuilder";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
|
||||
export enum AvailableVersion {
|
||||
v1
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export default abstract class ApiService implements SetRequestBuilderAfterBuild {
|
||||
constructor(private http: HttpClient, private notify: OpenNotifyService, private router: Router, protected tokenRefreshService: TokenRefreshService) {
|
||||
export default abstract class ApiService {
|
||||
constructor(protected http: HttpClient, protected notify: ToastrService, private router: Router) {
|
||||
}
|
||||
|
||||
private apiUrl = environment.apiUrl;
|
||||
private static isRefreshingToken: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
private static refreshTokenSubject: ReplaySubject<any> = new ReplaySubject(1);
|
||||
|
||||
protected abstract basePath: string;
|
||||
protected abstract version: AvailableVersion;
|
||||
private request: RequestData = RequestBuilder.getStandardRequestData();
|
||||
public static readonly tokenKey = 'auth_token';
|
||||
|
||||
public setRequestBuilder(request: RequestData): void {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
private static addQuery(endpoint: string, queryParams?: Record<string, string | number | boolean | Array<any> | null> | null): string {
|
||||
const url = new URL(endpoint);
|
||||
@ -57,7 +40,7 @@ export default abstract class ApiService implements SetRequestBuilderAfterBuild
|
||||
Object.keys(queryParams).forEach(key => {
|
||||
const value = queryParams[key];
|
||||
if (value !== null && value !== undefined) {
|
||||
if (typeof (value) === typeof (Array)) {
|
||||
if (Array.isArray(value)) {
|
||||
(value as Array<any>).forEach(x => url.searchParams.append(key, x.toString()));
|
||||
} else
|
||||
url.searchParams.append(key, value.toString());
|
||||
@ -69,122 +52,158 @@ export default abstract class ApiService implements SetRequestBuilderAfterBuild
|
||||
}
|
||||
|
||||
private static combineUrls(...parts: string[]): string {
|
||||
return parts.map(part => part.replace(/(^\/+|\/+$)/g, '')).join('/');
|
||||
return parts.map(part => (!part || part == '' ? '/' : part).replace(/(^\/+|\/+$)/g, '')).join('/');
|
||||
}
|
||||
|
||||
protected get combinedUrl() {
|
||||
return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, this.request.endpoint), this.request.queryParams)
|
||||
protected combinedUrl(request: RequestData) {
|
||||
return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, request.endpoint), request.queryParams);
|
||||
}
|
||||
|
||||
private makeHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put'): Observable<Type> {
|
||||
const doneEndpoint = this.combinedUrl;
|
||||
private sendHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData, secondTry: boolean = false): Observable<Type> {
|
||||
const doneEndpoint = this.combinedUrl(request);
|
||||
|
||||
return this.tokenRefreshService.getTokenRefreshing$().pipe(
|
||||
filter(refreshing => !refreshing),
|
||||
take(1),
|
||||
switchMap(_ => {
|
||||
return this.http.request<Type>(method, doneEndpoint, {
|
||||
withCredentials: this.request.withCredentials,
|
||||
headers: this.request.httpHeaders,
|
||||
body: this.request.data
|
||||
}).pipe(
|
||||
tap(_ => this.request = RequestBuilder.getStandardRequestData()),
|
||||
retryWithInterval<Type>(),
|
||||
catchError(error => {
|
||||
if (!this.request.silenceMode)
|
||||
this.handleError(error);
|
||||
return this.http.request<Type>(method, doneEndpoint, {
|
||||
withCredentials: request.withCredentials,
|
||||
headers: request.httpHeaders,
|
||||
body: request.data
|
||||
}).pipe(
|
||||
catchError(error => {
|
||||
if (request.needAuth && !secondTry && error.status === 401)
|
||||
return this.handle401Error(error).pipe(
|
||||
switchMap(() => this.sendHttpRequest<Type>(method, request, true))
|
||||
);
|
||||
else {
|
||||
if (!request.silenceMode)
|
||||
this.handleError(error);
|
||||
|
||||
this.request = RequestBuilder.getStandardRequestData();
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private refreshToken(): Observable<AuthRoles> {
|
||||
return this.http.get<AuthRoles>(ApiService.combineUrls(this.apiUrl, AvailableVersion[AvailableVersion.v1], 'Auth', 'ReLogin'), {
|
||||
withCredentials: true
|
||||
});
|
||||
}
|
||||
|
||||
private handle401Error(error: any): Observable<any> {
|
||||
if (ApiService.isRefreshingToken.value)
|
||||
return ApiService.refreshTokenSubject.asObservable();
|
||||
|
||||
ApiService.isRefreshingToken.next(true);
|
||||
return this.refreshToken().pipe(
|
||||
switchMap(_ => {
|
||||
ApiService.isRefreshingToken.next(false);
|
||||
ApiService.refreshTokenSubject.next(null);
|
||||
return of(null);
|
||||
}),
|
||||
catchError(err => {
|
||||
ApiService.isRefreshingToken.next(false);
|
||||
ApiService.refreshTokenSubject.error(err);
|
||||
ApiService.refreshTokenSubject = new ReplaySubject(1);
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private makeHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable<Type> {
|
||||
if (request.needAuth) {
|
||||
return ApiService.isRefreshingToken.pipe(
|
||||
distinctUntilChanged(),
|
||||
filter(isRefreshing => !isRefreshing),
|
||||
first(),
|
||||
switchMap(() => this.sendHttpRequest<Type>(method, request))
|
||||
);
|
||||
} else {
|
||||
return this.sendHttpRequest<Type>(method, request);
|
||||
}
|
||||
}
|
||||
|
||||
private getRequest(request: RequestData | string | null): RequestData {
|
||||
if (request === null)
|
||||
return this.createRequestBuilder().build;
|
||||
if (typeof request === 'string')
|
||||
return this.createRequestBuilder().setEndpoint(request as string).build;
|
||||
|
||||
return request as RequestData;
|
||||
}
|
||||
|
||||
public createRequestBuilder() {
|
||||
this.request = RequestBuilder.getStandardRequestData();
|
||||
return new RequestBuilder(this);
|
||||
return new RequestBuilder();
|
||||
}
|
||||
|
||||
public get<Type>(endpoint: string = ''): Observable<Type> {
|
||||
if (endpoint)
|
||||
this.request.endpoint = endpoint;
|
||||
return this.makeHttpRequest<Type>('get');
|
||||
public get<Type>(request: RequestData | string | null = null): Observable<Type> {
|
||||
return this.makeHttpRequest<Type>('get', this.getRequest(request));
|
||||
}
|
||||
|
||||
public post<Type>(endpoint: string = ''): Observable<Type> {
|
||||
if (endpoint)
|
||||
this.request.endpoint = endpoint;
|
||||
return this.makeHttpRequest<Type>('post');
|
||||
public post<Type>(request: RequestData | string | null = null): Observable<Type> {
|
||||
return this.makeHttpRequest<Type>('post', this.getRequest(request));
|
||||
}
|
||||
|
||||
public put<Type>(endpoint: string = ''): Observable<Type> {
|
||||
if (endpoint)
|
||||
this.request.endpoint = endpoint;
|
||||
return this.makeHttpRequest<Type>('put');
|
||||
public put<Type>(request: RequestData | string | null = null): Observable<Type> {
|
||||
return this.makeHttpRequest<Type>('put', this.getRequest(request));
|
||||
}
|
||||
|
||||
public delete<Type>(endpoint: string = ''): Observable<Type> {
|
||||
if (endpoint)
|
||||
this.request.endpoint = endpoint;
|
||||
return this.makeHttpRequest<Type>('delete');
|
||||
public delete<Type>(request: RequestData | string | null = null): Observable<Type> {
|
||||
return this.makeHttpRequest<Type>('delete', this.getRequest(request));
|
||||
}
|
||||
|
||||
public addAuth() {
|
||||
const token = localStorage.getItem(ApiService.tokenKey);
|
||||
|
||||
if (!token)
|
||||
return this;
|
||||
|
||||
const authToken = AuthToken.httpHeader((JSON.parse(token) as AuthToken));
|
||||
authToken.keys().forEach(key => this.request.httpHeaders = this.request.httpHeaders.append(key, authToken.get(key) ?? ''))
|
||||
|
||||
public addAuth(request: RequestData) {
|
||||
request.needAuth = true;
|
||||
request.withCredentials = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleError(error: HttpErrorResponse): void {
|
||||
// todo: change to Retry-After condition
|
||||
if (error.error.toString().includes("setup")) {
|
||||
if (error.error && error.error.detail && error.error.detail.includes("setup")) {
|
||||
this.router.navigate(['/setup/']).then();
|
||||
return;
|
||||
}
|
||||
|
||||
let message: string;
|
||||
let title: string;
|
||||
let message: string | undefined = undefined;
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
message = `Произошла ошибка: ${error.error.message}`;
|
||||
title = `Произошла ошибка: ${error.error.message}`;
|
||||
} else {
|
||||
switch (error.status) {
|
||||
case 0:
|
||||
message = 'Неизвестная ошибка. Пожалуйста, попробуйте позже.';
|
||||
break;
|
||||
case 400:
|
||||
message = 'Ошибка запроса. Пожалуйста, проверьте отправленные данные.';
|
||||
break;
|
||||
case 401:
|
||||
this.router.navigate(['/login/']).then();
|
||||
message = 'Ошибка авторизации. Пожалуйста, выполните вход с правильными учетными данными.';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Отказано в доступе. У вас нет разрешения на выполнение этого действия.';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Запрашиваемый ресурс не найден.';
|
||||
break;
|
||||
case 500:
|
||||
message = 'Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.';
|
||||
break;
|
||||
case 503:
|
||||
message = 'Сервер на обслуживании. Пожалуйста, попробуйте позже.';
|
||||
break;
|
||||
default:
|
||||
message = `Сервер вернул код ошибки: ${error.status}`;
|
||||
break;
|
||||
}
|
||||
if (error.error?.Error) {
|
||||
message += ` ${error.error.Error}`;
|
||||
if (error.error && error.error.type && error.error.title) {
|
||||
title = error.error.title || `Ошибка с кодом ${error.status}`;
|
||||
message = error.error.detail || 'Неизвестная ошибка';
|
||||
} else {
|
||||
switch (error.status) {
|
||||
case 0:
|
||||
title = 'Неизвестная ошибка. Пожалуйста, попробуйте позже.';
|
||||
break;
|
||||
case 400:
|
||||
title = 'Ошибка запроса. Пожалуйста, проверьте отправленные данные.';
|
||||
break;
|
||||
case 401:
|
||||
this.router.navigate(['/login/']).then();
|
||||
title = 'Ошибка авторизации. Пожалуйста, выполните вход с правильными учетными данными.';
|
||||
break;
|
||||
case 403:
|
||||
title = 'Отказано в доступе. У вас нет разрешения на выполнение этого действия.';
|
||||
break;
|
||||
case 404:
|
||||
title = 'Запрашиваемый ресурс не найден.';
|
||||
break;
|
||||
case 500:
|
||||
title = 'Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.';
|
||||
break;
|
||||
case 503:
|
||||
title = 'Сервер на обслуживании. Пожалуйста, попробуйте позже.';
|
||||
break;
|
||||
default:
|
||||
title = `Сервер вернул код ошибки: ${error.status}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!message)
|
||||
message = error.error.statusMessage;
|
||||
}
|
||||
this.notify.open(message, NotifyColor.Danger);
|
||||
this.notify.error(message == '' ? undefined : message, title);
|
||||
}
|
||||
}
|
||||
|
119
src/api/v1/authApi.service.ts
Normal file
119
src/api/v1/authApi.service.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {LoginRequest} from "@api/v1/loginRequest";
|
||||
import {catchError, map, Observable, of} from "rxjs";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {AvailableOAuthProvidersResponse} from "@api/v1/availableProvidersResponse";
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||
import {TwoFactorAuthRequest} from "@api/v1/twoFactorAuthRequest";
|
||||
import {OAuthAction} from "@model/oAuthAction";
|
||||
|
||||
export interface OAuthProviderData extends AvailableOAuthProvidersResponse {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export default class AuthApiService extends ApiService {
|
||||
public readonly basePath = 'Auth/';
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public login(login: LoginRequest) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('Login')
|
||||
.setData(login)
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.post<TwoFactorAuthentication>(request);
|
||||
}
|
||||
|
||||
public twoFactorAuth(data: TwoFactorAuthRequest) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('2FA')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public reLogin() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('ReLogin')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<AuthRoles>(request);
|
||||
}
|
||||
|
||||
public logout() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setWithCredentials()
|
||||
.setEndpoint('Logout')
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).get(request);
|
||||
}
|
||||
|
||||
public getRole(isSilence: boolean = true) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setSilenceMode(isSilence)
|
||||
.setEndpoint('GetRole')
|
||||
.build;
|
||||
|
||||
return this.addAuth(request)
|
||||
.get<AuthRoles>(request)
|
||||
.pipe(
|
||||
catchError(_ => {
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getProviderIcon(provider: OAuthProvider): string {
|
||||
switch (provider) {
|
||||
case OAuthProvider.Google:
|
||||
return 'assets/icons/google.svg';
|
||||
case OAuthProvider.Yandex:
|
||||
return 'assets/icons/yandex.svg';
|
||||
case OAuthProvider.MailRu:
|
||||
return 'assets/icons/mailru.svg';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public availableProviders(callback: string): Observable<OAuthProviderData[]> {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('AvailableProviders')
|
||||
.setQueryParams({callback: callback})
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<Array<AvailableOAuthProvidersResponse>>(request).pipe(
|
||||
map(data => {
|
||||
return data.map((provider) => ({
|
||||
...provider,
|
||||
icon: this.getProviderIcon(provider.provider),
|
||||
}) as OAuthProviderData);
|
||||
}));
|
||||
}
|
||||
|
||||
private handleTokenRequest(token: string, action: OAuthAction) {
|
||||
return this.createRequestBuilder()
|
||||
.setEndpoint('HandleToken')
|
||||
.setQueryParams({token: token, action: action})
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
}
|
||||
|
||||
public loginOAuth(token: string) {
|
||||
return this.get<TwoFactorAuthentication>(this.handleTokenRequest(token, OAuthAction.Login));
|
||||
}
|
||||
|
||||
public linkAccount(token: string): Observable<null> {
|
||||
const request = this.handleTokenRequest(token, OAuthAction.Bind);
|
||||
return this.addAuth(request).get(request);
|
||||
}
|
||||
}
|
89
src/api/v1/configuration/schedule.service.ts
Normal file
89
src/api/v1/configuration/schedule.service.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {CronUpdateScheduleResponse} from "@api/v1/configuration/cronUpdateScheduleResponse";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {map} from "rxjs";
|
||||
import CronUpdateSkip from "@model/cronUpdateSkip";
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleService extends ApiService {
|
||||
public readonly basePath = 'Configuration/Schedule';
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public getCronUpdateSchedule() {
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('CronUpdateSchedule')
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).get<CronUpdateScheduleResponse>(request);
|
||||
}
|
||||
|
||||
public postCronUpdateSchedule(cron: string) {
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('CronUpdateSchedule')
|
||||
.setQueryParams({cron: cron})
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).post<CronUpdateScheduleResponse>(request);
|
||||
}
|
||||
|
||||
public getStartTerm() {
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('StartTerm')
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).get<string>(request).pipe(map(date => new DateOnly(date)));
|
||||
}
|
||||
|
||||
public postStartTerm(startTerm: DateOnly, force: boolean) {
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('StartTerm')
|
||||
.setQueryParams({force: force, startTerm: startTerm.toString()})
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).post(request);
|
||||
}
|
||||
|
||||
public getCronUpdateSkip() {
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('CronUpdateSkip')
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).get<{ date?: string, start?: string, end?: string }[]>(request)
|
||||
.pipe(
|
||||
map(data => {
|
||||
return data.map(x => <CronUpdateSkip>{
|
||||
date: x.date ? new DateOnly(x.date) : null,
|
||||
start: x.start ? new DateOnly(x.start) : null,
|
||||
end: x.end ? new DateOnly(x.end) : null
|
||||
});
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public postCronUpdateSkip(data: CronUpdateSkip[]) {
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('CronUpdateSkip')
|
||||
.setData(data.map(x => <any>{
|
||||
start: x.start?.toString(),
|
||||
end: x.end?.toString(),
|
||||
date: x.date?.toString()
|
||||
}))
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).post<any>(request);
|
||||
}
|
||||
|
||||
public uploadScheduleFile(files: File[], campus: string[], force: boolean) {
|
||||
const formData = new FormData();
|
||||
files.forEach(file => formData.append('files', file, file.name));
|
||||
|
||||
const request = this.createRequestBuilder()
|
||||
.setEndpoint('Upload')
|
||||
.setData(formData)
|
||||
.setQueryParams({force: force, defaultCampus: campus})
|
||||
.build;
|
||||
|
||||
return this.addAuth(request).post(request);
|
||||
}
|
||||
}
|
@ -8,10 +8,11 @@ export class DisciplineService extends ApiService {
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public getDisciplines(page: number | null = null, pageSize: number | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setQueryParams({page: page, pageSize: pageSize})
|
||||
.build<ApiService>()
|
||||
.get<DisciplineResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<DisciplineResponse[]>(request);
|
||||
}
|
||||
|
||||
public getById(id: number) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {FacultyResponse} from "@api/v1/facultyResponse";
|
||||
import {FacultyDetailsResponse} from "@api/v1/facultyDetailsResponse";
|
||||
|
||||
@Injectable()
|
||||
export class FacultyService extends ApiService {
|
||||
@ -9,14 +8,10 @@ export class FacultyService extends ApiService {
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public getFaculties(page: number | null = null, pageSize: number | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setQueryParams({page: page, pageSize: pageSize})
|
||||
.build<ApiService>()
|
||||
.get<FacultyResponse[]>();
|
||||
.build;
|
||||
|
||||
}
|
||||
|
||||
public getById(id: number) {
|
||||
return this.get<FacultyDetailsResponse>(id.toString());
|
||||
return this.get<FacultyResponse[]>(request);
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,11 @@ export class GroupService extends ApiService {
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public getGroups(page: number | null = null, pageSize: number | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setQueryParams({page: page, pageSize: pageSize})
|
||||
.build<ApiService>()
|
||||
.get<GroupResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<GroupResponse[]>(request);
|
||||
}
|
||||
|
||||
public getById(id: number) {
|
||||
|
22
src/api/v1/import.service.ts
Normal file
22
src/api/v1/import.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||
|
||||
@Injectable()
|
||||
export class ImportService extends ApiService {
|
||||
public readonly basePath = 'Import/';
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public importToExcel(data: ScheduleRequest) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setData(data)
|
||||
.setEndpoint('ImportToExcel')
|
||||
.build;
|
||||
|
||||
console.log(this.combinedUrl(request));
|
||||
console.log(data);
|
||||
return this.http.post(this.combinedUrl(request), data, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
}
|
17
src/api/v1/lessonType.service.ts
Normal file
17
src/api/v1/lessonType.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {LessonTypeResponse} from "@api/v1/lessonTypeResponse";
|
||||
|
||||
@Injectable()
|
||||
export class LessonTypeService extends ApiService {
|
||||
public readonly basePath = 'LessonType/';
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public getLessonTypes(page: number | null = null, pageSize: number | null = null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setQueryParams({page: page, pageSize: pageSize})
|
||||
.build;
|
||||
|
||||
return this.get<LessonTypeResponse[]>(request);
|
||||
}
|
||||
}
|
@ -8,10 +8,11 @@ export class ProfessorService extends ApiService {
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public getProfessors(page: number | null = null, pageSize: number | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setQueryParams({page: page, pageSize: pageSize})
|
||||
.build<ApiService>()
|
||||
.get<ProfessorResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<ProfessorResponse[]>(request);
|
||||
}
|
||||
|
||||
public getById(id: number) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {DateOnly} from "@model/DateOnly";
|
||||
import {PeriodTimes} from "@model/pairPeriodTime";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {PairPeriodTime} from "@model/pairPeriodTime";
|
||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||
import {map} from "rxjs";
|
||||
@ -16,45 +16,50 @@ export class ScheduleService extends ApiService {
|
||||
}
|
||||
|
||||
public pairPeriod() {
|
||||
return this.get<PeriodTimes>('PairPeriod');
|
||||
return this.get<PairPeriodTime>('PairPeriod');
|
||||
}
|
||||
|
||||
public postSchedule(data: ScheduleRequest) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setData(data)
|
||||
.build<ApiService>()
|
||||
.post<ScheduleResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.post<ScheduleResponse[]>(request);
|
||||
}
|
||||
|
||||
public getByGroup(id : number, isEven: boolean | null = null, disciplines: Array<number> | null = null, professors: Array<number> | null = null, lectureHalls: Array<number> | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
public getByGroup(id: number, isEven: boolean | null = null, disciplines: Array<number> | null = null, professors: Array<number> | null = null, lectureHalls: Array<number> | null = null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GetByGroup/' + id.toString())
|
||||
.setQueryParams({isEven: isEven, disciplines: disciplines, professors: professors, lectureHalls: lectureHalls})
|
||||
.build<ApiService>()
|
||||
.get<ScheduleResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<ScheduleResponse[]>(request);
|
||||
}
|
||||
|
||||
public getByProfessor(id : number, isEven: boolean | null = null, disciplines: Array<number> | null = null, groups: Array<number> | null = null, lectureHalls: Array<number> | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
public getByProfessor(id: number, isEven: boolean | null = null, disciplines: Array<number> | null = null, groups: Array<number> | null = null, lectureHalls: Array<number> | null = null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GetByProfessor/' + id.toString())
|
||||
.setQueryParams({isEven: isEven, disciplines: disciplines, groups: groups, lectureHalls: lectureHalls})
|
||||
.build<ApiService>()
|
||||
.get<ScheduleResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<ScheduleResponse[]>(request);
|
||||
}
|
||||
|
||||
public getByLectureHall(id : number, isEven: boolean | null = null, disciplines: Array<number> | null = null, groups: Array<number> | null = null, professors: Array<number> | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
public getByLectureHall(id: number, isEven: boolean | null = null, disciplines: Array<number> | null = null, groups: Array<number> | null = null, professors: Array<number> | null = null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GetByLectureHall/' + id.toString())
|
||||
.setQueryParams({isEven: isEven, disciplines: disciplines, groups: groups, professors: professors})
|
||||
.build<ApiService>()
|
||||
.get<ScheduleResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<ScheduleResponse[]>(request);
|
||||
}
|
||||
|
||||
public getByDiscipline(id : number, isEven: boolean | null = null, groups: Array<number> | null = null, professors: Array<number> | null = null, lectureHalls: Array<number> | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
public getByDiscipline(id: number, isEven: boolean | null = null, groups: Array<number> | null = null, professors: Array<number> | null = null, lectureHalls: Array<number> | null = null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GetByDiscipline/' + id.toString())
|
||||
.setQueryParams({isEven: isEven, groups: groups, professors: professors, lectureHalls: lectureHalls})
|
||||
.build<ApiService>()
|
||||
.get<ScheduleResponse[]>();
|
||||
.build;
|
||||
|
||||
return this.get<ScheduleResponse[]>(request);
|
||||
}
|
||||
}
|
||||
|
26
src/api/v1/securityService.ts
Normal file
26
src/api/v1/securityService.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {PasswordPolicy} from "@model/passwordPolicy";
|
||||
|
||||
@Injectable()
|
||||
export default class SecurityService extends ApiService {
|
||||
public readonly basePath = 'Security/';
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public generateTotpQrCode(totpKey: string, username: string) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GenerateTotpQrCode')
|
||||
.setQueryParams({totpKey: totpKey, label: username})
|
||||
.build;
|
||||
|
||||
return this.combinedUrl(request);
|
||||
}
|
||||
|
||||
public passwordPolicy() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('PasswordPolicy')
|
||||
.build;
|
||||
|
||||
return this.get<PasswordPolicy>(request);
|
||||
}
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {DatabaseRequest} from "@api/v1/databaseRequest";
|
||||
import {CacheRequest} from "@api/v1/cacheRequest";
|
||||
import {catchError, of} from "rxjs";
|
||||
import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
|
||||
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
|
||||
import {CacheRequest} from "@api/v1/configuration/cacheRequest";
|
||||
import {CreateUserRequest} from "@api/v1/createUserRequest";
|
||||
import {LoggingRequest} from "@api/v1/loggingRequest";
|
||||
import {EmailRequest} from "@api/v1/emailRequest";
|
||||
import {ScheduleConfigurationRequest} from "@api/v1/scheduleConfigurationRequest";
|
||||
import {DateOnly} from "@model/DateOnly";
|
||||
import {LoggingRequest} from "@api/v1/configuration/loggingRequest";
|
||||
import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest";
|
||||
import {EmailRequest} from "@api/v1/configuration/emailRequest";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {CacheResponse} from "@api/v1/configuration/cacheResponse";
|
||||
import {PasswordPolicy} from "@model/passwordPolicy";
|
||||
import {UserResponse} from "@api/v1/userResponse";
|
||||
|
||||
@Injectable()
|
||||
export default class SetupService extends ApiService {
|
||||
@ -14,101 +19,217 @@ export default class SetupService extends ApiService {
|
||||
public readonly version = AvailableVersion.v1;
|
||||
|
||||
public checkToken(token: string) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('CheckToken')
|
||||
.setQueryParams({token: token})
|
||||
.build<ApiService>()
|
||||
.get<boolean>();
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<boolean>(request);
|
||||
}
|
||||
|
||||
public isConfiguredToken() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('IsConfiguredToken')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<boolean>(request).pipe(catchError(_ => {
|
||||
return of(false);
|
||||
}));
|
||||
}
|
||||
|
||||
public setPsql(data: DatabaseRequest) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetPsql')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public setMysql(data: DatabaseRequest) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetMysql')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public setSqlite(path: string | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetSqlite')
|
||||
.setQueryParams({path: path})
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.get<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public databaseConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('DatabaseConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<DatabaseResponse>(request);
|
||||
}
|
||||
|
||||
public setRedis(data: CacheRequest) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetRedis')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public setMemcached() {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetMemcached')
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public cacheConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('CacheConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<CacheResponse>(request);
|
||||
}
|
||||
|
||||
public setPasswordPolicy(data: PasswordPolicy | null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetPasswordPolicy')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public passwordPolicyConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('PasswordPolicyConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<PasswordPolicy>(request);
|
||||
}
|
||||
|
||||
public createAdmin(data: CreateUserRequest) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('CreateAdmin')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public adminConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('AdminConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<UserResponse>(request);
|
||||
}
|
||||
|
||||
public registerOAuth(token: string) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('HandleToken')
|
||||
.setQueryParams({token: token})
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<null>(request);
|
||||
}
|
||||
|
||||
public setLogging(data: LoggingRequest | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetLogging')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public loggingConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('LoggingConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<LoggingRequest>(request);
|
||||
}
|
||||
|
||||
public setEmail(data: EmailRequest | null = null) {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetEmail')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public setSchedule(data: ScheduleConfigurationRequest) {
|
||||
data.startTerm = new DateOnly(data.startTerm).toString();
|
||||
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetSchedule')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public generateTotpKey() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GenerateTotpKey')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<string>(request);
|
||||
}
|
||||
|
||||
public verifyTotp(code: string) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('VerifyTotp')
|
||||
.setWithCredentials()
|
||||
.setQueryParams({code: code})
|
||||
.build;
|
||||
|
||||
return this.get<boolean>(request);
|
||||
}
|
||||
|
||||
public scheduleConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('ScheduleConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<ScheduleConfigurationRequest>(request);
|
||||
}
|
||||
|
||||
public submit() {
|
||||
return this.createRequestBuilder()
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('Submit')
|
||||
.setWithCredentials()
|
||||
.build<ApiService>()
|
||||
.post<boolean>();
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public isConfigured() {
|
||||
|
@ -2,21 +2,21 @@ import {Component} from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {FooterComponent} from "@component/common/footer/footer.component";
|
||||
import localeRu from '@angular/common/locales/ru';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import {registerLocaleData} from '@angular/common';
|
||||
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||
import {TokenRefreshService} from "@service/token-refresh.service";
|
||||
import {HeaderComponent} from "@component/common/header/header.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, FooterComponent, FocusNextDirective],
|
||||
imports: [RouterOutlet, FooterComponent, FocusNextDirective, HeaderComponent],
|
||||
template: `
|
||||
<app-header/>
|
||||
<router-outlet/>
|
||||
<app-footer/>`
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(tokenRefreshService: TokenRefreshService) {
|
||||
constructor() {
|
||||
registerLocaleData(localeRu);
|
||||
tokenRefreshService.startTokenRefresh();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,29 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import {ApplicationConfig, LOCALE_ID} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {routes} from './app.routes';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {provideHttpClient} from "@angular/common/http";
|
||||
import {provideToastr} from "ngx-toastr";
|
||||
import {MAT_DATE_LOCALE, provideNativeDateAdapter} from "@angular/material/core";
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes), provideAnimationsAsync(), provideHttpClient()]
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideAnimationsAsync(),
|
||||
provideHttpClient(),
|
||||
provideToastr({
|
||||
timeOut: 5000,
|
||||
extendedTimeOut: 2000,
|
||||
positionClass: "toast-top-right",
|
||||
progressBar: true,
|
||||
progressAnimation: "decreasing",
|
||||
newestOnTop: true,
|
||||
tapToDismiss: true,
|
||||
disableTimeOut: false,
|
||||
autoDismiss: true,
|
||||
maxOpened: 5
|
||||
}),
|
||||
provideNativeDateAdapter(),
|
||||
{ provide: LOCALE_ID, useValue: 'ru' },
|
||||
{ provide: MAT_DATE_LOCALE, useValue: 'ru' }]
|
||||
};
|
||||
|
@ -8,6 +8,12 @@ import {ScheduleComponent as SetupScheduleComponent} from "@page/setup/schedule/
|
||||
import {SetupComponent} from "@page/setup/setup.component";
|
||||
import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component";
|
||||
import {SummaryComponent} from "@page/setup/summary/summary.component";
|
||||
import {LoginComponent} from "@page/login/login.component";
|
||||
import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component";
|
||||
import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.component";
|
||||
import {AdminComponent} from "@page/admin/admin.component";
|
||||
import {UnderConstructionComponent} from "@page/admin/under-construction/under-construction.component";
|
||||
import {ScheduleConfigurationComponent} from "@page/admin/schedule-configuration/schedule-configuration.component";
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
|
||||
@ -20,9 +26,20 @@ export const routes: Routes = [
|
||||
{path: 'schedule', component: SetupScheduleComponent},
|
||||
{path: 'logging', component: LoggingComponent},
|
||||
{path: 'summary', component: SummaryComponent},
|
||||
{path: 'password-policy', component: PasswordPolicyComponent},
|
||||
{path: 'two-factor', component: TwoFactorComponent},
|
||||
{path: '', redirectTo: 'welcome', pathMatch: 'full'}
|
||||
]
|
||||
},
|
||||
{path: 'login', title: 'Вход', component: LoginComponent},
|
||||
{
|
||||
path: 'admin', title: 'Админ панель', component: AdminComponent, children: [
|
||||
{path: 'schedule', component: ScheduleConfigurationComponent},
|
||||
{path: 'institute', component: UnderConstructionComponent},
|
||||
{path: 'account', component: UnderConstructionComponent},
|
||||
{path: 'server', component: UnderConstructionComponent},
|
||||
{path: '', redirectTo: 'schedule', pathMatch: 'full'},
|
||||
{path: '**', redirectTo: 'schedule', pathMatch: 'full'}
|
||||
]
|
||||
}
|
||||
/*{path: 'not-found', title: '404 страница не найдена'},
|
||||
{path: '**', redirectTo: '/not-found'}*/
|
||||
];
|
||||
|
47
src/assets/icons/google.svg
Normal file
47
src/assets/icons/google.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Social_Icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea4335;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3, .cls-4 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #4285f4;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fbbc05;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #34a853;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_x31__stroke">
|
||||
<g id="Google">
|
||||
<rect width="128" height="128" rx="96" ry="96" fill="#fff"/>
|
||||
<rect class="cls-5" width="128" height="128"/>
|
||||
|
||||
<g transform="scale(0.67, 0.67)" transform-origin="center">
|
||||
<path class="cls-3"
|
||||
d="M27.58,64c0-4.16.69-8.14,1.92-11.88L7.94,35.65c-4.2,8.53-6.57,18.15-6.57,28.35s2.37,19.8,6.56,28.33l21.56-16.5c-1.22-3.72-1.9-7.69-1.9-11.83"/>
|
||||
<path class="cls-1"
|
||||
d="M65.46,26.18c9.03,0,17.19,3.2,23.6,8.44l18.64-18.62C96.34,6.11,81.77,0,65.46,0,40.13,0,18.36,14.48,7.94,35.65l21.57,16.47c4.97-15.09,19.14-25.94,35.95-25.94"/>
|
||||
<path class="cls-4"
|
||||
d="M65.46,101.82c-16.81,0-30.98-10.85-35.95-25.94l-21.57,16.47c10.42,21.17,32.19,35.65,57.52,35.65,15.63,0,30.56-5.55,41.76-15.95l-20.47-15.83c-5.78,3.64-13.05,5.6-21.28,5.6"/>
|
||||
<path class="cls-2"
|
||||
d="M126.63,64c0-3.78-.58-7.85-1.46-11.64h-59.72v24.73h34.38c-1.72,8.43-6.4,14.91-13.09,19.13l20.47,15.83c11.77-10.92,19.42-27.19,19.42-48.05"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
22
src/assets/icons/mailru.svg
Normal file
22
src/assets/icons/mailru.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="mailru" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 492.91 492.91">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #0874fc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="cls-2" cx="246.45" cy="246.46" r="246.46"/>
|
||||
<g id="Logo">
|
||||
<g id="yellow">
|
||||
<path class="cls-1"
|
||||
d="M241.66,168.96c21.11,0,40.96,9.33,55.53,23.94v.05c0-7.01,4.72-12.3,11.28-12.3l1.66-.02c10.25,0,12.35,9.7,12.35,12.78l.05,109.06c-.73,7.14,7.36,10.82,11.85,6.25,17.51-17.99,38.46-92.52-10.89-135.69-45.99-40.25-107.7-33.62-140.52-11-34.89,24.06-57.21,77.31-35.53,127.32,23.64,54.57,91.28,70.83,131.49,54.61,20.36-8.22,29.77,19.3,8.62,28.3-31.95,13.62-120.86,12.25-162.4-59.71-28.06-48.59-26.57-134.08,47.86-178.37,56.94-33.88,132.01-24.49,177.28,22.78,47.32,49.42,44.56,141.96-1.59,177.96-20.91,16.34-51.97.43-51.77-23.39l-.21-7.79c-14.56,14.45-33.94,22.88-55.05,22.88-41.71,0-78.41-36.71-78.41-78.4,0-42.13,36.7-79.25,78.41-79.25h0ZM294.16,245.19c-1.57-30.54-24.24-48.91-51.62-48.91h-1.03c-31.59,0-49.11,24.84-49.11,53.06,0,31.6,21.2,51.56,48.99,51.56,30.99,0,51.37-22.7,52.84-49.55l-.07-6.17Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
17
src/assets/icons/yandex.svg
Normal file
17
src/assets/icons/yandex.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="yandex" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: white;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_x33_91-yandex">
|
||||
<rect width="512" height="512" rx="256" ry="256" fill="red"/>
|
||||
<g transform="scale(0.67, 0.67) translate(100, 128)">
|
||||
<path class="cls-1"
|
||||
d="M278.55,309.73l-78.52,176.27h-57.23l86.25-188.49c-40.52-20.58-67.56-57.86-67.56-126.77-.09-96.49,61.1-144.74,133.78-144.74h73.94v460h-49.5v-176.27h-41.15ZM319.7,67.78h-26.42c-39.89,0-78.52,26.41-78.52,102.96s35.4,97.75,78.52,97.75h26.42V67.78h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 665 B |
85
src/components/OAuthProviders/OAuthProviders.css
Normal file
85
src/components/OAuthProviders/OAuthProviders.css
Normal file
@ -0,0 +1,85 @@
|
||||
.provider-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.provider-container a {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease-in-out, filter 0.3s ease;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease, filter 0.3s ease;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.provider-container a:hover .provider-icon {
|
||||
transform: scale(1.1); /* Slight zoom-in effect on hover */
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* Adding shadow for effect */
|
||||
}
|
||||
|
||||
.provider-container .provider-item.disabled {
|
||||
pointer-events: none; /* Disables click */
|
||||
opacity: 0.5; /* Dims the icon to indicate it is disabled */
|
||||
}
|
||||
|
||||
.provider-container .provider-item.disabled .provider-icon {
|
||||
filter: grayscale(100%) contrast(100%); /* Desaturates image if disabled */
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink {
|
||||
filter: grayscale(50%) contrast(60%);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink:hover {
|
||||
filter: grayscale(0%) contrast(100%);
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink::after {
|
||||
content: '×';
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 90%;
|
||||
transform: translate(-100%, 0%) scale(0.0);
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink:hover::after {
|
||||
transform: translate(-50%, -50%) scale(1.0);
|
||||
}
|
||||
|
||||
.provider-item .provider-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(1.0);
|
||||
border-radius: 50%;
|
||||
}
|
23
src/components/OAuthProviders/OAuthProviders.html
Normal file
23
src/components/OAuthProviders/OAuthProviders.html
Normal file
@ -0,0 +1,23 @@
|
||||
@if (!loading && providers.length !== 0) {
|
||||
<hr/>
|
||||
<div>
|
||||
<p class="mat-body-2 secondary">{{ message }}</p>
|
||||
|
||||
<div class="provider-container">
|
||||
@for (provider of providers; track $index) {
|
||||
<a class="provider-item" (click)="provider.disabled ? confirmDelete(provider) : openOAuth(provider)"
|
||||
[class.disabled]="!canUnlink && provider.disabled || provider.active"
|
||||
[class.provider-unlink]="canUnlink && provider.disabled">
|
||||
<img [alt]="provider.providerName" [src]="provider.icon"
|
||||
class="provider-icon" draggable="false"/>
|
||||
@if (provider.active) {
|
||||
<app-data-spinner class="provider-spinner"/>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else if (loading) {
|
||||
<hr/>
|
||||
<app-data-spinner style="display: flex; justify-content: center;"/>
|
||||
}
|
194
src/components/OAuthProviders/OAuthProviders.ts
Normal file
194
src/components/OAuthProviders/OAuthProviders.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core';
|
||||
import AuthApiService, {OAuthProviderData} from "@api/v1/authApi.service";
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {catchError, finalize, Observable, switchMap, tap} from "rxjs";
|
||||
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||
import {OAuthAction} from "@model/oAuthAction";
|
||||
import SetupService from "@api/v1/setup.service";
|
||||
|
||||
interface AvailableOAuthProviders extends OAuthProviderData {
|
||||
disabled: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-confirm-dialog',
|
||||
template: `
|
||||
<h1 mat-dialog-title>Удалить провайдера?</h1>
|
||||
<mat-dialog-content>
|
||||
<p>Вы уверены, что хотите удалить провайдера {{ data.provider.name }}?</p>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions style="display: flex; justify-content: flex-end;">
|
||||
<button mat-button (click)="onCancel()">Отмена</button>
|
||||
<button mat-raised-button color="warn" (click)="onConfirm()">Удалить</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatButton
|
||||
]
|
||||
})
|
||||
export class DeleteConfirmDialog {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DeleteConfirmDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
}
|
||||
|
||||
onConfirm(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'OAuthProviders',
|
||||
imports: [
|
||||
DataSpinnerComponent
|
||||
],
|
||||
templateUrl: './OAuthProviders.html',
|
||||
styleUrl: './OAuthProviders.css',
|
||||
providers: [SetupService, AuthApiService]
|
||||
})
|
||||
export class OAuthProviders implements OnInit {
|
||||
protected providers: AvailableOAuthProviders[] = [];
|
||||
protected _activeProvidersId: OAuthProvider[] = [];
|
||||
protected _activeProviders: string[] = [];
|
||||
protected loading = true;
|
||||
|
||||
@Input() message: string = 'Вы можете войти в аккаунт через';
|
||||
|
||||
@Input() set activeProviders(data: string[]) {
|
||||
this._activeProviders = data;
|
||||
this.updateDisabledProviders();
|
||||
}
|
||||
|
||||
@Input() set activeProvidersId(data: OAuthProvider[]) {
|
||||
this._activeProvidersId = data;
|
||||
this.updateDisabledProviders();
|
||||
}
|
||||
|
||||
@Input() canUnlink: boolean = false;
|
||||
@Input() action: OAuthAction = OAuthAction.Login;
|
||||
@Input() isSetup: boolean = false;
|
||||
|
||||
@Output() public oAuthUpdateProviders = new EventEmitter();
|
||||
@Output() public oAuthLoginResult: EventEmitter<TwoFactorAuthentication> = new EventEmitter();
|
||||
|
||||
constructor(private setupApi: SetupService,
|
||||
private authApi: AuthApiService,
|
||||
private notify: ToastrService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const fullUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
|
||||
this.authApi.availableProviders(fullUrl).subscribe(providers => {
|
||||
this.updateDisabledProviders(providers);
|
||||
});
|
||||
|
||||
this.route.queryParamMap
|
||||
.pipe(
|
||||
switchMap(params => {
|
||||
const result = params.get('result');
|
||||
|
||||
if (!result) {
|
||||
this.loading = false; // Нет результата, завершение загрузки
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.handleOAuthResult(result); // Обрабатываем результат
|
||||
}),
|
||||
catchError(_ => {
|
||||
this.loading = false;
|
||||
return [];
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private handleOAuthResult(result: string): Observable<any> {
|
||||
switch (this.action) {
|
||||
case OAuthAction.Login:
|
||||
return this.authApi.loginOAuth(result).pipe(
|
||||
tap(auth => {
|
||||
this.oAuthLoginResult.emit(auth);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
);
|
||||
|
||||
case OAuthAction.Bind:
|
||||
if (this.isSetup) {
|
||||
return this.setupApi.registerOAuth(result).pipe(
|
||||
tap(() => {
|
||||
this.oAuthUpdateProviders.emit();
|
||||
}),
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
);
|
||||
} else
|
||||
throw new Error('Action "Bind" requires setup mode to be enabled.');
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown action type for action ' + this.action);
|
||||
}
|
||||
}
|
||||
|
||||
private updateDisabledProviders(data: OAuthProviderData[] | null = null) {
|
||||
this.providers = (data ?? this.providers).map(provider => {
|
||||
return {
|
||||
...provider,
|
||||
disabled: this._activeProvidersId.includes(provider.provider) || this._activeProviders.includes(provider.providerName),
|
||||
active: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected openOAuth(provider: AvailableOAuthProviders) {
|
||||
const oauthWindow = window.open(
|
||||
provider.redirect,
|
||||
'_self'
|
||||
);
|
||||
|
||||
if (!oauthWindow) {
|
||||
this.notify.error('Не удалось открыть OAuth окно');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected confirmDelete(provider: AvailableOAuthProviders) {
|
||||
const dialogRef = this.dialog.open(DeleteConfirmDialog, {data: {provider}});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.deleteProvider(provider);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected deleteProvider(provider: AvailableOAuthProviders) {
|
||||
// todo: remove provider
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<mat-card style="margin: 16px; padding: 16px;">
|
||||
<mat-card-header style="margin-bottom: 15px;">
|
||||
<mat-card-title>{{ title }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ng-content></ng-content>
|
||||
</mat-card-content>
|
||||
<mat-card-actions style="display: flex; justify-content: end; margin-top: 15px;">
|
||||
@if (isLoading) {
|
||||
<app-data-spinner/>
|
||||
} @else {
|
||||
<button mat-raised-button color="accent" [disabled]="!isSaveEnabled" (click)="onSave()">
|
||||
Сохранить
|
||||
</button>
|
||||
}
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
@ -0,0 +1,37 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {catchError, Observable, tap} from "rxjs";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-configuration-card',
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatButton,
|
||||
DataSpinnerComponent
|
||||
],
|
||||
templateUrl: './configuration-card.component.html'
|
||||
})
|
||||
export class ConfigurationCardComponent {
|
||||
@Input() title: string = '';
|
||||
@Input() isSaveEnabled: boolean = false;
|
||||
@Input() saveFunction!: () => Observable<any>;
|
||||
@Output() onSaveFunction = new EventEmitter<any>();
|
||||
|
||||
protected isLoading: boolean = false;
|
||||
|
||||
onSave(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
const result = this.saveFunction().pipe(catchError(err => {
|
||||
this.isLoading = false;
|
||||
throw err;
|
||||
}),
|
||||
tap(_ => {
|
||||
this.isLoading = false;
|
||||
}));
|
||||
|
||||
this.onSaveFunction.emit(result);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<h2 mat-dialog-title>Удаление расписания при изменении значения</h2>
|
||||
<mat-dialog-content>
|
||||
<p>Вы хотите удалить старое расписание?</p>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Нет</button>
|
||||
<button mat-button [mat-dialog-close]="true" color="warn">Да, удалить</button>
|
||||
</mat-dialog-actions>
|
@ -0,0 +1,22 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-delete-schedule-dialog',
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatButton
|
||||
],
|
||||
templateUrl: './confirm-delete-schedule-dialog.component.html'
|
||||
})
|
||||
export class ConfirmDeleteScheduleDialogComponent {
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<app-configuration-card [title]="'Cron для обновление расписания'"
|
||||
[isSaveEnabled]="cronExpression != cronExpressionBefore"
|
||||
[saveFunction]="saveFunction()"
|
||||
(onSaveFunction)="onSave($event)">
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>cron</mat-label>
|
||||
<input matInput type="text" [(ngModel)]="cronExpression"/>
|
||||
</mat-form-field>
|
||||
|
||||
<p>Следующие запуски:</p>
|
||||
<ul>
|
||||
@for (date of nextRunDates; track $index) {
|
||||
<li>{{ date }}</li>
|
||||
}
|
||||
</ul>
|
||||
</app-configuration-card>
|
@ -0,0 +1,48 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {Observable} from "rxjs";
|
||||
import {CronUpdateScheduleResponse} from "@api/v1/configuration/cronUpdateScheduleResponse";
|
||||
|
||||
@Component({
|
||||
selector: 'app-cron-update-schedule',
|
||||
imports: [
|
||||
ConfigurationCardComponent,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './cron-update-schedule.component.html',
|
||||
providers: [ScheduleService]
|
||||
})
|
||||
export class CronUpdateScheduleComponent {
|
||||
protected nextRunDates: string[] = [];
|
||||
protected cronExpression: string = '';
|
||||
protected cronExpressionBefore: string = '';
|
||||
|
||||
constructor(private api: ScheduleService) {
|
||||
api.getCronUpdateSchedule().subscribe(data => {
|
||||
this.nextRunDates = data.nextStart?.map(x => this.convertDateToString(x)) ?? [];
|
||||
this.cronExpression = data.cron;
|
||||
this.cronExpressionBefore = data.cron;
|
||||
});
|
||||
}
|
||||
|
||||
private convertDateToString(data: Date): string {
|
||||
data = new Date(data);
|
||||
return data.toLocaleDateString() + ' ' + data.toLocaleTimeString();
|
||||
}
|
||||
|
||||
protected saveFunction() {
|
||||
return () => this.api.postCronUpdateSchedule(this.cronExpression);
|
||||
}
|
||||
|
||||
protected onSave(data: Observable<CronUpdateScheduleResponse>): void {
|
||||
data.subscribe(apiData => {
|
||||
this.nextRunDates = apiData.nextStart?.map(x => this.convertDateToString(x)) ?? [];
|
||||
this.cronExpressionBefore = apiData.cron;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<app-configuration-card
|
||||
[title]="'Загрузка расписания Excel'"
|
||||
[isSaveEnabled]="selectedFiles.length > 0"
|
||||
[saveFunction]="saveFunction()"
|
||||
(onSaveFunction)="onUpload($event)">
|
||||
|
||||
<input type="file" #fileInput (change)="onFileSelected($event)" multiple accept=".xlsx, .xls" style="display: none;">
|
||||
|
||||
@if (fileLoading) {
|
||||
<app-data-spinner/>
|
||||
} @else {
|
||||
<button mat-raised-button color="primary" (click)="onFileChooseClick()">
|
||||
Выберите файлы
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (selectedFiles.length > 0) {
|
||||
<div style="margin-top: 15px;">
|
||||
<p>Выбранные файлы:</p>
|
||||
@for (item of selectedFiles; track $index) {
|
||||
<p>
|
||||
{{ item.file.name }}
|
||||
</p>
|
||||
<mat-form-field color="accent" style="margin-bottom: 18px;">
|
||||
<mat-label>Кампус по умолчанию</mat-label>
|
||||
<input matInput type="text" [(ngModel)]="item.campus"
|
||||
[matAutocomplete]="auto">
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onSelectCampus($event.option.value, item)">
|
||||
@for (option of onFilter(item.campus); track $index) {
|
||||
<mat-option [value]="option">
|
||||
{{ option }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</app-configuration-card>
|
@ -0,0 +1,91 @@
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {
|
||||
ConfirmDeleteScheduleDialogComponent
|
||||
} from "@component/admin/confirm-delete-schedule-dialog/confirm-delete-schedule-dialog.component";
|
||||
import {Observable, switchMap} from "rxjs";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {MatAutocomplete, MatAutocompleteTrigger, MatOption} from "@angular/material/autocomplete";
|
||||
import {CampusService} from "@api/v1/campus.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-file-upload',
|
||||
imports: [
|
||||
ConfigurationCardComponent,
|
||||
MatButtonModule,
|
||||
MatIcon,
|
||||
DataSpinnerComponent,
|
||||
MatFormFieldModule,
|
||||
FormsModule,
|
||||
MatInput,
|
||||
MatAutocomplete,
|
||||
MatAutocompleteTrigger,
|
||||
MatOption,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
templateUrl: './schedule-file-upload.component.html',
|
||||
providers: [ScheduleService, CampusService]
|
||||
})
|
||||
export class ScheduleFileUploadComponent {
|
||||
protected selectedFiles: { file: File, campus: string }[] = [];
|
||||
protected fileLoading: boolean = false;
|
||||
protected campuses: string[] = [];
|
||||
@ViewChild('fileInput') input!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private api: ScheduleService,
|
||||
private notify: ToastrService,
|
||||
campus: CampusService) {
|
||||
campus.getCampus().subscribe(data => {
|
||||
this.campuses = data.map(x => x.codeName);
|
||||
});
|
||||
}
|
||||
|
||||
protected onSelectCampus(value: string, item: { file: File, campus: string }) {
|
||||
item.campus = value;
|
||||
}
|
||||
|
||||
protected saveFunction() {
|
||||
return () => {
|
||||
const dialogRef = this.dialog.open(ConfirmDeleteScheduleDialogComponent);
|
||||
|
||||
return dialogRef.afterClosed().pipe(switchMap(result => {
|
||||
return this.api.uploadScheduleFile(
|
||||
this.selectedFiles.map(x => x.file),
|
||||
this.selectedFiles.map(x => x.campus),
|
||||
result);
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
protected onFilter(value: string): string[] {
|
||||
const filterValue = value?.toLowerCase() || '';
|
||||
return this.campuses.filter(campus => campus.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
protected onFileChooseClick() {
|
||||
this.fileLoading = true;
|
||||
this.input.nativeElement.click();
|
||||
}
|
||||
|
||||
protected onFileSelected(event: any): void {
|
||||
this.fileLoading = false;
|
||||
this.selectedFiles = Array.from(event.target.files).map(file => ({file: <File>file, campus: ''}));
|
||||
}
|
||||
|
||||
protected onUpload(data: Observable<any>): void {
|
||||
data.subscribe(_ => {
|
||||
this.notify.info(`Файлы в размере ${this.selectedFiles.length} успешно загружены. Задача поставлена в очередь`);
|
||||
this.selectedFiles = [];
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
.date-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.date-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button[mat-icon-button] {
|
||||
margin-left: auto;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<app-configuration-card [title]="'Список пропуска обновления расписания'"
|
||||
[isSaveEnabled]="validateSaveButton() && !isDisableAddNewItem()"
|
||||
[saveFunction]="saveFunction()"
|
||||
(onSaveFunction)="onSave($event)">
|
||||
|
||||
<div class="date-list">
|
||||
@for (dateItem of dateItems; track $index) {
|
||||
<div class="date-item">
|
||||
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Диапазон дат</mat-label>
|
||||
<mat-date-range-input [rangePicker]="rangePicker"
|
||||
[disabled]="dateItems[$index].date">
|
||||
<input matStartDate [(ngModel)]="dateItem.start"
|
||||
placeholder="Начало"
|
||||
(dateChange)="validateDate($index)"
|
||||
[min]="CurrentDate">
|
||||
<input matEndDate [(ngModel)]="dateItem.end"
|
||||
placeholder="Конец"
|
||||
(dateChange)="validateDate($index)"
|
||||
[min]="CurrentDate">
|
||||
</mat-date-range-input>
|
||||
<mat-datepicker-toggle matSuffix [for]="rangePicker"></mat-datepicker-toggle>
|
||||
<mat-date-range-picker #rangePicker></mat-date-range-picker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Конкретная дата</mat-label>
|
||||
<input matInput [matDatepicker]="specificDatePicker"
|
||||
[(ngModel)]="dateItem.date"
|
||||
(dateChange)="validateDate($index)"
|
||||
[min]="CurrentDate"
|
||||
[disabled]="dateItems[$index].start != null || dateItems[$index].end != null">
|
||||
<mat-datepicker-toggle matSuffix [for]="specificDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #specificDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-icon-button color="warn" (click)="removeDate($index)" style="height: 100%;">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="accent"
|
||||
[disabled]="isDisableAddNewItem()"
|
||||
(click)="addDate()">
|
||||
<mat-icon>add</mat-icon>
|
||||
Добавить строку
|
||||
</button>
|
||||
</app-configuration-card>
|
@ -0,0 +1,93 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||
import CronUpdateSkip from "@model/cronUpdateSkip";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {addDays} from "@progress/kendo-date-math";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-skip-update-schedule',
|
||||
imports: [
|
||||
MatFormFieldModule,
|
||||
MatDatepickerModule,
|
||||
MatInput,
|
||||
FormsModule,
|
||||
ConfigurationCardComponent,
|
||||
MatButtonModule,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './skip-update-schedule.component.html',
|
||||
styleUrl: './skip-update-schedule.component.css',
|
||||
providers: [ScheduleService]
|
||||
})
|
||||
export class SkipUpdateScheduleComponent {
|
||||
dateItems: { start?: Date, end?: Date, date?: Date }[] = [];
|
||||
dateItemsBefore: { start?: Date, end?: Date, date?: Date }[] = [];
|
||||
|
||||
constructor(private api: ScheduleService) {
|
||||
api.getCronUpdateSkip().subscribe(data => {
|
||||
this.dateItems = data.map(x => <{ start?: Date, end?: Date, date?: Date }>{
|
||||
start: x.start?.date,
|
||||
end: x.end?.date,
|
||||
date: x.date?.date
|
||||
});
|
||||
if (this.dateItems.length == 0)
|
||||
this.addDate();
|
||||
|
||||
this.dateItemsBefore = JSON.parse(JSON.stringify(this.dateItems));
|
||||
});
|
||||
}
|
||||
|
||||
addDate(): void {
|
||||
this.dateItems.push({start: undefined, end: undefined, date: undefined});
|
||||
}
|
||||
|
||||
removeDate(index: number): void {
|
||||
this.dateItems.splice(index, 1);
|
||||
}
|
||||
|
||||
validateDate(index: number): void {
|
||||
const item = this.dateItems[index];
|
||||
|
||||
if (item.start && item.start < this.CurrentDate)
|
||||
item.start = undefined;
|
||||
if (item.end && item.end < this.CurrentDate)
|
||||
item.end = undefined;
|
||||
if (item.date && item.date < this.CurrentDate)
|
||||
item.date = undefined;
|
||||
}
|
||||
|
||||
isDisableAddNewItem() {
|
||||
return this.dateItems.some(item => (!item.start || !item.end) && !item.date);
|
||||
}
|
||||
|
||||
validateSaveButton(): boolean {
|
||||
return (this.dateItems.length == 0 || this.dateItems.some(item =>
|
||||
(item.start && item.end) || item.date
|
||||
)) && JSON.stringify(this.dateItems) != JSON.stringify(this.dateItemsBefore);
|
||||
}
|
||||
|
||||
saveFunction() {
|
||||
return () => this.api.postCronUpdateSkip(this.dateItems.map(x =>
|
||||
<CronUpdateSkip>{
|
||||
start: x.start ? new DateOnly(x.start) : undefined,
|
||||
end: x.end ? new DateOnly(x.end) : undefined,
|
||||
date: x.date ? new DateOnly(x.date) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
onSave(event: Observable<any>): void {
|
||||
event.subscribe(_ => {
|
||||
this.dateItemsBefore = JSON.parse(JSON.stringify(this.dateItems));
|
||||
});
|
||||
}
|
||||
|
||||
protected CurrentDate: Date = addDays(new Date(), -1);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<app-configuration-card
|
||||
[title]="'Дата начала семестра'"
|
||||
[isSaveEnabled]="startDate != startDateBefore"
|
||||
[saveFunction]="saveFunction()"
|
||||
(onSaveFunction)="onSave($event)">
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Дата начала семестра</mat-label>
|
||||
<input matInput [matDatepicker]="datePicker" [(ngModel)]="startDate" [min]="ValidMinDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="datePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #datePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</app-configuration-card>
|
@ -0,0 +1,61 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from "@angular/material/datepicker";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {addDays} from "@progress/kendo-date-math";
|
||||
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {
|
||||
ConfirmDeleteScheduleDialogComponent
|
||||
} from "@component/admin/confirm-delete-schedule-dialog/confirm-delete-schedule-dialog.component";
|
||||
import {Observable, switchMap} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-term-start-date',
|
||||
imports: [
|
||||
MatFormFieldModule,
|
||||
MatDatepickerToggle,
|
||||
FormsModule,
|
||||
MatDatepickerInput,
|
||||
MatDatepicker,
|
||||
ConfigurationCardComponent,
|
||||
MatInput
|
||||
],
|
||||
templateUrl: './term-start-date.component.html',
|
||||
providers: [ScheduleService]
|
||||
})
|
||||
export class TermStartDateComponent {
|
||||
protected startDate: Date = new Date();
|
||||
protected startDateBefore: Date = new Date();
|
||||
|
||||
constructor(private api: ScheduleService, private dialog: MatDialog) {
|
||||
this.api.getStartTerm().subscribe(data => {
|
||||
this.startDate = data.date;
|
||||
this.startDateBefore = this.startDate;
|
||||
console.log(this.startDate == this.startDateBefore);
|
||||
});
|
||||
}
|
||||
|
||||
protected saveFunction() {
|
||||
return () => {
|
||||
const dialogRef = this.dialog.open(ConfirmDeleteScheduleDialogComponent, {
|
||||
data: {startDate: this.startDate}
|
||||
});
|
||||
|
||||
return dialogRef.afterClosed().pipe(switchMap(result => {
|
||||
return this.api.postStartTerm(new DateOnly(this.startDate), result);
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
protected onSave(data: Observable<any>): void {
|
||||
data.subscribe(_ => {
|
||||
this.startDateBefore = this.startDate;
|
||||
});
|
||||
}
|
||||
|
||||
protected ValidMinDate = addDays(new Date(), -180);
|
||||
}
|
@ -1 +1 @@
|
||||
<mat-progress-spinner [color]="color" mode="indeterminate" [diameter]="scale" />
|
||||
<mat-progress-spinner [color]="color" mode="indeterminate" [diameter]="scale"/>
|
||||
|
@ -1,13 +1,11 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
||||
import {NgStyle} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-data-spinner',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatProgressSpinner,
|
||||
NgStyle
|
||||
],
|
||||
templateUrl: './data-spinner.component.html'
|
||||
})
|
||||
|
@ -22,8 +22,8 @@
|
||||
<hr/>
|
||||
<div class="app-footer-copyright">
|
||||
<span>Powered by <a href="https://winsomnia.net">Winsomnia</a> ©{{ currentYear }}.</span>
|
||||
<a href="https://opensource.org/license/mit/">Code licensed under an MIT-style License.</a>
|
||||
<span>Current Version: {{ version }}</span>
|
||||
<a href="https://opensource.org/license/mit/">Code licensed under an MIT-style License.</a>
|
||||
<span>Current Version: {{ version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
9
src/components/common/header/header.component.css
Normal file
9
src/components/common/header/header.component.css
Normal file
@ -0,0 +1,9 @@
|
||||
mat-toolbar a {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mat-toolbar a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
4
src/components/common/header/header.component.html
Normal file
4
src/components/common/header/header.component.html
Normal file
@ -0,0 +1,4 @@
|
||||
<mat-toolbar style="justify-content: space-between;">
|
||||
<a href="/" style="color: inherit;">Winsomnia</a>
|
||||
<a href="/admin" style="color: inherit; font-size: 14px" *appHasRole="AuthRoles.Admin">Админ панель</a>
|
||||
</mat-toolbar>
|
19
src/components/common/header/header.component.ts
Normal file
19
src/components/common/header/header.component.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatToolbar} from "@angular/material/toolbar";
|
||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatToolbar,
|
||||
HasRoleDirective
|
||||
],
|
||||
templateUrl: './header.component.html',
|
||||
styleUrl: './header.component.css'
|
||||
})
|
||||
export class HeaderComponent {
|
||||
|
||||
protected readonly AuthRoles = AuthRoles;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
@if (loading) {
|
||||
<app-data-spinner/>
|
||||
} @else {
|
||||
<button mat-fab color="primary" (click)="retryFunction.emit()">
|
||||
<button mat-fab color="primary" (click)="retryLoad()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {MatButton, MatFabButton} from "@angular/material/button";
|
||||
import {MatFabButton} from "@angular/material/button";
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading-indicator',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DataSpinnerComponent,
|
||||
MatButton,
|
||||
MatIcon,
|
||||
MatFabButton
|
||||
],
|
||||
@ -18,4 +17,9 @@ import {MatButton, MatFabButton} from "@angular/material/button";
|
||||
export class LoadingIndicatorComponent {
|
||||
@Input() loading: boolean = true;
|
||||
@Output() retryFunction: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
protected retryLoad() {
|
||||
this.loading = true;
|
||||
this.retryFunction.emit();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
.notification-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
margin-left: 8px;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<div class="notification-content" [class]="data.className">
|
||||
<span>
|
||||
{{ data.message }}
|
||||
</span>
|
||||
<button mat-icon-button class="close-button" (click)="dismiss()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@if (showProgressBar) {
|
||||
<mat-progress-bar mode="determinate" [value]="progress" [color]="color"/>
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {MatProgressBar} from "@angular/material/progress-bar";
|
||||
import {MAT_SNACK_BAR_DATA, MatSnackBarRef} from "@angular/material/snack-bar";
|
||||
import {MatIconButton} from "@angular/material/button";
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatIconButton,
|
||||
MatIcon,
|
||||
MatProgressBar
|
||||
],
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrl: './notification.component.css'
|
||||
})
|
||||
|
||||
export class NotificationComponent {
|
||||
showProgressBar: boolean = false;
|
||||
progress: number = 100;
|
||||
color: string = "primary";
|
||||
|
||||
constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any, private snackBarRef: MatSnackBarRef<NotificationComponent>) {
|
||||
if (data.duration) {
|
||||
this.startProgress(data.duration);
|
||||
this.showProgressBar = true;
|
||||
}
|
||||
if (data.color) {
|
||||
this.color = data.color;
|
||||
}
|
||||
}
|
||||
|
||||
dismiss(): void {
|
||||
this.snackBarRef.dismiss();
|
||||
}
|
||||
|
||||
private startProgress(duration: number): void {
|
||||
const interval: number = duration / 100;
|
||||
const progressInterval = setInterval(async () => {
|
||||
this.progress--;
|
||||
if (this.progress === 0) {
|
||||
clearInterval(progressInterval);
|
||||
setTimeout(() => {
|
||||
this.dismiss();
|
||||
}, 1000);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<div [formGroup]="formGroup" style="display: flex; flex-direction: column; align-items: stretch;">
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Пароль</mat-label>
|
||||
<input matInput
|
||||
matTooltip="Укажите пароль"
|
||||
formControlName="password"
|
||||
required
|
||||
[type]="hidePass ? 'password' : 'text'"
|
||||
id="passwordNextFocus"
|
||||
focusNext="focusNext">
|
||||
|
||||
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||
[attr.aria-pressed]="hidePass">
|
||||
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
@if (formGroup.get('password')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Пароль является <i>обязательным</i>
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@if (formGroup.get('password')?.hasError('minlength')) {
|
||||
<mat-error>
|
||||
Пароль должен быть не менее {{ policy.minimumLength }} символов
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@if (formGroup.get('password')?.hasError('pattern')) {
|
||||
<mat-error>
|
||||
Пароль должен содержать:
|
||||
@if (policy.requireLettersDifferentCase) {
|
||||
Латинские символы разных регистров
|
||||
} @else if (policy.requireLetter) {
|
||||
Один латинский символ
|
||||
} @else if (policy.requireDigit) {
|
||||
Одну цифру
|
||||
}
|
||||
@if (policy.requireSpecialCharacter) {
|
||||
Специальный символ
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
@ -0,0 +1,76 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatIconButton} from "@angular/material/button";
|
||||
import {FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {PasswordPolicy} from "@model/passwordPolicy";
|
||||
import SecurityService from "@api/v1/securityService";
|
||||
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||
import SetupService from "@api/v1/setup.service";
|
||||
|
||||
@Component({
|
||||
selector: 'password-input',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatInput,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
MatIcon,
|
||||
FocusNextDirective
|
||||
],
|
||||
templateUrl: './password-input.component.html',
|
||||
providers: [SecurityService, SetupService]
|
||||
})
|
||||
export class PasswordInputComponent {
|
||||
protected hidePass = true;
|
||||
protected policy!: PasswordPolicy;
|
||||
@Input() formGroup!: FormGroup;
|
||||
@Input() focusNext: string | undefined;
|
||||
@Input() isSetupMode: boolean = false;
|
||||
|
||||
constructor(securityApi: SecurityService, setupApi: SetupService) {
|
||||
if (this.isSetupMode)
|
||||
setupApi.passwordPolicyConfiguration().subscribe(policy => {
|
||||
this.policy = policy;
|
||||
this.updateValueAndValidity(policy);
|
||||
});
|
||||
else
|
||||
securityApi.passwordPolicy().subscribe(policy => {
|
||||
this.policy = policy;
|
||||
this.updateValueAndValidity(policy);
|
||||
});
|
||||
}
|
||||
|
||||
private updateValueAndValidity(policy: PasswordPolicy): void {
|
||||
const validators: ValidatorFn[] = [Validators.required];
|
||||
|
||||
if (policy.minimumLength) {
|
||||
validators.push(Validators.minLength(policy.minimumLength));
|
||||
}
|
||||
|
||||
if (policy.requireLettersDifferentCase) {
|
||||
validators.push(Validators.pattern(/(?=.*[a-z])(?=.*[A-Z])/));
|
||||
} else if (policy.requireLetter) {
|
||||
validators.push(Validators.pattern(/[A-Za-z]/));
|
||||
} else if (policy.requireDigit) {
|
||||
validators.push(Validators.pattern(/\d/));
|
||||
}
|
||||
|
||||
if (policy.requireSpecialCharacter) {
|
||||
validators.push(Validators.pattern(/[!@#$%^&*(),.?":{}|<>]/));
|
||||
}
|
||||
|
||||
this.formGroup.get('password')?.setValidators(validators);
|
||||
this.formGroup.get('password')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
protected togglePassword(event: MouseEvent) {
|
||||
this.hidePass = !this.hidePass;
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
<section class="mat-elevation-z8 table-section" tabindex="0">
|
||||
<section class="mat-elevation-z8 table-section" tabindex="0"
|
||||
[style.overflow]="(dataSource.length === 0 || isLoad ? 'hidden' : 'auto')">
|
||||
@if (dataSource.length === 0 || isLoad) {
|
||||
<div class="overlay">
|
||||
@if (isLoad) {
|
||||
@ -50,10 +51,12 @@
|
||||
<div class="mat-body-1">{{ elementData["discipline"] }}</div>
|
||||
<!-- Type of Occupation -->
|
||||
@for (typeOfOccupation of elementData["typeOfOccupations"]; track $index) {
|
||||
@if ($index !== 0) {
|
||||
<br/>
|
||||
@if ($index === 0 && elementData["typeOfOccupations"][$index - 1] !== typeOfOccupation) {
|
||||
@if ($index !== 0) {
|
||||
<br/>
|
||||
}
|
||||
<div class="mat-body">({{ typeOfOccupation }})</div>
|
||||
}
|
||||
<div class="mat-body">({{typeOfOccupation}})</div>
|
||||
}
|
||||
|
||||
<!-- Professors -->
|
||||
@ -97,14 +100,12 @@
|
||||
}
|
||||
|
||||
<!-- Group -->
|
||||
@if (!isOneGroup) {
|
||||
<div class="mat-body">
|
||||
<i>
|
||||
<mat-icon fontIcon="group"/>
|
||||
</i>
|
||||
{{ elementData["group"] }}
|
||||
</div>
|
||||
}
|
||||
<div class="mat-body">
|
||||
<i>
|
||||
<mat-icon fontIcon="group"/>
|
||||
</i>
|
||||
{{ elementData["group"] }}
|
||||
</div>
|
||||
|
||||
@if ($index + 1 !== element.data[daysOfWeek.indexOf(day) + 1].length) {
|
||||
<hr style="margin: 10px 0;"/>
|
||||
|
@ -3,7 +3,6 @@ import {MatTableDataSource, MatTableModule} from "@angular/material/table";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {DatePipe} from "@angular/common";
|
||||
import {addDays} from "@progress/kendo-date-math";
|
||||
import {MatDivider} from "@angular/material/divider";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||
|
||||
@ -23,7 +22,6 @@ interface Dictionary {
|
||||
MatTableModule,
|
||||
MatIcon,
|
||||
DatePipe,
|
||||
MatDivider,
|
||||
DataSpinnerComponent
|
||||
],
|
||||
templateUrl: './table.component.html',
|
||||
@ -31,20 +29,25 @@ interface Dictionary {
|
||||
})
|
||||
|
||||
export class TableComponent implements OnChanges {
|
||||
protected tableDataSource: MatTableDataSource<TableData> = new MatTableDataSource<TableData>([]);
|
||||
protected daysOfWeek: string[] = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
|
||||
protected displayedColumns: string[] = ['pairNumber'];
|
||||
protected dataSource: ScheduleResponse[] = [];
|
||||
protected isOneGroup: boolean = false;
|
||||
|
||||
@Input() currentWeek!: number;
|
||||
@Input() startWeek!: Date;
|
||||
@Input() isLoad: boolean = false;
|
||||
|
||||
private isDisciplineWithWeeks: boolean = false;
|
||||
protected tableDataSource: MatTableDataSource<TableData> = new MatTableDataSource<TableData>([]);
|
||||
private backupDisciplines: string[] = [];
|
||||
protected daysOfWeek: string[] = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
|
||||
protected displayedColumns: string[] = ['pairNumber'];
|
||||
protected dataSource: ScheduleResponse[] = [];
|
||||
|
||||
@Input() set disciplineWithWeeks(value: boolean) {
|
||||
this.isDisciplineWithWeeks = value;
|
||||
this.convertData();
|
||||
}
|
||||
|
||||
@Input() set data(schedule: ScheduleResponse[]) {
|
||||
this.dataSource = schedule;
|
||||
this.convertData();
|
||||
this.isOneGroup = schedule.every((item, _, array) => item.group === array[0].group);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: any) {
|
||||
@ -61,21 +64,52 @@ export class TableComponent implements OnChanges {
|
||||
this.isLoad = true;
|
||||
let tableData: TableData[] = [];
|
||||
|
||||
for (let i: number = 1; i <= 7; i++) {
|
||||
for (let pairNumber: number = 1; pairNumber <= 7; pairNumber++) {
|
||||
let convertedData: TableData = {
|
||||
pairNumber: i,
|
||||
pairNumber: pairNumber,
|
||||
data: {}
|
||||
};
|
||||
|
||||
for (let k: number = 1; k < 7; k++) {
|
||||
convertedData.data[k.toString()] = this.dataSource.filter(x =>
|
||||
x.pairNumber === i &&
|
||||
x.dayOfWeek === k &&
|
||||
x.isEven === (this.currentWeek % 2 === 0) &&
|
||||
(
|
||||
(x.isExcludedWeeks && (!x.weeks || !x.weeks.includes(this.currentWeek))) ||
|
||||
(!x.isExcludedWeeks && (!x.weeks || x.weeks.includes(this.currentWeek)))
|
||||
));
|
||||
for (let dayOfWeek: number = 1; dayOfWeek < 7; dayOfWeek++) {
|
||||
let filteredData = this.dataSource.filter(x =>
|
||||
x.pairNumber === pairNumber &&
|
||||
x.dayOfWeek === dayOfWeek &&
|
||||
x.isEven === (this.currentWeek % 2 === 0)
|
||||
);
|
||||
|
||||
if (!this.isDisciplineWithWeeks)
|
||||
filteredData = filteredData.filter(x =>
|
||||
x.isExcludedWeeks == undefined ||
|
||||
x.weeks == undefined ||
|
||||
x.weeks.length == 0 ||
|
||||
(x.isExcludedWeeks && !x.weeks.includes(this.currentWeek)) ||
|
||||
(!x.isExcludedWeeks && x.weeks.includes(this.currentWeek))
|
||||
);
|
||||
|
||||
const groupedData = filteredData.reduce((acc, item) => {
|
||||
const key = `${item.typeOfOccupations.join(', ')}-${item.lectureHalls}-${item.campus}-${item.discipline}-${item.professors.join(', ')}-${item.isExcludedWeeks}-${item.weeks?.join(', ') || ''}`;
|
||||
if (!acc[key])
|
||||
acc[key] = {...item, groups: [item.group]};
|
||||
else
|
||||
acc[key].groups.push(item.group);
|
||||
return acc;
|
||||
}, {} as { [key: string]: ScheduleResponse & { groups: string[] } });
|
||||
|
||||
convertedData.data[dayOfWeek.toString()] = Object.values(groupedData).map(item => {
|
||||
item.group = item.groups.join(', ');
|
||||
|
||||
if (this.isDisciplineWithWeeks && item.weeks !== undefined && item.weeks.length > 0 && item.isExcludedWeeks !== undefined) {
|
||||
if (this.backupDisciplines[item.disciplineId])
|
||||
item.discipline = this.backupDisciplines[item.disciplineId];
|
||||
else
|
||||
this.backupDisciplines[item.disciplineId] = item.discipline;
|
||||
|
||||
item.discipline = `${item.isExcludedWeeks ? 'кр.' : ''} ${item.weeks.sort((x, y) => x - y).join(', ')} н. ${item.discipline}`;
|
||||
} else if (this.backupDisciplines[item.disciplineId])
|
||||
item.discipline = this.backupDisciplines[item.disciplineId];
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
tableData.push(convertedData);
|
||||
|
@ -0,0 +1,5 @@
|
||||
.div-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
@ -5,14 +5,18 @@
|
||||
Факультет
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="chooseFaculty($event)">
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onFacultySelected($event.value)" #facultyChip>
|
||||
|
||||
@for (faculty of faculties; track $index) {
|
||||
<mat-chip-option [value]="faculty.id" color="accent">
|
||||
{{ faculty.name }}
|
||||
</mat-chip-option>
|
||||
} @empty {
|
||||
<app-loading-indicator [loading]="facultiesLoaded !== null"
|
||||
(retryFunction)="loadFaculties()"/>
|
||||
}
|
||||
|
||||
@if (faculties === null) {
|
||||
<app-loading-indicator [loading]="true"
|
||||
(retryFunction)="loadFaculties()"
|
||||
#facultyIndicator/>
|
||||
}
|
||||
</mat-chip-listbox>
|
||||
</mat-expansion-panel>
|
||||
@ -23,14 +27,20 @@
|
||||
Курс
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="chooseCourseNumber($event)" [formControl]="formChipCourse">
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onCourseSelected($event.value)"
|
||||
[formControl]="formChipCourse"
|
||||
#courseChip>
|
||||
|
||||
@for (course of courseNumbers; track $index) {
|
||||
<mat-chip-option [value]="course" color="accent">
|
||||
{{ course }}
|
||||
</mat-chip-option>
|
||||
} @empty {
|
||||
<app-loading-indicator [loading]="groupsLoaded !== null"
|
||||
(retryFunction)="loadCourseGroup()"/>
|
||||
}
|
||||
|
||||
@if (courseNumbers === null) {
|
||||
<app-loading-indicator [loading]="true"
|
||||
(retryFunction)="loadCourseGroup()"
|
||||
#courseIndicator/>
|
||||
}
|
||||
</mat-chip-listbox>
|
||||
</mat-expansion-panel>
|
||||
@ -41,14 +51,70 @@
|
||||
Группа
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="chooseGroup($event)" [formControl]="formChipGroup">
|
||||
@for (group of filteredGroups; track $index) {
|
||||
<mat-chip-option [value]="group.id" color="accent">
|
||||
{{ group.name }}
|
||||
</mat-chip-option>
|
||||
} @empty {
|
||||
<app-loading-indicator [loading]="groupsLoaded !== null"
|
||||
(retryFunction)="loadCourseGroup()"/>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onGroupSelected($event.value)"
|
||||
[formControl]="formChipGroup"
|
||||
#groupChip>
|
||||
|
||||
@if (filteredGroupsSpecialist && filteredGroupsSpecialist.length > 0) {
|
||||
<div class="div-wrapper">
|
||||
Специалитет:
|
||||
</div>
|
||||
|
||||
<div class="div-wrapper">
|
||||
@for (group of filteredGroupsSpecialist; track $index) {
|
||||
<mat-chip-option [value]="group.id" color="accent">
|
||||
{{ group.name }}
|
||||
</mat-chip-option>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filteredGroupsSpecialist && filteredGroupsSpecialist.length > 0 && (filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0 || filteredGroupsMagistracy && filteredGroupsMagistracy.length > 0)) {
|
||||
<div class="div-wrapper">
|
||||
<hr/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0) {
|
||||
<div class="div-wrapper">
|
||||
Бакалавариат:
|
||||
</div>
|
||||
|
||||
<div class="div-wrapper">
|
||||
@for (group of filteredGroupsBehaviour; track $index) {
|
||||
<mat-chip-option [value]="group.id" color="accent">
|
||||
{{ group.name }}
|
||||
</mat-chip-option>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (((filteredGroupsSpecialist && filteredGroupsSpecialist.length > 0 && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0) ||
|
||||
((!filteredGroupsSpecialist || filteredGroupsSpecialist.length === 0) && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0)) &&
|
||||
filteredGroupsMagistracy && filteredGroupsMagistracy.length > 0) {
|
||||
<div class="div-wrapper">
|
||||
<hr/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filteredGroupsMagistracy && filteredGroupsMagistracy.length > 0) {
|
||||
<div class="div-wrapper">
|
||||
Магистратура:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@for (group of filteredGroupsMagistracy; track $index) {
|
||||
<mat-chip-option [value]="group.id" color="accent">
|
||||
{{ group.name }}
|
||||
</mat-chip-option>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (faculties === null) {
|
||||
<app-loading-indicator [loading]="true"
|
||||
(retryFunction)="loadCourseGroup()"
|
||||
#groupIndicator/>
|
||||
}
|
||||
</mat-chip-listbox>
|
||||
</mat-expansion-panel>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Component, EventEmitter, Output, ViewChild} from '@angular/core';
|
||||
import {Component, EventEmitter, ViewChild} from '@angular/core';
|
||||
import {MatExpansionModule, MatExpansionPanel} from "@angular/material/expansion";
|
||||
import {MatChipListboxChange, MatChipsModule} from '@angular/material/chips';
|
||||
import {MatChipListbox, MatChipsModule} from '@angular/material/chips';
|
||||
import {FormControl, FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {catchError} from "rxjs";
|
||||
import {LoadingIndicatorComponent} from "@component/common/loading-indicator/loading-indicator.component";
|
||||
@ -8,6 +8,12 @@ import {GroupResponse} from "@api/v1/groupResponse";
|
||||
import {FacultyResponse} from "@api/v1/facultyResponse";
|
||||
import {FacultyService} from "@api/v1/faculty.service";
|
||||
import {GroupService} from "@api/v1/group.service";
|
||||
import {IScheduleTab} from "@component/schedule/tabs/ischedule-tab";
|
||||
import {TabSelect, TabStorageService} from "@service/tab-storage.service";
|
||||
|
||||
enum Enclosure {
|
||||
faculty, course, group
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-group',
|
||||
@ -23,122 +29,218 @@ import {GroupService} from "@api/v1/group.service";
|
||||
styleUrl: './group.component.css',
|
||||
providers: [FacultyService, GroupService]
|
||||
})
|
||||
export class GroupComponent implements IScheduleTab {
|
||||
protected faculties: FacultyResponse[] | null = null;
|
||||
protected courseNumbers: number[] | null = null;
|
||||
private groups: GroupResponse[] | null = null;
|
||||
protected filteredGroupsBehaviour: GroupResponse[] | null = null;
|
||||
protected filteredGroupsMagistracy: GroupResponse[] | null = null;
|
||||
protected filteredGroupsSpecialist: GroupResponse[] | null = null;
|
||||
|
||||
export class GroupComponent {
|
||||
protected facultyId: number | null = null;
|
||||
protected courseNumber: number | null = null;
|
||||
|
||||
protected filteredGroups: GroupResponse[] = [];
|
||||
protected courseNumbers: number[] = [];
|
||||
private groups: GroupResponse[] = [];
|
||||
|
||||
protected formChipCourse: FormControl = new FormControl();
|
||||
protected formChipGroup: FormControl = new FormControl();
|
||||
|
||||
protected faculties: FacultyResponse[] = [];
|
||||
|
||||
@ViewChild('courseNumberPanel') courseNumberPanel!: MatExpansionPanel;
|
||||
@ViewChild('groupPanel') groupPanel!: MatExpansionPanel;
|
||||
|
||||
protected facultiesLoaded: boolean | null = false;
|
||||
protected groupsLoaded: boolean | null = false;
|
||||
@ViewChild('facultyChip') facultyChip!: MatChipListbox;
|
||||
@ViewChild('courseChip') courseChip!: MatChipListbox;
|
||||
@ViewChild('groupChip') groupChip!: MatChipListbox;
|
||||
|
||||
@Output() eventResult = new EventEmitter<number>();
|
||||
@ViewChild('facultyIndicator') facultyIndicator!: LoadingIndicatorComponent;
|
||||
@ViewChild('courseIndicator') courseIndicator!: LoadingIndicatorComponent;
|
||||
@ViewChild('groupIndicator') groupIndicator!: LoadingIndicatorComponent;
|
||||
|
||||
private resetCourse() {
|
||||
this.courseNumber = null;
|
||||
this.groups = [];
|
||||
this.formChipCourse.reset();
|
||||
this.courseChip.value = undefined;
|
||||
}
|
||||
|
||||
private resetGroup() {
|
||||
this.filteredGroupsBehaviour = [];
|
||||
this.filteredGroupsMagistracy = [];
|
||||
this.filteredGroupsSpecialist = [];
|
||||
this.formChipGroup.reset();
|
||||
this.groupChip.value = undefined;
|
||||
}
|
||||
|
||||
public eventResult = new EventEmitter<number>();
|
||||
public selectChangeEvent = new EventEmitter<TabSelect[]>();
|
||||
|
||||
constructor(private facultyApi: FacultyService, private groupApi: GroupService) {
|
||||
this.loadFaculties();
|
||||
}
|
||||
|
||||
private getSelectedTabs(): TabSelect[] {
|
||||
const faculty = this.facultyChip.value;
|
||||
const course = this.courseChip.value;
|
||||
const group = this.groupChip.value;
|
||||
|
||||
const result: TabSelect[] = [];
|
||||
|
||||
if (faculty)
|
||||
result.push(new TabSelect(faculty, this.faculties!.find(x => x.id === faculty)?.name ?? ''));
|
||||
|
||||
if (course)
|
||||
result.push(new TabSelect(course, course.toString()));
|
||||
|
||||
if (group)
|
||||
result.push(new TabSelect(group, this.groups!.find(x => x.id == group)?.name ?? ''));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public getEnclosureList(): string[] {
|
||||
return Object.keys(Enclosure).filter((item) => {
|
||||
return isNaN(Number(item));
|
||||
});
|
||||
}
|
||||
|
||||
protected loadFaculties() {
|
||||
this.facultiesLoaded = false;
|
||||
this.facultyApi.getFaculties()
|
||||
.pipe(catchError(error => {
|
||||
this.facultiesLoaded = null;
|
||||
this.facultyIndicator.loading = false;
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(data => {
|
||||
this.faculties = data;
|
||||
this.facultiesLoaded = true;
|
||||
|
||||
let selected = TabStorageService.selected?.selected[this.getEnclosureList()[Enclosure.faculty]];
|
||||
|
||||
if (selected) {
|
||||
let selectedFaculty = data.find(x => x.id === selected.index);
|
||||
|
||||
if (selectedFaculty === undefined || selectedFaculty.name !== selected.name)
|
||||
selectedFaculty = data.find(x => x.name === selected.name);
|
||||
|
||||
if (selectedFaculty !== undefined) {
|
||||
TabStorageService.trySelectChip(selectedFaculty.id, this.facultyChip);
|
||||
this.onFacultySelected(selectedFaculty.id, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private filteringCourseNumber() {
|
||||
this.courseNumbers = Array.from(
|
||||
new Set(
|
||||
this.groups
|
||||
.filter(x => x.facultyId === this.facultyId)
|
||||
.map(x => x.courseNumber)
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private filteringGroup() {
|
||||
this.filteredGroups = this.groups.filter(x => x.facultyId === this.facultyId && x.courseNumber === this.courseNumber);
|
||||
}
|
||||
|
||||
protected loadCourseGroup() {
|
||||
if (this.groups.length === 0) {
|
||||
this.groupsLoaded = false;
|
||||
if (this.facultyId === null)
|
||||
return;
|
||||
|
||||
this.groupApi.getGroups().pipe(
|
||||
catchError(error => {
|
||||
this.groupsLoaded = null;
|
||||
if (this.groups === null || this.groups.length === 0 || this.groups[0].facultyId !== this.facultyId) {
|
||||
this.groupApi.getByFaculty(this.facultyId)
|
||||
.pipe(catchError(error => {
|
||||
this.groupIndicator.loading = false;
|
||||
this.courseIndicator.loading = false;
|
||||
throw error;
|
||||
})
|
||||
).subscribe(data => {
|
||||
this.groups = data;
|
||||
if (this.courseNumber === null)
|
||||
this.filteringCourseNumber();
|
||||
else
|
||||
this.filteringGroup();
|
||||
}))
|
||||
.subscribe(data => {
|
||||
this.groups = data;
|
||||
this.courseNumbers = Array.from(
|
||||
new Set(
|
||||
this.groups!
|
||||
.map(x => x.courseNumber)
|
||||
.sort((a, b) => a - b))
|
||||
);
|
||||
|
||||
this.groupsLoaded = true;
|
||||
});
|
||||
let selected = TabStorageService.selected?.selected[this.getEnclosureList()[Enclosure.course]];
|
||||
if (selected) {
|
||||
let selectedCourse = this.courseNumbers.find(x => x === selected.index);
|
||||
|
||||
return
|
||||
if (selectedCourse === undefined)
|
||||
selectedCourse = this.courseNumbers.find(x => x.toString() === selected.name);
|
||||
|
||||
if (selectedCourse !== undefined) {
|
||||
TabStorageService.trySelectChip(selectedCourse, this.courseChip);
|
||||
this.onCourseSelected(selectedCourse, true);
|
||||
}
|
||||
}
|
||||
|
||||
let selectedGroupStorage = TabStorageService.selected?.selected[this.getEnclosureList()[Enclosure.group]];
|
||||
if (selectedGroupStorage) {
|
||||
let selectedGroup = data.find(x => x.id === selectedGroupStorage.index);
|
||||
|
||||
if (selectedGroup === undefined || selectedGroup.name !== selectedGroupStorage.name)
|
||||
selectedGroup = data.find(x => x.name === selectedGroupStorage.name);
|
||||
|
||||
if (selectedGroup !== undefined) {
|
||||
TabStorageService.trySelectChip(selectedGroup.id, this.groupChip);
|
||||
this.onGroupSelected(selectedGroup.id, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.courseNumber === null)
|
||||
this.filteringCourseNumber();
|
||||
else
|
||||
this.filteringGroup();
|
||||
if (this.courseNumber !== null) {
|
||||
const groupByCourse = this.groups!.filter(x => x.courseNumber === this.courseNumber);
|
||||
groupByCourse.forEach(x => {
|
||||
if (x.name[2].toUpperCase() === 'Б')
|
||||
this.filteredGroupsBehaviour?.push(x);
|
||||
else if (x.name[2].toUpperCase() === 'С')
|
||||
this.filteredGroupsSpecialist?.push(x);
|
||||
else
|
||||
this.filteredGroupsMagistracy?.push(x);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected chooseFaculty(event: MatChipListboxChange) {
|
||||
this.courseNumber = null;
|
||||
this.groups = [];
|
||||
this.formChipGroup.reset();
|
||||
this.formChipCourse.reset();
|
||||
protected onFacultySelected(index: number, loadMode: boolean = false) {
|
||||
this.resetCourse();
|
||||
this.resetGroup();
|
||||
|
||||
if (event.value === undefined || event.value === null) {
|
||||
if (index === undefined) {
|
||||
this.facultyId = null;
|
||||
return;
|
||||
}
|
||||
this.facultyId = event.value;
|
||||
|
||||
if (loadMode)
|
||||
this.facultyChip.value = index;
|
||||
else
|
||||
this.selectChangeEvent.emit(this.getSelectedTabs());
|
||||
|
||||
this.facultyId = index;
|
||||
this.courseNumberPanel.open();
|
||||
|
||||
this.loadCourseGroup();
|
||||
}
|
||||
|
||||
protected chooseCourseNumber(event: MatChipListboxChange) {
|
||||
this.filteredGroups = [];
|
||||
this.formChipGroup.reset();
|
||||
protected onCourseSelected(course: number, loadMode: boolean = false) {
|
||||
this.resetGroup();
|
||||
|
||||
if (event.value === undefined || event.value === null) {
|
||||
if (course === undefined) {
|
||||
this.courseNumber = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.courseNumber = event.value;
|
||||
if (loadMode)
|
||||
this.courseChip.value = course;
|
||||
else
|
||||
this.selectChangeEvent.emit(this.getSelectedTabs());
|
||||
|
||||
this.courseNumber = course;
|
||||
this.groupPanel.open();
|
||||
this.loadCourseGroup();
|
||||
}
|
||||
|
||||
protected chooseGroup(event: MatChipListboxChange) {
|
||||
if (event.value === undefined || event.value === null)
|
||||
protected onGroupSelected(index: number, loadMode: boolean = false) {
|
||||
if (index === undefined)
|
||||
return;
|
||||
|
||||
if (loadMode)
|
||||
this.groupChip.value = index;
|
||||
|
||||
this.selectChangeEvent.emit(this.getSelectedTabs());
|
||||
|
||||
this.groupPanel.close();
|
||||
this.eventResult.emit(event.value);
|
||||
this.eventResult.emit(index);
|
||||
}
|
||||
|
||||
public load() {
|
||||
if (this.faculties === null)
|
||||
this.loadFaculties();
|
||||
}
|
||||
}
|
||||
|
12
src/components/schedule/tabs/ischedule-tab.ts
Normal file
12
src/components/schedule/tabs/ischedule-tab.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {EventEmitter} from "@angular/core";
|
||||
import {TabSelect} from "@service/tab-storage.service";
|
||||
|
||||
|
||||
export interface IScheduleTab {
|
||||
load(): void;
|
||||
|
||||
getEnclosureList(): string[];
|
||||
|
||||
eventResult: EventEmitter<number>;
|
||||
selectChangeEvent: EventEmitter<TabSelect[]>;
|
||||
}
|
@ -5,13 +5,14 @@
|
||||
Кампус
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="chooseCampus($event)">
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onCampusSelected($event.value)" #campusChip>
|
||||
@for (campus of campuses; track $index) {
|
||||
<mat-chip-option [value]="campus.id" color="accent">
|
||||
{{ campus.codeName }}
|
||||
</mat-chip-option>
|
||||
} @empty {
|
||||
<app-loading-indicator [loading]="campusesLoaded !== null" (retryFunction)="loadCampuses()"/>
|
||||
}
|
||||
@if (campuses === null) {
|
||||
<app-loading-indicator [loading]="true" (retryFunction)="loadCampuses()" #campusIndicator/>
|
||||
}
|
||||
</mat-chip-listbox>
|
||||
</mat-expansion-panel>
|
||||
@ -22,13 +23,15 @@
|
||||
Кабинет
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="chooseLectureHall($event)" [formControl]="chipLecture">
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onLectureHallSelected($event.value)"
|
||||
[formControl]="formLectureHalls" #lectureChip>
|
||||
@for (lectureHall of lectureHallsFiltered; track $index) {
|
||||
<mat-chip-option [value]="lectureHall.id" color="accent">
|
||||
{{ lectureHall.name }}
|
||||
</mat-chip-option>
|
||||
} @empty {
|
||||
<app-loading-indicator [loading]="lectureHallsLoaded !== null" (retryFunction)="loadLectureHalls()"/>
|
||||
}
|
||||
@if (lectureHallsFiltered === null) {
|
||||
<app-loading-indicator [loading]="true" (retryFunction)="loadLectureHalls()" #lectureIndicator/>
|
||||
}
|
||||
</mat-chip-listbox>
|
||||
</mat-expansion-panel>
|
||||
|
@ -1,14 +1,19 @@
|
||||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {Component, EventEmitter, ViewChild} from '@angular/core';
|
||||
import {MatAccordion, MatExpansionModule, MatExpansionPanel} from "@angular/material/expansion";
|
||||
import {MatChipListboxChange, MatChipsModule} from "@angular/material/chips";
|
||||
import {catchError, Observable, of} from "rxjs";
|
||||
import {MatChipListbox, MatChipsModule} from "@angular/material/chips";
|
||||
import {catchError} from "rxjs";
|
||||
import {FormControl, ReactiveFormsModule} from "@angular/forms";
|
||||
import {LoadingIndicatorComponent} from "@component/common/loading-indicator/loading-indicator.component";
|
||||
import {CampusBasicInfoResponse} from "@api/v1/campusBasicInfoResponse";
|
||||
import {LectureHallResponse} from "@api/v1/lectureHallResponse";
|
||||
import {CampusService} from "@api/v1/campus.service";
|
||||
import {LectureHallService} from "@api/v1/lectureHall.service";
|
||||
import {IScheduleTab} from "@component/schedule/tabs/ischedule-tab";
|
||||
import {TabSelect, TabStorageService} from "@service/tab-storage.service";
|
||||
|
||||
enum Enclosure {
|
||||
campus, lecture
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-lecture-hall',
|
||||
@ -16,7 +21,6 @@ import {LectureHallService} from "@api/v1/lectureHall.service";
|
||||
imports: [
|
||||
MatChipsModule,
|
||||
MatExpansionModule,
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
MatAccordion,
|
||||
LoadingIndicatorComponent
|
||||
@ -25,80 +29,141 @@ import {LectureHallService} from "@api/v1/lectureHall.service";
|
||||
styleUrl: './lecture-hall.component.css',
|
||||
providers: [CampusService, LectureHallService]
|
||||
})
|
||||
|
||||
export class LectureHallComponent {
|
||||
export class LectureHallComponent implements IScheduleTab {
|
||||
protected campusId: number | null = null;
|
||||
protected chipLecture: FormControl = new FormControl();
|
||||
protected formLectureHalls: FormControl = new FormControl();
|
||||
|
||||
protected campuses: CampusBasicInfoResponse[] | null = null;
|
||||
protected lectureHallsFiltered: LectureHallResponse[] | null = null;
|
||||
|
||||
@ViewChild('lecturePanel') lecturePanel!: MatExpansionPanel;
|
||||
@ViewChild('lectureIndicator') lectureIndicator!: LoadingIndicatorComponent;
|
||||
@ViewChild('campusIndicator') campusIndicator!: LoadingIndicatorComponent;
|
||||
|
||||
protected campuses: CampusBasicInfoResponse[] = [];
|
||||
protected campusesLoaded: boolean | null = false;
|
||||
@ViewChild('campusChip') campusChip!: MatChipListbox;
|
||||
@ViewChild('lectureChip') lectureChip!: MatChipListbox;
|
||||
|
||||
private lectureHalls: LectureHallResponse[] = [];
|
||||
protected lectureHallsFiltered: LectureHallResponse[] = [];
|
||||
protected lectureHallsLoaded: boolean | null = false;
|
||||
private lectureHalls: LectureHallResponse[] | null = null;
|
||||
|
||||
@Output() eventResult = new EventEmitter<number>();
|
||||
public eventResult = new EventEmitter<number>();
|
||||
public selectChangeEvent = new EventEmitter<TabSelect[]>();
|
||||
|
||||
constructor(private campusApi: CampusService, private lectureHallApi: LectureHallService) {
|
||||
this.loadCampuses();
|
||||
}
|
||||
|
||||
private getSelectedTabs(): TabSelect[] {
|
||||
const campus = this.campusChip.value;
|
||||
const lecture = this.lectureChip.value;
|
||||
|
||||
const result: TabSelect[] = [];
|
||||
|
||||
if (campus)
|
||||
result.push(new TabSelect(campus, this.campuses!.find(x => x.id === campus)?.codeName ?? ''));
|
||||
|
||||
if (lecture)
|
||||
result.push(new TabSelect(lecture, this.lectureHallsFiltered!.find(x => x.id === lecture)?.name ?? ''));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getEnclosureList(): string[] {
|
||||
return Object.keys(Enclosure).filter((item) => {
|
||||
return isNaN(Number(item));
|
||||
});
|
||||
}
|
||||
|
||||
protected loadCampuses() {
|
||||
this.campusesLoaded = false;
|
||||
this.campusApi.getCampus()
|
||||
.pipe(catchError(error => {
|
||||
this.campusesLoaded = null;
|
||||
this.campusIndicator.loading = false;
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(data => {
|
||||
this.campuses = data;
|
||||
this.campusesLoaded = true;
|
||||
|
||||
let selected = TabStorageService.selected?.selected[this.getEnclosureList()[Enclosure.campus]];
|
||||
if (selected) {
|
||||
let selectedCampus = data.find(x => x.id === selected.index);
|
||||
|
||||
if (selectedCampus === undefined || selectedCampus.codeName !== selected.name)
|
||||
selectedCampus = data.find(x => x.codeName === selected.name);
|
||||
|
||||
if (selectedCampus !== undefined) {
|
||||
TabStorageService.trySelectChip(selectedCampus.id, this.campusChip);
|
||||
this.onCampusSelected(selectedCampus.id, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private filteringLectureHalls() {
|
||||
this.lectureHallsFiltered = this.lectureHalls.filter(x => x.campusId === this.campusId);
|
||||
this.lectureHallsFiltered = this.lectureHalls?.filter(x => x.campusId === this.campusId) ?? null;
|
||||
}
|
||||
|
||||
protected chooseCampus(event: MatChipListboxChange) {
|
||||
this.chipLecture.reset();
|
||||
protected onCampusSelected(index: number, loadMode: boolean = false) {
|
||||
this.formLectureHalls.reset();
|
||||
this.lectureChip.value = undefined;
|
||||
|
||||
if (event.value === undefined || event.value === null) {
|
||||
if (loadMode)
|
||||
this.campusChip.value = index;
|
||||
else
|
||||
this.selectChangeEvent.emit(this.getSelectedTabs());
|
||||
|
||||
if (index === undefined) {
|
||||
this.campusId = null;
|
||||
this.lectureHalls = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.campusId = event.value;
|
||||
this.campusId = index;
|
||||
this.lecturePanel.open();
|
||||
|
||||
if (this.lectureHalls.length === 0)
|
||||
if (this.lectureHalls === null)
|
||||
this.loadLectureHalls();
|
||||
else
|
||||
this.filteringLectureHalls();
|
||||
}
|
||||
|
||||
protected loadLectureHalls() {
|
||||
this.lectureHallsLoaded = false;
|
||||
this.lectureHallApi.getLectureHalls()
|
||||
.pipe(catchError(error => {
|
||||
this.lectureHallsLoaded = null;
|
||||
this.lectureIndicator.loading = false;
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(data => {
|
||||
this.lectureHalls = data;
|
||||
this.filteringLectureHalls();
|
||||
this.lectureHallsLoaded = true;
|
||||
|
||||
let selected = TabStorageService.selected?.selected[this.getEnclosureList()[Enclosure.lecture]];
|
||||
if (selected) {
|
||||
let selectedLecture = data.find(x => x.id === selected.index);
|
||||
|
||||
if (selectedLecture === undefined || selectedLecture.name !== selected.name)
|
||||
selectedLecture = data.find(x => x.name === selected.name);
|
||||
|
||||
if (selectedLecture !== undefined) {
|
||||
TabStorageService.trySelectChip(selectedLecture.id, this.lectureChip);
|
||||
this.onLectureHallSelected(selectedLecture.id, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected chooseLectureHall(event: MatChipListboxChange) {
|
||||
if (event.value === undefined || event.value === null)
|
||||
protected onLectureHallSelected(index: number, loadMode: boolean = false) {
|
||||
if (index === undefined)
|
||||
return;
|
||||
|
||||
if (loadMode)
|
||||
this.lectureChip.value = index;
|
||||
else
|
||||
this.selectChangeEvent.emit(this.getSelectedTabs());
|
||||
|
||||
this.lecturePanel.close();
|
||||
this.eventResult.emit(event.value);
|
||||
this.eventResult.emit(index);
|
||||
}
|
||||
|
||||
public load() {
|
||||
if (this.campuses === null)
|
||||
this.loadCampuses();
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,27 @@
|
||||
<!--suppress CssInvalidPropertyValue -->
|
||||
<button mat-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" [id]="idButton">{{ textButton }}</button>
|
||||
<button mat-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" [id]="idButton"
|
||||
style="margin-bottom: 10px;">{{ textButton }}
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu" [hasBackdrop]="false" class="menu-options">
|
||||
<div (click)="$event.stopPropagation()" (keydown)="$event.stopPropagation()" style="padding: 0 15px 15px">
|
||||
<div class="header-menu">
|
||||
<mat-form-field appearance="outline" color="accent" style="display:flex;">
|
||||
<input matInput placeholder="Поиск..." [(ngModel)]="searchQuery" [disabled]="data.length === 0">
|
||||
<button mat-icon-button matSuffix (click)="clearSearchQuery()" [disabled]="data.length === 0">
|
||||
<input matInput placeholder="Поиск..." [(ngModel)]="searchQuery"
|
||||
[disabled]="data === null || data.length === 0">
|
||||
<button mat-icon-button matSuffix (click)="clearSearchQuery()" [disabled]="data === null || data.length === 0">
|
||||
<mat-icon style="color: var(--mdc-filled-button-label-text-color);">close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="button-group">
|
||||
<mat-checkbox (click)="checkData()" [disabled]="data.length === 0" #chooseCheckbox/>
|
||||
<button mat-button (click)="clearAll()" [disabled]="data.length === 0">Очистить</button>
|
||||
<mat-checkbox (click)="checkData()" [disabled]="data === null || data.length === 0" #chooseCheckbox/>
|
||||
<button mat-button (click)="clearAll()" [disabled]="data === null || data.length === 0">Очистить</button>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
@if (data.length === 0) {
|
||||
<app-loading-indicator style="display: flex; justify-content: center;" [loading]="dataLoaded !== null"
|
||||
@if (data === null || data.length === 0) {
|
||||
<app-loading-indicator style="display: flex; justify-content: center;" [loading]="data === null"
|
||||
(retryFunction)="retryLoadData.emit()"/>
|
||||
} @else {
|
||||
<mat-selection-list>
|
||||
|
@ -47,17 +47,19 @@ export interface SelectData {
|
||||
export class OtherComponent {
|
||||
private _searchQuery: string = '';
|
||||
protected filteredData: BehaviorSubject<SelectData[]> = new BehaviorSubject<SelectData[]>([]);
|
||||
protected data: SelectData[] = [];
|
||||
protected data: SelectData[] | null = null;
|
||||
|
||||
@Input() idButton!: string;
|
||||
@Input() textButton!: string;
|
||||
@ViewChild('menuTrigger') menuTrigger!: MatMenuTrigger;
|
||||
@ViewChild('chooseCheckbox') chooseCheckbox!: MatCheckbox;
|
||||
|
||||
@Input() dataLoaded: boolean | null = false;
|
||||
@Output() retryLoadData: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
get selectedIds(): number[] {
|
||||
if (this.data === null)
|
||||
return [];
|
||||
|
||||
return this.data.filter(x => x.selected).map(x => x.id);
|
||||
}
|
||||
|
||||
@ -72,6 +74,9 @@ export class OtherComponent {
|
||||
}
|
||||
|
||||
private updateCheckBox() {
|
||||
if (this.data === null)
|
||||
return;
|
||||
|
||||
this.chooseCheckbox.checked = this.data.every(x => x.selected);
|
||||
this.chooseCheckbox.indeterminate = this.data.some(x => x.selected) && !this.chooseCheckbox.checked;
|
||||
}
|
||||
@ -82,6 +87,9 @@ export class OtherComponent {
|
||||
}
|
||||
|
||||
protected updateFilteredData(): void {
|
||||
if (this.data === null)
|
||||
return;
|
||||
|
||||
this.filteredData.next(this.data.filter(x =>
|
||||
x.name.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
));
|
||||
@ -92,7 +100,7 @@ export class OtherComponent {
|
||||
}
|
||||
|
||||
protected clearAll(): void {
|
||||
this.data.forEach(x => x.selected = false);
|
||||
this.data?.forEach(x => x.selected = false);
|
||||
|
||||
if (this.searchQuery !== '') {
|
||||
const updatedData = this.filteredData.value.map(x => {
|
||||
@ -109,7 +117,7 @@ export class OtherComponent {
|
||||
const check: boolean = this.filteredData.value.some(x => !x.selected) && !this.filteredData.value.every(x => x.selected);
|
||||
|
||||
const updatedData = this.filteredData.value.map(data => {
|
||||
this.data.find(x => x.id === data.id)!.selected = check;
|
||||
this.data!.find(x => x.id === data.id)!.selected = check;
|
||||
return {...data, selected: check};
|
||||
});
|
||||
|
||||
@ -118,7 +126,7 @@ export class OtherComponent {
|
||||
}
|
||||
|
||||
protected checkboxStateChange(item: number) {
|
||||
const data = this.data.find(x => x.id === item)!;
|
||||
const data = this.data!.find(x => x.id === item)!;
|
||||
data.selected = !data.selected;
|
||||
const updatedData = this.filteredData.value;
|
||||
updatedData.find(x => x.id === item)!.selected = data.selected;
|
||||
|
@ -1,11 +1,12 @@
|
||||
<div class="search-content">
|
||||
@if (professors.length === 0) {
|
||||
<app-loading-indicator [loading]="professorsLoaded !== null" (retryFunction)="loadProfessors()"/>
|
||||
@if (professors === null) {
|
||||
<app-loading-indicator [loading]="true" (retryFunction)="loadProfessors()"
|
||||
#professorIndicator/>
|
||||
} @else {
|
||||
<mat-form-field color="accent" style="width: 100%;">
|
||||
<input type="text" placeholder="Поиск..." matInput [formControl]="professorControl" [matAutocomplete]="auto">
|
||||
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onOptionSelected($event)"
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onOptionSelected($event.option.value)"
|
||||
[autoActiveFirstOption]="false" [hideSingleSelectionIndicator]="true">
|
||||
@for (option of filteredProfessors | async; track option) {
|
||||
<mat-option [value]="option.id">
|
||||
|
@ -1,12 +1,14 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from "@angular/core";
|
||||
import {Component, EventEmitter, OnInit, ViewChild} from "@angular/core";
|
||||
import {MatFormField, MatInput} from "@angular/material/input";
|
||||
import {FormControl, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatAutocompleteModule, MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {catchError, map, Observable, startWith} from "rxjs";
|
||||
import {LoadingIndicatorComponent} from "@component/common/loading-indicator/loading-indicator.component";
|
||||
import {ProfessorResponse} from "@api/v1/professorResponse";
|
||||
import {ProfessorService} from "@api/v1/professor.service";
|
||||
import {IScheduleTab} from "@component/schedule/tabs/ischedule-tab";
|
||||
import {TabSelect, TabStorageService} from "@service/tab-storage.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-professor',
|
||||
@ -23,31 +25,44 @@ import {ProfessorService} from "@api/v1/professor.service";
|
||||
styleUrl: './professor.component.css',
|
||||
providers: [ProfessorService]
|
||||
})
|
||||
export class ProfessorComponent implements OnInit {
|
||||
export class ProfessorComponent implements OnInit, IScheduleTab {
|
||||
protected professorControl = new FormControl();
|
||||
protected filteredProfessors!: Observable<ProfessorResponse[]>;
|
||||
|
||||
protected professors: ProfessorResponse[] = [];
|
||||
protected professorsLoaded: boolean | null = false;
|
||||
protected professors: ProfessorResponse[] | null = null;
|
||||
|
||||
@Output() eventResult = new EventEmitter<number>();
|
||||
@ViewChild('professorIndicator') professorIndicator!: LoadingIndicatorComponent;
|
||||
|
||||
public eventResult = new EventEmitter<number>();
|
||||
public selectChangeEvent = new EventEmitter<TabSelect[]>();
|
||||
|
||||
constructor(private api: ProfessorService) {
|
||||
this.loadProfessors();
|
||||
}
|
||||
|
||||
getEnclosureList(): string[] {
|
||||
return ['professor'];
|
||||
}
|
||||
|
||||
protected loadProfessors() {
|
||||
if (this.professors.length === 0) {
|
||||
this.professorsLoaded = false;
|
||||
|
||||
if (this.professors === null || this.professors.length === 0) {
|
||||
this.api.getProfessors()
|
||||
.pipe(catchError(error => {
|
||||
this.professorsLoaded = null;
|
||||
this.professorIndicator.loading = false;
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(data => {
|
||||
this.professors = data;
|
||||
this.professorsLoaded = true;
|
||||
|
||||
let selected = TabStorageService.selected?.selected['professor'];
|
||||
if (selected) {
|
||||
let selectedProfessor = data.find(x => x.id === selected.index);
|
||||
|
||||
if (selectedProfessor === undefined || selectedProfessor.name !== selected.name)
|
||||
selectedProfessor = data.find(x => x.name === selected.name);
|
||||
|
||||
if (selectedProfessor !== undefined)
|
||||
this.onOptionSelected(selectedProfessor.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -61,20 +76,32 @@ export class ProfessorComponent implements OnInit {
|
||||
|
||||
private _filterProfessors(value: string | number): ProfessorResponse[] {
|
||||
if (typeof value === 'string') {
|
||||
if (value === '') return [];
|
||||
const filterValue = value.toLowerCase();
|
||||
return this.professors.filter(teacher => teacher.name.toLowerCase().includes(filterValue));
|
||||
if (value === '')
|
||||
return [];
|
||||
|
||||
const filterValue = value.toLowerCase().replace('ё', 'е');
|
||||
return this.professors?.filter(teacher => teacher.name.toLowerCase().replace('ё', 'е').includes(filterValue)) ?? [];
|
||||
} else {
|
||||
const selectedTeacher = this.professors.find(teacher => teacher.id === value);
|
||||
const selectedTeacher = this.professors?.find(teacher => teacher.id === value);
|
||||
return selectedTeacher ? [selectedTeacher] : [];
|
||||
}
|
||||
}
|
||||
|
||||
protected onOptionSelected(event: MatAutocompleteSelectedEvent) {
|
||||
const selectedOption = this.professors.find(teacher => teacher.id === event.option.value);
|
||||
protected onOptionSelected(index: number) {
|
||||
if (index === undefined)
|
||||
return;
|
||||
|
||||
const selectedOption = this.professors?.find(teacher => teacher.id === index);
|
||||
if (selectedOption) {
|
||||
this.professorControl.setValue(selectedOption.name);
|
||||
this.eventResult.emit(selectedOption.id);
|
||||
|
||||
this.selectChangeEvent.emit([new TabSelect(selectedOption.id, selectedOption.name)]);
|
||||
}
|
||||
}
|
||||
|
||||
public load() {
|
||||
if (this.professors === null)
|
||||
this.loadProfessors();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.padding-content div {
|
||||
padding: 30px 15px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.margin-other-button {
|
||||
|
@ -1,35 +1,32 @@
|
||||
<mat-tab-group dynamicHeight mat-stretch-tabs="false" mat-align-tabs="start" color="accent" class="padding-content"
|
||||
(selectedTabChange)="chooseTabs($event)">
|
||||
#tabGroup
|
||||
(selectedTabChange)="chooseTabs($event.index)">
|
||||
<mat-tab label="Группа">
|
||||
<div>
|
||||
<app-group (eventResult)="groupSelected($event)"/>
|
||||
<app-group #groupTab/>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Преподаватель">
|
||||
<div>
|
||||
<app-professor (eventResult)="professorSelected($event)"/>
|
||||
<app-professor #professorTab/>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Кабинет">
|
||||
<div>
|
||||
<app-lecture-hall (eventResult)="lectureHallSelected($event)"/>
|
||||
<app-lecture-hall #lectureHallTab/>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<!--
|
||||
<mat-tab label="Другое">
|
||||
|
||||
<mat-tab label="Другое" *appHasRole="AuthRoles.Admin">
|
||||
<div class="margin-other-button">
|
||||
<app-other idButton="disciplines-button" textButton="Дисциплины" #discipline [dataLoaded]="disciplinesLoaded"
|
||||
(retryLoadData)="loadDisciplines()"/>
|
||||
<app-other idButton="lecture-button" textButton="Кабинеты" #lecture
|
||||
[dataLoaded]="campusesLoaded && lectureHallsLoaded" (retryLoadData)="loadLectureHalls()"/>
|
||||
<app-other idButton="group-button" textButton="Группы" #group [dataLoaded]="facultiesLoaded && groupLoaded"
|
||||
(retryLoadData)="loadGroups()"/>
|
||||
<app-other idButton="professor-button" textButton="Профессоры" #professor [dataLoaded]="professorsLoaded"
|
||||
(retryLoadData)="professorsLoad()"/>
|
||||
<app-other idButton="disciplines-button" textButton="Дисциплины" #discipline (retryLoadData)="loadDisciplines()"/>
|
||||
<app-other idButton="lecture-button" textButton="Кабинеты" #lecture (retryLoadData)="loadLectureHalls()"/>
|
||||
<app-other idButton="group-button" textButton="Группы" #group (retryLoadData)="loadGroups()"/>
|
||||
<app-other idButton="professor-button" textButton="Профессоры" #professor (retryLoadData)="loadProfessors()"/>
|
||||
<app-other idButton="lesson-type-button" textButton="Тип занятия" #lesson_type (retryLoadData)="loadLessonType()"/>
|
||||
<section>
|
||||
<button mat-flat-button>Отфильтровать</button>
|
||||
<button mat-flat-button (click)="otherFilter()">Отфильтровать</button>
|
||||
</section>
|
||||
</div>
|
||||
</mat-tab>
|
||||
-->
|
||||
</mat-tab-group>
|
||||
|
@ -1,15 +1,25 @@
|
||||
import {Component, EventEmitter, Output} from '@angular/core';
|
||||
import {OtherComponent} from "@component/schedule/tabs/other/other.component";
|
||||
import {MatTab, MatTabChangeEvent, MatTabGroup} from "@angular/material/tabs";
|
||||
import {map, Observable} from "rxjs";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {AfterViewInit, Component, EventEmitter, Output, ViewChild} from '@angular/core';
|
||||
import {OtherComponent, SelectData} from "@component/schedule/tabs/other/other.component";
|
||||
import {MatTab, MatTabGroup} from "@angular/material/tabs";
|
||||
import {Observable} from "rxjs";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {GroupComponent} from "@component/schedule/tabs/group/group.component";
|
||||
import {ProfessorComponent} from "@component/schedule/tabs/professor/professor.component";
|
||||
import {LectureHallComponent} from "@component/schedule/tabs/lecture-hall/lecture-hall.component";
|
||||
import {ScheduleService} from "@api/v1/schedule.service";
|
||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||
import {IScheduleTab} from "@component/schedule/tabs/ischedule-tab";
|
||||
import {DisciplineService} from "@api/v1/discipline.service";
|
||||
import {LectureHallService} from "@api/v1/lectureHall.service";
|
||||
import {GroupService} from "@api/v1/group.service";
|
||||
import {ProfessorService} from "@api/v1/professor.service";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||
import {TabSelectType, TabStorageService} from "@service/tab-storage.service";
|
||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||
import {CampusService} from "@api/v1/campus.service";
|
||||
import {LessonTypeService} from "@api/v1/lessonType.service";
|
||||
|
||||
export enum TabsSelect {
|
||||
Group,
|
||||
@ -27,133 +37,207 @@ export enum TabsSelect {
|
||||
MatTab,
|
||||
ReactiveFormsModule,
|
||||
MatButton,
|
||||
DataSpinnerComponent,
|
||||
GroupComponent,
|
||||
ProfessorComponent,
|
||||
LectureHallComponent
|
||||
LectureHallComponent,
|
||||
FormsModule,
|
||||
HasRoleDirective
|
||||
],
|
||||
templateUrl: './tabs.component.html',
|
||||
styleUrl: './tabs.component.css',
|
||||
providers: [ScheduleService]
|
||||
providers: [
|
||||
ScheduleService,
|
||||
DisciplineService,
|
||||
LectureHallService,
|
||||
GroupService,
|
||||
ProfessorService,
|
||||
TabStorageService,
|
||||
CampusService,
|
||||
LessonTypeService]
|
||||
})
|
||||
|
||||
export class TabsComponent {
|
||||
@Output() eventResult = new EventEmitter<[TabsSelect, number, Observable<ScheduleResponse[]>]>();
|
||||
export class TabsComponent implements AfterViewInit {
|
||||
@Output() eventResult = new EventEmitter<[TabsSelect, number, Observable<ScheduleResponse[]>, ScheduleRequest]>();
|
||||
private currentTab: number = -1;
|
||||
|
||||
constructor(private scheduleApi: ScheduleService) {
|
||||
constructor(private scheduleApi: ScheduleService,
|
||||
private disciplineApi: DisciplineService,
|
||||
private lectureApi: LectureHallService,
|
||||
private groupApi: GroupService,
|
||||
private professorApi: ProfessorService,
|
||||
private tabStorage: TabStorageService,
|
||||
private campusApi: CampusService,
|
||||
private lessonTypeApi: LessonTypeService) {
|
||||
}
|
||||
|
||||
protected groupSelected(id: number) {
|
||||
this.eventResult.emit(
|
||||
ngAfterViewInit(): void {
|
||||
this.groupTab.selectChangeEvent.subscribe(event => this.tabStorage.select(TabSelectType.group, event));
|
||||
this.professorTab.selectChangeEvent.subscribe(event => this.tabStorage.select(TabSelectType.professor, event));
|
||||
this.lectureHallTab.selectChangeEvent.subscribe(event => this.tabStorage.select(TabSelectType.lecture, event));
|
||||
|
||||
this.groupTab.eventResult.subscribe(event => this.eventResult.emit(
|
||||
[
|
||||
TabsSelect.Group,
|
||||
id,
|
||||
this.scheduleApi.getByGroup(id)
|
||||
.pipe(
|
||||
map(g =>
|
||||
g.map(data =>
|
||||
({
|
||||
dayOfWeek: data.dayOfWeek,
|
||||
pairNumber: data.pairNumber,
|
||||
isEven: data.isEven,
|
||||
discipline: data.discipline,
|
||||
disciplineId: data.disciplineId,
|
||||
isExcludedWeeks: data.isExcludedWeeks,
|
||||
weeks: data.weeks,
|
||||
typeOfOccupations: data.typeOfOccupations,
|
||||
group: data.group,
|
||||
groupId: data.groupId,
|
||||
lectureHalls: data.lectureHalls,
|
||||
lectureHallsId: data.lectureHallsId,
|
||||
professors: data.professors,
|
||||
professorsId: data.professorsId,
|
||||
campus: data.campus,
|
||||
campusId: data.campusId,
|
||||
linkToMeet: data.linkToMeet
|
||||
}))
|
||||
)
|
||||
)
|
||||
event,
|
||||
this.scheduleApi.getByGroup(event),
|
||||
{groups: [event]}
|
||||
]
|
||||
);
|
||||
}
|
||||
));
|
||||
|
||||
protected professorSelected(id: number) {
|
||||
this.eventResult.emit(
|
||||
this.professorTab.eventResult.subscribe(event => this.eventResult.emit(
|
||||
[
|
||||
TabsSelect.Professor,
|
||||
id,
|
||||
this.scheduleApi.getByProfessor(id)
|
||||
.pipe(
|
||||
map(p =>
|
||||
p.map(data =>
|
||||
({
|
||||
dayOfWeek: data.dayOfWeek,
|
||||
pairNumber: data.pairNumber,
|
||||
isEven: data.isEven,
|
||||
discipline: data.discipline,
|
||||
disciplineId: data.disciplineId,
|
||||
isExcludedWeeks: data.isExcludedWeeks,
|
||||
weeks: data.weeks,
|
||||
typeOfOccupations: data.typeOfOccupations,
|
||||
group: data.group,
|
||||
groupId: data.groupId,
|
||||
lectureHalls: data.lectureHalls,
|
||||
lectureHallsId: data.lectureHallsId,
|
||||
professors: data.professors,
|
||||
professorsId: data.professorsId,
|
||||
campus: data.campus,
|
||||
campusId: data.campusId,
|
||||
linkToMeet: data.linkToMeet
|
||||
}))
|
||||
)
|
||||
)
|
||||
event,
|
||||
this.scheduleApi.getByProfessor(event),
|
||||
{professors: [event]}
|
||||
]
|
||||
);
|
||||
}
|
||||
));
|
||||
|
||||
protected lectureHallSelected(id: number) {
|
||||
this.eventResult.emit(
|
||||
this.lectureHallTab.eventResult.subscribe(event => this.eventResult.emit(
|
||||
[
|
||||
TabsSelect.LectureHall,
|
||||
id,
|
||||
this.scheduleApi.getByLectureHall(id)
|
||||
.pipe(
|
||||
map(lh =>
|
||||
lh.map(data =>
|
||||
({
|
||||
dayOfWeek: data.dayOfWeek,
|
||||
pairNumber: data.pairNumber,
|
||||
isEven: data.isEven,
|
||||
discipline: data.discipline,
|
||||
disciplineId: data.disciplineId,
|
||||
isExcludedWeeks: data.isExcludedWeeks,
|
||||
weeks: data.weeks,
|
||||
typeOfOccupations: data.typeOfOccupations,
|
||||
group: data.group,
|
||||
groupId: data.groupId,
|
||||
lectureHalls: data.lectureHalls,
|
||||
lectureHallsId: data.lectureHallsId,
|
||||
professors: data.professors,
|
||||
professorsId: data.professorsId,
|
||||
campus: data.campus,
|
||||
campusId: data.campusId,
|
||||
linkToMeet: data.linkToMeet
|
||||
}))
|
||||
)
|
||||
)
|
||||
event,
|
||||
this.scheduleApi.getByLectureHall(event),
|
||||
{lectureHalls: [event]}
|
||||
]
|
||||
);
|
||||
));
|
||||
|
||||
let selected = TabStorageService.selected;
|
||||
|
||||
let index = 0;
|
||||
|
||||
if (selected !== null) {
|
||||
const selectedKeys = Object.keys(selected?.selected);
|
||||
if (selected.type === null) {
|
||||
if (this.groupTab.getEnclosureList().every((value, index) => value === selectedKeys[index]))
|
||||
index = 0;
|
||||
else if (this.professorTab.getEnclosureList().every((value, index) => value === selectedKeys[index]))
|
||||
index = 1;
|
||||
else if (this.lectureHallTab.getEnclosureList().every((value, index) => value === selectedKeys[index]))
|
||||
index = 2;
|
||||
} else
|
||||
index = selected.type;
|
||||
}
|
||||
|
||||
if (index === 0)
|
||||
this.chooseTabs(0).then();
|
||||
else
|
||||
this.tabs.selectedIndex = index;
|
||||
}
|
||||
|
||||
protected async chooseTabs(event: MatTabChangeEvent) {
|
||||
switch (event.index) {
|
||||
protected async chooseTabs(index: number) {
|
||||
let needGetEnclosure = false;
|
||||
|
||||
if (this.currentTab !== index) {
|
||||
this.currentTab = index;
|
||||
needGetEnclosure = true;
|
||||
}
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
this.groupTab.load();
|
||||
if (needGetEnclosure)
|
||||
this.tabStorage.enclosure = this.groupTab.getEnclosureList();
|
||||
break;
|
||||
case 1:
|
||||
this.professorTab.load();
|
||||
if (needGetEnclosure)
|
||||
this.tabStorage.enclosure = this.professorTab.getEnclosureList();
|
||||
break;
|
||||
case 2:
|
||||
this.lectureHallTab.load();
|
||||
if (needGetEnclosure)
|
||||
this.tabStorage.enclosure = this.lectureHallTab.getEnclosureList();
|
||||
break;
|
||||
case 3:
|
||||
await this.loadDisciplines();
|
||||
await this.loadLectureHalls();
|
||||
await this.loadGroups();
|
||||
await this.loadProfessors();
|
||||
await this.loadLessonType();
|
||||
break;
|
||||
default:
|
||||
await this.chooseTabs(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
protected async loadDisciplines() {
|
||||
this.disciplineApi.getDisciplines().subscribe(data => {
|
||||
this.disciplineEx.Data = data.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name
|
||||
}) as SelectData);
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadLectureHalls() {
|
||||
this.campusApi.getCampus().subscribe(campus => {
|
||||
this.lectureApi.getLectureHalls().subscribe(data => {
|
||||
this.lectureHallEx.Data = data.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name + ` (${campus.find(c => c.id == x.campusId)?.codeName})`
|
||||
}) as SelectData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadGroups() {
|
||||
this.groupApi.getGroups().subscribe(data => {
|
||||
this.groupEx.Data = data.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name
|
||||
}) as SelectData);
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadProfessors() {
|
||||
this.professorApi.getProfessors().subscribe(data => {
|
||||
this.professorEx.Data = data.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name
|
||||
}) as SelectData);
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadLessonType() {
|
||||
this.lessonTypeApi.getLessonTypes().subscribe(data => {
|
||||
this.lessonTypeEx.Data = data.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name
|
||||
}) as SelectData);
|
||||
});
|
||||
}
|
||||
|
||||
@ViewChild('groupTab') groupTab!: IScheduleTab;
|
||||
@ViewChild('professorTab') professorTab!: IScheduleTab;
|
||||
@ViewChild('lectureHallTab') lectureHallTab!: IScheduleTab;
|
||||
|
||||
@ViewChild('discipline') disciplineEx!: OtherComponent;
|
||||
@ViewChild('lecture') lectureHallEx!: OtherComponent;
|
||||
@ViewChild('group') groupEx!: OtherComponent;
|
||||
@ViewChild('professor') professorEx!: OtherComponent;
|
||||
*/
|
||||
@ViewChild('lesson_type') lessonTypeEx!: OtherComponent;
|
||||
|
||||
@ViewChild('tabGroup') tabs!: MatTabGroup;
|
||||
protected readonly AuthRoles = AuthRoles;
|
||||
|
||||
protected otherFilter() {
|
||||
const data: ScheduleRequest = ({
|
||||
groups: this.groupEx.selectedIds,
|
||||
disciplines: this.disciplineEx.selectedIds,
|
||||
professors: this.professorEx.selectedIds,
|
||||
lectureHalls: this.lectureHallEx.selectedIds,
|
||||
lessonType: this.lessonTypeEx.selectedIds
|
||||
});
|
||||
|
||||
this.eventResult.emit(
|
||||
[
|
||||
TabsSelect.Other,
|
||||
0,
|
||||
this.scheduleApi.postSchedule(data),
|
||||
data
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
35
src/directives/has-role.directive.ts
Normal file
35
src/directives/has-role.directive.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
import AuthApiService from "@api/v1/authApi.service";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {catchError, of} from "rxjs";
|
||||
|
||||
@Directive({
|
||||
selector: '[appHasRole]',
|
||||
standalone: true,
|
||||
providers: [AuthApiService]
|
||||
})
|
||||
export class HasRoleDirective {
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private authService: AuthApiService
|
||||
) {
|
||||
}
|
||||
|
||||
@Input() set appHasRole(role: AuthRoles) {
|
||||
this.viewContainer.clear();
|
||||
|
||||
this.authService
|
||||
.getRole()
|
||||
.pipe(catchError(_ => {
|
||||
this.viewContainer.clear();
|
||||
return of(null);
|
||||
}))
|
||||
.subscribe(data => {
|
||||
if (data === role)
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
else
|
||||
this.viewContainer.clear();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export const environment = {
|
||||
apiUrl: 'http://localhost:5269/api/',
|
||||
apiUrl: 'http://localhost:8080/api/',
|
||||
maxRetry: 5,
|
||||
retryDelay: 1500
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
export const environment = {
|
||||
apiUrl: 'https://mirea.winsomnia.net/api',
|
||||
apiUrl: 'https://mirea.winsomnia.net/api/',
|
||||
maxRetry: 3,
|
||||
retryDelay: 1500
|
||||
};
|
||||
|
@ -6,10 +6,14 @@
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
import {appConfig} from './app/app.config';
|
||||
import {AppComponent} from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
|
28
src/pages/admin/admin.component.css
Normal file
28
src/pages/admin/admin.component.css
Normal file
@ -0,0 +1,28 @@
|
||||
mat-sidenav-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
mat-sidenav {
|
||||
width: auto;
|
||||
min-width: 200px;
|
||||
max-width: 20vw;
|
||||
}
|
||||
|
||||
mat-nav-list a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
mat-icon {
|
||||
margin-right: 16px;
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.active-link {
|
||||
backdrop-filter: contrast(75%);
|
||||
}
|
21
src/pages/admin/admin.component.html
Normal file
21
src/pages/admin/admin.component.html
Normal file
@ -0,0 +1,21 @@
|
||||
<mat-card *appHasRole="AuthRoles.Admin">
|
||||
<mat-sidenav-container>
|
||||
<mat-sidenav mode="side" opened>
|
||||
<mat-nav-list>
|
||||
@for (link of navLinks; track $index) {
|
||||
<a
|
||||
mat-list-item
|
||||
[class.active-link]="isActive(link.route)"
|
||||
(click)="navigate(link.route)"
|
||||
[disabled]="isActive(link.route)">
|
||||
<mat-icon>{{ link.icon }}</mat-icon>
|
||||
<span>{{ link.label }}</span>
|
||||
</a>
|
||||
}
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</mat-card>
|
59
src/pages/admin/admin.component.ts
Normal file
59
src/pages/admin/admin.component.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatCard} from "@angular/material/card";
|
||||
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||
import {Router, RouterOutlet} from "@angular/router";
|
||||
import AuthApiService from "@api/v1/authApi.service";
|
||||
import {MatListItem, MatNavList} from "@angular/material/list";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCard,
|
||||
HasRoleDirective,
|
||||
MatNavList,
|
||||
MatSidenavModule,
|
||||
RouterOutlet,
|
||||
MatListItem,
|
||||
MatIcon,
|
||||
],
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrl: './admin.component.css',
|
||||
providers: [AuthApiService]
|
||||
})
|
||||
export class AdminComponent {
|
||||
navLinks = [
|
||||
{label: 'Расписание', route: 'schedule', icon: 'calendar_month'},
|
||||
{label: 'Институт', route: 'institute', icon: 'school'},
|
||||
{label: 'Аккаунт', route: 'account', icon: 'person'},
|
||||
{label: 'Сервер', route: 'server', icon: 'settings'},
|
||||
];
|
||||
|
||||
constructor(private auth: AuthApiService, private router: Router) {
|
||||
this.auth.getRole()
|
||||
.subscribe(data => {
|
||||
if (data === null)
|
||||
router.navigate(['login']).then();
|
||||
});
|
||||
}
|
||||
|
||||
isActive(route: string): boolean {
|
||||
return this.router.isActive(`/admin/${route}`, {
|
||||
paths: 'exact',
|
||||
queryParams: 'ignored',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored',
|
||||
});
|
||||
}
|
||||
|
||||
navigate(route: string): void {
|
||||
if (!this.isActive(route)) {
|
||||
this.router.navigate([`/admin/${route}`]).then();
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly AuthRoles = AuthRoles;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/* Основной контейнер */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
flex: 1 1 calc(50% - 16px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.container > :first-child:nth-last-child(1),
|
||||
.container > :first-child:nth-last-child(1) ~ * {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.container > * {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<h2 style="margin: 15px;">Конфигурация расписания</h2>
|
||||
|
||||
<div class="container">
|
||||
<app-term-start-date></app-term-start-date>
|
||||
<app-schedule-file-upload></app-schedule-file-upload>
|
||||
<app-cron-update-schedule></app-cron-update-schedule>
|
||||
<app-skip-update-schedule></app-skip-update-schedule>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {CronUpdateScheduleComponent} from "@component/admin/cron-update-schedule/cron-update-schedule.component";
|
||||
import {SkipUpdateScheduleComponent} from "@component/admin/skip-update-schedule/skip-update-schedule.component";
|
||||
import {TermStartDateComponent} from "@component/admin/term-start-date/term-start-date.component";
|
||||
import {ScheduleFileUploadComponent} from "@component/admin/schedule-file-upload/schedule-file-upload.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-configuration',
|
||||
imports: [
|
||||
CronUpdateScheduleComponent,
|
||||
SkipUpdateScheduleComponent,
|
||||
TermStartDateComponent,
|
||||
ScheduleFileUploadComponent
|
||||
],
|
||||
templateUrl: './schedule-configuration.component.html',
|
||||
styleUrl: './schedule-configuration.component.css'
|
||||
})
|
||||
export class ScheduleConfigurationComponent {
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
.under-construction {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.under-construction h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.under-construction p {
|
||||
font-size: 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div class="under-construction">
|
||||
<h1>Страница находится в разработке</h1>
|
||||
<p>Пожалуйста, зайдите позже.</p>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-under-construction',
|
||||
imports: [],
|
||||
templateUrl: './under-construction.component.html',
|
||||
styleUrl: './under-construction.component.css'
|
||||
})
|
||||
export class UnderConstructionComponent {
|
||||
|
||||
}
|
26
src/pages/login/login.component.css
Normal file
26
src/pages/login/login.component.css
Normal file
@ -0,0 +1,26 @@
|
||||
.formLogin {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 40vh;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.formLogin mat-card {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.formLogin p {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.formLogin form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.formLoginButton {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
69
src/pages/login/login.component.html
Normal file
69
src/pages/login/login.component.html
Normal file
@ -0,0 +1,69 @@
|
||||
<mat-sidenav-container class="formLogin">
|
||||
<mat-card>
|
||||
<p class="mat-h3">
|
||||
Вход в систему
|
||||
</p>
|
||||
|
||||
<form [formGroup]="loginForm">
|
||||
@if (!requiresTwoFactorAuth) {
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Имя пользователя/email</mat-label>
|
||||
<input matInput
|
||||
formControlName="user"
|
||||
matTooltip='Укажите имя пользователя используя латинские буквы и цифры без пробелов или email'
|
||||
required
|
||||
focusNext="passwordNextFocus">
|
||||
|
||||
@if (loginForm.get('user')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Имя пользователя или email является <i>обязательным</i>
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@if (loginForm.get('user')?.hasError('minlength')) {
|
||||
<mat-error>
|
||||
Количество символов должно быть не менее 4
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<password-input [focusNext]="'loginNextFocus'" [formGroup]="loginForm"/>
|
||||
} @else {
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Код 2FA</mat-label>
|
||||
<input matInput
|
||||
formControlName="twoFactorCode"
|
||||
matTooltip="Введите код из приложения"
|
||||
required
|
||||
focusNext="loginNextFocus">
|
||||
@if (loginForm.get('twoFactorCode')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Код 2FA обязателен.
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
</form>
|
||||
|
||||
@if (!requiresTwoFactorAuth) {
|
||||
<OAuthProviders (oAuthLoginResult)="loginOAuth($event)"/>
|
||||
}
|
||||
|
||||
<mat-error>
|
||||
{{ errorText }}
|
||||
</mat-error>
|
||||
|
||||
<div class="formLoginButton">
|
||||
@if (loaderActive) {
|
||||
<app-data-spinner [scale]="40"/>
|
||||
} @else {
|
||||
<button mat-flat-button color="accent"
|
||||
[disabled]="loginButtonIsDisable"
|
||||
(click)="requiresTwoFactorAuth ? login2Fa() : login()"
|
||||
id="loginNextFocus">
|
||||
Войти
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</mat-card>
|
||||
</mat-sidenav-container>
|
132
src/pages/login/login.component.ts
Normal file
132
src/pages/login/login.component.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatSidenavContainer} from "@angular/material/sidenav";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatCard} from "@angular/material/card";
|
||||
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import AuthApiService from "@api/v1/authApi.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {catchError} from "rxjs";
|
||||
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||
import {PasswordInputComponent} from "@component/common/password-input/password-input.component";
|
||||
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatSidenavContainer,
|
||||
MatFormFieldModule,
|
||||
MatInput,
|
||||
MatTooltip,
|
||||
MatButton,
|
||||
MatCard,
|
||||
ReactiveFormsModule,
|
||||
FocusNextDirective,
|
||||
DataSpinnerComponent,
|
||||
PasswordInputComponent,
|
||||
OAuthProviders
|
||||
],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.css',
|
||||
providers: [AuthApiService]
|
||||
})
|
||||
export class LoginComponent {
|
||||
protected loginForm!: FormGroup;
|
||||
protected loaderActive: boolean = false;
|
||||
protected loginButtonIsDisable: boolean = true;
|
||||
protected errorText: string = '';
|
||||
protected requiresTwoFactorAuth: boolean = false;
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private auth: AuthApiService, private router: Router) {
|
||||
this.auth.getRole()
|
||||
.subscribe(data => {
|
||||
if (data !== null)
|
||||
router.navigate(['admin']).then();
|
||||
});
|
||||
|
||||
this.loginForm = this.formBuilder.group({
|
||||
user: ['',],
|
||||
password: ['',]
|
||||
}
|
||||
);
|
||||
|
||||
this.loginForm.get('password')?.setValidators([
|
||||
Validators.required,
|
||||
Validators.minLength(8)
|
||||
]);
|
||||
|
||||
this.loginForm.get('user')?.setValidators([
|
||||
Validators.required,
|
||||
Validators.minLength(4)
|
||||
]);
|
||||
|
||||
this.loginForm.valueChanges.subscribe(() => {
|
||||
this.loginButtonIsDisable = !this.loginForm.valid;
|
||||
});
|
||||
}
|
||||
|
||||
private updateTwoFactorValidation(data: TwoFactorAuthentication) {
|
||||
if (data === TwoFactorAuthentication.None) {
|
||||
this.router.navigate(['admin']).then();
|
||||
return;
|
||||
}
|
||||
|
||||
this.requiresTwoFactorAuth = true;
|
||||
this.loginForm.addControl(
|
||||
'twoFactorCode',
|
||||
new FormControl('', Validators.required)
|
||||
);
|
||||
this.loginForm.removeControl('user');
|
||||
this.loginForm.removeControl('password');
|
||||
this.loginButtonIsDisable = !this.loginForm.valid;
|
||||
}
|
||||
|
||||
protected login() {
|
||||
this.loaderActive = true;
|
||||
|
||||
this.auth.login({
|
||||
username: this.loginForm.get('user')?.value,
|
||||
password: this.loginForm.get('password')?.value
|
||||
})
|
||||
.pipe(catchError(error => {
|
||||
this.loaderActive = false;
|
||||
this.errorText = error.error.detail;
|
||||
this.loginButtonIsDisable = true;
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(x => {
|
||||
this.loaderActive = false;
|
||||
this.errorText = '';
|
||||
this.updateTwoFactorValidation(x);
|
||||
});
|
||||
}
|
||||
|
||||
protected login2Fa() {
|
||||
this.loaderActive = true;
|
||||
|
||||
this.auth.twoFactorAuth({
|
||||
code: this.loginForm.get('twoFactorCode')?.value,
|
||||
method: TwoFactorAuthentication.TotpRequired
|
||||
})
|
||||
.pipe(catchError(error => {
|
||||
this.loaderActive = false;
|
||||
this.errorText = error.error.detail;
|
||||
this.loginButtonIsDisable = true;
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(_ => {
|
||||
this.loaderActive = false;
|
||||
this.errorText = '';
|
||||
this.router.navigate(['admin']).then();
|
||||
});
|
||||
}
|
||||
|
||||
protected loginOAuth(result: TwoFactorAuthentication) {
|
||||
this.updateTwoFactorValidation(result);
|
||||
}
|
||||
}
|
8
src/pages/schedule/confirm-dialog.component.html
Normal file
8
src/pages/schedule/confirm-dialog.component.html
Normal file
@ -0,0 +1,8 @@
|
||||
<h1 mat-dialog-title>Подтверждение</h1>
|
||||
<div mat-dialog-content>
|
||||
<p>Вы уверены, что хотите запросить Excel с выбранными данными?</p>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button color="accent" (click)="onConfirm()">Запросить</button>
|
||||
<button mat-button (click)="onCancel()">Отмена</button>
|
||||
</div>
|
28
src/pages/schedule/confirm-dialog.component.ts
Normal file
28
src/pages/schedule/confirm-dialog.component.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog';
|
||||
import {MatButton} from "@angular/material/button";
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog',
|
||||
templateUrl: './confirm-dialog.component.html',
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatButton
|
||||
],
|
||||
standalone: true
|
||||
})
|
||||
export class ConfirmDialogComponent {
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
|
||||
}
|
||||
|
||||
protected onConfirm(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
protected onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
.schedule {
|
||||
padding: 50px 15%;
|
||||
min-height: 60vh;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.schedule mat-sidenav-content {
|
||||
overflow: inherit;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
|
@ -1,5 +1,26 @@
|
||||
<mat-sidenav-container class="schedule">
|
||||
<app-tabs (eventResult)="result($event)"/>
|
||||
<app-table-header [startWeek]="startWeek" [currentWeek]="currentWeek" (weekEvent)="handleWeekEvent($event)" #tableHeader/>
|
||||
<app-table [currentWeek]="currentWeek" [startWeek]="startWeek" [data]="data" [isLoad]="isLoadTable"/>
|
||||
<mat-sidenav-content>
|
||||
<app-tabs (eventResult)="result($event)"/>
|
||||
</mat-sidenav-content>
|
||||
|
||||
<mat-sidenav-content>
|
||||
<app-table-header [startWeek]="startWeek" [currentWeek]="currentWeek" (weekEvent)="handleWeekEvent($event)"
|
||||
#tableHeader/>
|
||||
</mat-sidenav-content>
|
||||
|
||||
<mat-sidenav-content>
|
||||
<app-table [currentWeek]="currentWeek" [startWeek]="startWeek" [data]="data" [isLoad]="isLoadTable"
|
||||
[disciplineWithWeeks]="disciplineWithWeeks"/>
|
||||
</mat-sidenav-content>
|
||||
|
||||
<mat-sidenav-content style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в
|
||||
дисциплине
|
||||
</mat-checkbox>
|
||||
@if (excelImportLoader) {
|
||||
<app-data-spinner/>
|
||||
} @else {
|
||||
<button mat-button (click)="openDialog()" *appHasRole="AuthRoles.Admin">Импортировать расписание (.xlsx)</button>
|
||||
}
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
@ -1,68 +1,101 @@
|
||||
import {Component, LOCALE_ID, ViewChild} from '@angular/core';
|
||||
import {TableComponent} from "@component/schedule/table/table.component";
|
||||
import {MatFormField, MatInput} from "@angular/material/input";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {AdditionalText, TableHeaderComponent} from "@component/schedule/table-header/table-header.component";
|
||||
import {addDays, weekInYear} from "@progress/kendo-date-math";
|
||||
import {MatCard} from "@angular/material/card";
|
||||
import {MatSidenavContainer} from "@angular/material/sidenav";
|
||||
import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component";
|
||||
import {catchError, Observable} from "rxjs";
|
||||
import {ScheduleService} from "@api/v1/schedule.service";
|
||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||
import {PeriodTimes} from "@model/pairPeriodTime";
|
||||
import {PairPeriodTime} from "@model/pairPeriodTime";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {TabStorageService} from "@service/tab-storage.service";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {ConfirmDialogComponent} from "@page/schedule/confirm-dialog.component";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {ImportService} from "@api/v1/import.service";
|
||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||
import {TableComponent} from "@component/schedule/table/table.component";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TableComponent,
|
||||
MatInput,
|
||||
MatFormField,
|
||||
MatButton,
|
||||
FormsModule,
|
||||
MatSidenavModule,
|
||||
TabsComponent,
|
||||
TableHeaderComponent,
|
||||
MatCard,
|
||||
MatSidenavContainer,
|
||||
TabsComponent
|
||||
TableComponent,
|
||||
MatCheckbox,
|
||||
DataSpinnerComponent,
|
||||
MatButton,
|
||||
HasRoleDirective
|
||||
],
|
||||
templateUrl: './schedule.component.html',
|
||||
styleUrl: './schedule.component.css',
|
||||
providers: [
|
||||
ScheduleService,
|
||||
{provide: LOCALE_ID, useValue: 'ru-RU'}
|
||||
ImportService
|
||||
]
|
||||
})
|
||||
|
||||
export class ScheduleComponent {
|
||||
protected startWeek!: Date;
|
||||
private lastRequest: ScheduleRequest | null = null;
|
||||
|
||||
protected startWeek: Date;
|
||||
protected data: ScheduleResponse[] = [];
|
||||
protected startTerm: Date;
|
||||
protected isLoadTable: boolean = false;
|
||||
protected pairPeriods: PeriodTimes = {};
|
||||
protected pairPeriods: PairPeriodTime | null = null;
|
||||
protected disciplineWithWeeks: boolean = false;
|
||||
protected excelImportLoader: boolean = false;
|
||||
|
||||
@ViewChild('tableHeader') childComponent!: TableHeaderComponent;
|
||||
|
||||
constructor(api: ScheduleService) {
|
||||
this.calculateCurrentWeek();
|
||||
constructor(api: ScheduleService,
|
||||
route: ActivatedRoute,
|
||||
private importApi: ImportService,
|
||||
private notify: ToastrService,
|
||||
public dialog: MatDialog) {
|
||||
route.queryParams.subscribe(params => {
|
||||
TabStorageService.selectDataFromQuery(params);
|
||||
});
|
||||
|
||||
this.startTerm = new Date(1, 1, 1);
|
||||
this.startWeek = new Date(1, 1, 1);
|
||||
|
||||
let disciplineWithWeeksStorage = localStorage.getItem('disciplineWithWeeks');
|
||||
|
||||
if (disciplineWithWeeksStorage)
|
||||
this.disciplineWithWeeks = disciplineWithWeeksStorage.toLowerCase() === 'true';
|
||||
|
||||
api.pairPeriod().subscribe(date => {
|
||||
this.pairPeriods = date;
|
||||
});
|
||||
api.startTerm().subscribe(date => {
|
||||
this.startTerm = date.date;
|
||||
this.calculateCurrentWeek();
|
||||
});
|
||||
}
|
||||
|
||||
protected result(data: [TabsSelect, number, Observable<ScheduleResponse[]>]) {
|
||||
protected result(data: [TabsSelect, number, Observable<ScheduleResponse[]>, ScheduleRequest]) {
|
||||
this.isLoadTable = true;
|
||||
this.lastRequest = data[3];
|
||||
|
||||
data[2]
|
||||
.pipe(catchError(error => {
|
||||
this.data = [];
|
||||
throw error;
|
||||
}))
|
||||
.subscribe(x => {
|
||||
if (x == undefined || x.length === 0) {
|
||||
this.isLoadTable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = x;
|
||||
switch (data[0]) {
|
||||
case TabsSelect.Group:
|
||||
@ -70,8 +103,6 @@ export class ScheduleComponent {
|
||||
break;
|
||||
case TabsSelect.Professor:
|
||||
let indexProfessor = this.data[0].professorsId.findIndex(p => p === data[1]);
|
||||
console.log(indexProfessor);
|
||||
console.log(data[1]);
|
||||
this.childComponent.AdditionalText(AdditionalText.Professor, this.data[0].professors[indexProfessor]);
|
||||
break;
|
||||
case TabsSelect.LectureHall:
|
||||
@ -89,6 +120,9 @@ export class ScheduleComponent {
|
||||
private calculateCurrentWeek() {
|
||||
let currentDate = new Date();
|
||||
|
||||
if (currentDate.getDate() < this.startTerm.getDate())
|
||||
currentDate = this.startTerm;
|
||||
|
||||
function startOfWeek(date: Date) {
|
||||
return addDays(date, -date.getDay() + 1);
|
||||
}
|
||||
@ -99,10 +133,10 @@ export class ScheduleComponent {
|
||||
this.startWeek = this.startTerm;
|
||||
}
|
||||
|
||||
protected handleWeekEvent(eventData: boolean | null) {
|
||||
if (eventData === null) {
|
||||
protected handleWeekEvent(forward: boolean | null) {
|
||||
if (forward === null) {
|
||||
this.calculateCurrentWeek();
|
||||
} else if (eventData) {
|
||||
} else if (forward) {
|
||||
this.startWeek = addDays(this.startWeek, 7);
|
||||
} else {
|
||||
this.startWeek = addDays(this.startWeek, -7);
|
||||
@ -110,6 +144,56 @@ export class ScheduleComponent {
|
||||
}
|
||||
|
||||
get currentWeek(): number {
|
||||
return (weekInYear(this.startWeek) - weekInYear(this.startTerm)) + 1;
|
||||
const startTermWeek = weekInYear(this.startTerm);
|
||||
let startWeekNumber;
|
||||
const startWeek = addDays(this.startWeek, 6);
|
||||
|
||||
if (startWeek.getFullYear() > this.startTerm.getFullYear())
|
||||
startWeekNumber = weekInYear(new Date(this.startTerm.getFullYear(), 11, 29)) + weekInYear(startWeek);
|
||||
else
|
||||
startWeekNumber = weekInYear(startWeek);
|
||||
|
||||
let result = startWeekNumber - startTermWeek + 1;
|
||||
|
||||
if (result <= 0)
|
||||
result = 1;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected changeDisciplineWeeksView(checked: boolean) {
|
||||
localStorage.setItem('disciplineWithWeeks', checked.toString());
|
||||
this.disciplineWithWeeks = checked;
|
||||
}
|
||||
|
||||
protected openDialog() {
|
||||
if (this.lastRequest == null) {
|
||||
this.notify.error("Запрос на импорт невозможен, поскольку данные таблицы не были выбраны", "Ошибка импорта");
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result && this.lastRequest != null) {
|
||||
this.excelImportLoader = true;
|
||||
this.importApi.importToExcel(this.lastRequest).subscribe({
|
||||
next: (blob: Blob) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'schedule.xlsx';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
this.excelImportLoader = false;
|
||||
},
|
||||
error: _ => {
|
||||
this.excelImportLoader = false;
|
||||
this.notify.error("Не удалось импортировать файл Excel");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly AuthRoles = AuthRoles;
|
||||
}
|
||||
|
13
src/pages/setup/cache/cache.component.html
vendored
13
src/pages/setup/cache/cache.component.html
vendored
@ -15,7 +15,7 @@
|
||||
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>База данных</mat-label>
|
||||
<mat-select (valueChange)="onDatabaseChange($event)">
|
||||
<mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
|
||||
<mat-option value="redis">Redis</mat-option>
|
||||
<mat-option value="memcached">Memcached</mat-option>
|
||||
</mat-select>
|
||||
@ -29,7 +29,8 @@
|
||||
<input matInput
|
||||
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
||||
required
|
||||
formControlName="server">
|
||||
formControlName="server"
|
||||
focusNext="serverNextFocus">
|
||||
|
||||
@if (databaseForm.get('server')?.hasError('required')) {
|
||||
<mat-error>
|
||||
@ -49,7 +50,9 @@
|
||||
<input matInput
|
||||
matTooltip="Укажите порт сервера"
|
||||
required
|
||||
formControlName="port">
|
||||
formControlName="port"
|
||||
id="serverNextFocus"
|
||||
focusNext="passwordNextFocus">
|
||||
|
||||
@if (databaseForm.get('port')?.hasError('required')) {
|
||||
<mat-error>
|
||||
@ -69,7 +72,9 @@
|
||||
<input matInput
|
||||
matTooltip="Укажите пароль"
|
||||
formControlName="password"
|
||||
[type]="hidePass ? 'password' : 'text'">
|
||||
[type]="hidePass ? 'password' : 'text'"
|
||||
id="passwordNextFocus"
|
||||
focusNext="nextButtonFocus">
|
||||
|
||||
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||
[attr.aria-pressed]="hidePass">
|
||||
|
35
src/pages/setup/cache/cache.component.ts
vendored
35
src/pages/setup/cache/cache.component.ts
vendored
@ -8,6 +8,9 @@ import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatIconButton} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {of} from "rxjs";
|
||||
import {CacheType} from "@model/cacheType";
|
||||
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||
|
||||
@Component({
|
||||
selector: 'app-cache',
|
||||
@ -19,7 +22,8 @@ import {MatIcon} from "@angular/material/icon";
|
||||
MatInput,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
FocusNextDirective
|
||||
],
|
||||
templateUrl: './cache.component.html'
|
||||
})
|
||||
@ -40,6 +44,35 @@ export class CacheComponent {
|
||||
this.databaseForm.valueChanges.subscribe(() => {
|
||||
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
||||
});
|
||||
|
||||
this.api.cacheConfiguration().subscribe(response => {
|
||||
if (!response)
|
||||
return;
|
||||
|
||||
this.navigationService.setSkipButtonState(true);
|
||||
this.navigationService.skipButtonAction = () => of(true);
|
||||
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
|
||||
|
||||
|
||||
this.databaseForm.patchValue({
|
||||
server: response.server,
|
||||
port: response.port,
|
||||
password: response.password,
|
||||
});
|
||||
|
||||
let type: string;
|
||||
|
||||
switch (response.type) {
|
||||
case CacheType.Redis:
|
||||
type = "redis";
|
||||
break;
|
||||
case CacheType.Memcached:
|
||||
type = "memcached";
|
||||
break;
|
||||
}
|
||||
this.database = type;
|
||||
this.onDatabaseChange(type);
|
||||
});
|
||||
}
|
||||
|
||||
onDatabaseChange(selectedDatabase: string) {
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
@if (createAdminForm.get('user')?.hasError('pattern')) {
|
||||
<mat-error>
|
||||
Имя пользователя должен содержать латинские сиволы и цифры и быть не менее 4 символов
|
||||
Имя пользователя должен содержать латинские символы и цифры и быть не менее 4 символов
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@ -53,37 +53,7 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field color="accent" style="margin-bottom: 20px">
|
||||
<mat-label>Пароль</mat-label>
|
||||
<input matInput
|
||||
matTooltip="Укажите пароль"
|
||||
formControlName="password"
|
||||
required
|
||||
[type]="hidePass ? 'password' : 'text'">
|
||||
|
||||
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||
[attr.aria-pressed]="hidePass">
|
||||
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
@if (createAdminForm.get('password')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Пароль является <i>обязательным</i>
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@if (createAdminForm.get('password')?.hasError('minlength')) {
|
||||
<mat-error>
|
||||
Пароль должен быть не менее 8 символов
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@if (createAdminForm.get('password')?.hasError('pattern')) {
|
||||
<mat-error>
|
||||
Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!@#$%^&*)
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<password-input [formGroup]="createAdminForm" [isSetupMode]="true"/>
|
||||
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Повторите пароль</mat-label>
|
||||
@ -105,5 +75,10 @@
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
|
||||
(oAuthUpdateProviders)="updateProviders()"
|
||||
[message]="'Или можете получить часть данных от сторонних сервисов'"
|
||||
[action]="OAuthAction.Bind" [isSetup]="true"/>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {NavigationService} from "@service/navigation.service";
|
||||
import {passwordMatchValidator} from '@service/password-match.validator';
|
||||
@ -9,6 +10,12 @@ import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatIconButton} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import AuthApiService from "@api/v1/authApi.service";
|
||||
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
import {PasswordInputComponent} from "@component/common/password-input/password-input.component";
|
||||
import {OAuthAction} from "@model/oAuthAction";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-admin',
|
||||
@ -20,18 +27,22 @@ import {MatIcon} from "@angular/material/icon";
|
||||
MatInput,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
OAuthProviders,
|
||||
PasswordInputComponent
|
||||
],
|
||||
templateUrl: './create-admin.component.html'
|
||||
templateUrl: './create-admin.component.html',
|
||||
providers: [AuthApiService, Location]
|
||||
})
|
||||
|
||||
export class CreateAdminComponent {
|
||||
protected createAdminForm!: FormGroup;
|
||||
protected hidePass = true;
|
||||
protected hideRetypePass = true;
|
||||
protected activatedProviders: OAuthProvider[] = [];
|
||||
|
||||
constructor(
|
||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||
constructor(private router: Router,
|
||||
private location: Location,
|
||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||
this.createAdminForm = this.formBuilder.group({
|
||||
user: ['', Validators.pattern(/^([A-Za-z0-9]){4,}$/)],
|
||||
email: ['', Validators.email],
|
||||
@ -41,12 +52,6 @@ export class CreateAdminComponent {
|
||||
{validators: passwordMatchValidator('password', 'retype')}
|
||||
);
|
||||
|
||||
this.createAdminForm.get('password')?.setValidators([Validators.required,
|
||||
Validators.pattern(/[A-Z]/),
|
||||
Validators.pattern(/[!@#$%^&*]/),
|
||||
Validators.minLength(8)
|
||||
]);
|
||||
|
||||
this.navigationService.setNextButtonState(false);
|
||||
this.createAdminForm.valueChanges.subscribe(() => {
|
||||
this.navigationService.setNextButtonState(this.createAdminForm.valid);
|
||||
@ -60,15 +65,35 @@ export class CreateAdminComponent {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.updateAdminData();
|
||||
}
|
||||
|
||||
protected togglePassword(event: MouseEvent) {
|
||||
this.hidePass = !this.hidePass;
|
||||
event.stopPropagation();
|
||||
private updateAdminData() {
|
||||
this.api.adminConfiguration().subscribe(configuration => {
|
||||
if (configuration) {
|
||||
if (this.createAdminForm.get('email')?.value == 0)
|
||||
this.createAdminForm.get('email')?.setValue(configuration.email);
|
||||
|
||||
if (this.createAdminForm.get('user')?.value == 0)
|
||||
this.createAdminForm.get('user')?.setValue(configuration.username);
|
||||
|
||||
this.activatedProviders = configuration.usedOAuthProviders;
|
||||
}
|
||||
|
||||
const currentPath = this.router.url.split('?')[0];
|
||||
this.location.replaceState(currentPath);
|
||||
});
|
||||
}
|
||||
|
||||
protected toggleRetypePassword(event: MouseEvent) {
|
||||
this.hideRetypePass = !this.hideRetypePass;
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
protected updateProviders() {
|
||||
this.updateAdminData();
|
||||
}
|
||||
|
||||
protected readonly OAuthAction = OAuthAction;
|
||||
}
|
||||
|
@ -17,9 +17,9 @@
|
||||
</p>
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>База данных</mat-label>
|
||||
<mat-select (valueChange)="onDatabaseChange($event)">
|
||||
<mat-option value="SetMysql">MySQL</mat-option>
|
||||
<mat-option value="SetPsql">PostgreSQL</mat-option>
|
||||
<mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
|
||||
<mat-option value="mysql">MySQL</mat-option>
|
||||
<mat-option value="psql">PostgreSQL</mat-option>
|
||||
<mat-option value="sqlite">Sqlite</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@ -57,7 +57,8 @@
|
||||
<input matInput
|
||||
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
||||
required
|
||||
formControlName="server">
|
||||
formControlName="server"
|
||||
focusNext="portNextFocus">
|
||||
|
||||
@if (databaseForm.get('server')?.hasError('required')) {
|
||||
<mat-error>
|
||||
@ -77,7 +78,9 @@
|
||||
<input matInput
|
||||
matTooltip="Укажите порт сервера"
|
||||
required
|
||||
formControlName="port">
|
||||
formControlName="port"
|
||||
id="portNextFocus"
|
||||
focusNext="databaseNextFocus">
|
||||
|
||||
@if (databaseForm.get('port')?.hasError('required')) {
|
||||
<mat-error>
|
||||
@ -97,7 +100,9 @@
|
||||
<input matInput
|
||||
matTooltip="Укажите название базы данных"
|
||||
required
|
||||
formControlName="database_name">
|
||||
formControlName="database_name"
|
||||
id="databaseNextFocus"
|
||||
focusNext="userNextFocus">
|
||||
|
||||
@if (databaseForm.get('database_name')?.hasError('required')) {
|
||||
<mat-error>
|
||||
@ -117,7 +122,9 @@
|
||||
<input matInput
|
||||
matTooltip="Укажите пользователя, который имеет доступ к базе данных"
|
||||
required
|
||||
formControlName="user">
|
||||
formControlName="user"
|
||||
id="userNextFocus"
|
||||
focusNext="passwordNextFocus">
|
||||
|
||||
@if (databaseForm.get('user')?.hasError('required')) {
|
||||
<mat-error>
|
||||
@ -137,7 +144,9 @@
|
||||
<input matInput
|
||||
matTooltip="Укажите пароль"
|
||||
formControlName="password"
|
||||
[type]="hidePass ? 'password' : 'text'">
|
||||
[type]="hidePass ? 'password' : 'text'"
|
||||
id="passwordNextFocus"
|
||||
focusNext="nextButtonFocus">
|
||||
|
||||
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||
[attr.aria-pressed]="hidePass">
|
||||
|
@ -2,14 +2,17 @@ import {Component} from '@angular/core';
|
||||
import {NavigationService} from "@service/navigation.service";
|
||||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import SetupService from "@api/v1/setup.service";
|
||||
import {DatabaseRequest} from "@api/v1/databaseRequest";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatIconButton} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon"
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
|
||||
import {of} from "rxjs";
|
||||
import {DatabaseType} from "@model/databaseType";
|
||||
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||
|
||||
@Component({
|
||||
selector: 'app-database',
|
||||
@ -22,7 +25,8 @@ import {MatCheckbox} from "@angular/material/checkbox";
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
MatIcon,
|
||||
MatCheckbox
|
||||
MatCheckbox,
|
||||
FocusNextDirective
|
||||
],
|
||||
templateUrl: './database.component.html'
|
||||
})
|
||||
@ -49,6 +53,42 @@ export class DatabaseComponent {
|
||||
this.databaseForm.valueChanges.subscribe(() => {
|
||||
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
||||
});
|
||||
|
||||
this.api.databaseConfiguration().subscribe(response => {
|
||||
if (!response)
|
||||
return;
|
||||
|
||||
this.navigationService.setSkipButtonState(true);
|
||||
this.navigationService.skipButtonAction = () => of(true);
|
||||
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
|
||||
|
||||
|
||||
this.databaseForm.patchValue({
|
||||
server: response.server,
|
||||
port: response.port,
|
||||
database_name: response.database,
|
||||
user: response.user,
|
||||
ssl: response.ssl,
|
||||
password: response.password,
|
||||
folder: response.pathToDatabase
|
||||
});
|
||||
|
||||
let type: string;
|
||||
|
||||
switch (response.type) {
|
||||
case DatabaseType.Mysql:
|
||||
type = "mysql";
|
||||
break;
|
||||
case DatabaseType.PostgresSql:
|
||||
type = "psql";
|
||||
break;
|
||||
case DatabaseType.Sqlite:
|
||||
type = "sqlite";
|
||||
break;
|
||||
}
|
||||
this.database = type;
|
||||
this.onDatabaseChange(type);
|
||||
});
|
||||
}
|
||||
|
||||
private createForm(database: string) {
|
||||
|
@ -4,6 +4,11 @@
|
||||
Настройте систему логирования как будет удобно для отображения.
|
||||
Можно настроить путь к файлу, имена файлов или вовсе отключить логирование в файл.
|
||||
</p>
|
||||
<p class="mat-body-2 secondary">
|
||||
Также вы можете настроить интеграцию с Seq.
|
||||
Введите необходимые данные и мы отправим тестовый лог на сервер Seq. Его уровень будет Warning.
|
||||
Если тестовый лог не появился вернитесь на данный шаг и перепроверьте данные.
|
||||
</p>
|
||||
|
||||
<form [formGroup]="loggingSettings">
|
||||
<p>
|
||||
@ -31,9 +36,18 @@
|
||||
matTooltip="Укажите название файла, в который будут записаны логи"
|
||||
formControlName="logName">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Сервер Seq</mat-label>
|
||||
<input matInput
|
||||
matTooltip="Укажите сервер Seq вначале указав схему (http/https)"
|
||||
formControlName="seqServer">
|
||||
</mat-form-field>
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Api ключ Seq</mat-label>
|
||||
<input matInput
|
||||
matTooltip="Укажите ключ API, который вы создали в Seq"
|
||||
formControlName="seqKey">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<button mat-flat-button color="accent" (click)="skipButton()">Пропустить</button>
|
||||
</div>
|
||||
|
@ -6,8 +6,8 @@ import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatButton, MatIconButton} from "@angular/material/button";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {of} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-logging',
|
||||
@ -18,9 +18,7 @@ import {MatCheckbox} from "@angular/material/checkbox";
|
||||
MatSelectModule,
|
||||
MatInput,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
MatCheckbox,
|
||||
MatButton
|
||||
MatCheckbox
|
||||
|
||||
],
|
||||
templateUrl: './logging.component.html'
|
||||
@ -39,16 +37,17 @@ export class LoggingComponent {
|
||||
}
|
||||
}
|
||||
|
||||
protected skipButton() {
|
||||
this.navigationService.skipNavigation.emit(() => this.api.setLogging(null));
|
||||
}
|
||||
|
||||
constructor(
|
||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||
this.navigationService.setSkipButtonState(true);
|
||||
this.navigationService.skipButtonAction = () => this.api.setLogging(null);
|
||||
|
||||
this.loggingSettings = this.formBuilder.group({
|
||||
enabled: [true, Validators.required],
|
||||
logPath: [''],
|
||||
logName: ['']
|
||||
logName: [''],
|
||||
seqServer: [''],
|
||||
seqKey: ['']
|
||||
}
|
||||
);
|
||||
|
||||
@ -59,11 +58,27 @@ export class LoggingComponent {
|
||||
|
||||
this.navigationService.nextButtonAction = () => {
|
||||
return this.api.setLogging({
|
||||
"enableLogToFile": this.loggingSettings.get('cron')?.value,
|
||||
"logFileName": this.loggingSettings.get('logName')?.value,
|
||||
"logFilePath": this.loggingSettings.get('logPath')?.value
|
||||
enableLogToFile: this.loggingSettings.get('enabled')?.value,
|
||||
logFileName: this.loggingSettings.get('logName')?.value,
|
||||
logFilePath: this.loggingSettings.get('logPath')?.value,
|
||||
apiServerSeq: this.loggingSettings.get('seqServer')?.value,
|
||||
apiKeySeq: this.loggingSettings.get('seqKey')?.value
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
api.loggingConfiguration().subscribe(x => {
|
||||
if (!x)
|
||||
return;
|
||||
|
||||
this.navigationService.skipButtonAction = () => of(true);
|
||||
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
|
||||
|
||||
this.loggingSettings.get('enabled')?.setValue(x.enableLogToFile);
|
||||
this.loggingSettings.get('logName')?.setValue(x.logFileName);
|
||||
this.loggingSettings.get('logPath')?.setValue(x.logFilePath);
|
||||
this.loggingSettings.get('seqServer')?.setValue(x.apiServerSeq);
|
||||
this.loggingSettings.get('seqKey')?.setValue(x.apiKeySeq);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
<h1>Настройка политики паролей</h1>
|
||||
<hr/>
|
||||
<p class="mat-body-2 secondary">
|
||||
Задайте параметры для обеспечения безопасности паролей.
|
||||
<br/>
|
||||
Можно установить минимальную длину пароля и другие требования, чтобы усилить защиту учетных записей.
|
||||
</p>
|
||||
|
||||
<form [formGroup]="policyForm">
|
||||
<p>
|
||||
Введите данные для настройки политики паролей:
|
||||
</p>
|
||||
|
||||
<div style="display:flex; flex-direction: column;">
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>Минимальная длина пароля</mat-label>
|
||||
<input matInput
|
||||
type="number"
|
||||
matTooltip="Укажите минимальное количество длины пароля"
|
||||
formControlName="minimumLength">
|
||||
@if (policyForm.get('minimumLength')?.hasError('min')) {
|
||||
<mat-error>
|
||||
Пароль не может быть меньше 6 символов
|
||||
</mat-error>
|
||||
}
|
||||
@if (policyForm.get('minimumLength')?.hasError('max')) {
|
||||
<mat-error>
|
||||
Пароль не может быть больше 12 символов
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="requireLetter">
|
||||
Требовать наличие букв в пароле
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-checkbox formControlName="requireLettersDifferentCase">
|
||||
Требовать буквы разного регистра (заглавные и строчные)
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-checkbox formControlName="requireDigit">
|
||||
Требовать наличие цифр в пароле
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-checkbox formControlName="requireSpecialCharacter">
|
||||
Требовать наличие специальных символов (например, !, $, #)
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
77
src/pages/setup/password-policy/password-policy.component.ts
Normal file
77
src/pages/setup/password-policy/password-policy.component.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {NavigationService} from "@service/navigation.service";
|
||||
import SetupService from "@api/v1/setup.service";
|
||||
import {of} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-policy',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCheckbox,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
MatTooltip,
|
||||
ReactiveFormsModule,
|
||||
MatError
|
||||
],
|
||||
templateUrl: './password-policy.component.html'
|
||||
})
|
||||
export class PasswordPolicyComponent {
|
||||
protected policyForm!: FormGroup;
|
||||
|
||||
constructor(
|
||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||
this.policyForm = this.formBuilder.group({
|
||||
minimumLength: ['', [
|
||||
Validators.required,
|
||||
Validators.min(6),
|
||||
Validators.max(12)
|
||||
]],
|
||||
requireLetter: [false],
|
||||
requireLettersDifferentCase: [false],
|
||||
requireDigit: [false],
|
||||
requireSpecialCharacter: [false]
|
||||
});
|
||||
|
||||
this.api.passwordPolicyConfiguration().subscribe(response => {
|
||||
if (!response)
|
||||
return;
|
||||
|
||||
this.navigationService.setSkipButtonState(true);
|
||||
this.navigationService.skipButtonAction = () => of(true);
|
||||
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
|
||||
|
||||
this.policyForm.patchValue({
|
||||
minimumLength: response.minimumLength,
|
||||
requireLetter: response.requireLetter,
|
||||
requireLettersDifferentCase: response.requireLettersDifferentCase,
|
||||
requireDigit: response.requireDigit,
|
||||
requireSpecialCharacter: response.requireSpecialCharacter
|
||||
});
|
||||
});
|
||||
|
||||
this.navigationService.setNextButtonState(false);
|
||||
this.policyForm.valueChanges.subscribe(() => {
|
||||
this.navigationService.setNextButtonState(this.policyForm.valid);
|
||||
});
|
||||
|
||||
this.navigationService.setSkipButtonState(true);
|
||||
this.navigationService.skipButtonAction = () => this.api.setPasswordPolicy(null);
|
||||
|
||||
this.navigationService.nextButtonAction = () => {
|
||||
return this.api.setPasswordPolicy(({
|
||||
minimumLength: this.policyForm.get('minimumLength')?.value,
|
||||
requireLetter: this.policyForm.get('requireLetter')?.value,
|
||||
requireLettersDifferentCase: this.policyForm.get('requireLettersDifferentCase')?.value,
|
||||
requireDigit: this.policyForm.get('requireDigit')?.value,
|
||||
requireSpecialCharacter: this.policyForm.get('requireSpecialCharacter')?.value
|
||||
}));
|
||||
};
|
||||
};
|
||||
}
|
@ -7,9 +7,9 @@ import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatTooltip} from "@angular/material/tooltip";
|
||||
import {MatIconButton} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {of} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-conf',
|
||||
@ -20,22 +20,23 @@ import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||
MatSelectModule,
|
||||
MatInput,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
MatIcon,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule
|
||||
],
|
||||
templateUrl: './schedule.component.html'
|
||||
templateUrl: './schedule.component.html',
|
||||
})
|
||||
|
||||
export class ScheduleComponent {
|
||||
protected scheduleSettings!: FormGroup;
|
||||
|
||||
constructor(
|
||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService, private _adapter: DateAdapter<any>) {
|
||||
this._adapter.setLocale('ru');
|
||||
this.scheduleSettings = this.formBuilder.group({
|
||||
cron: ['0 */6 * * *', Validators.pattern(/^([^\s]+\s){4}[^\s]{1}$/)],
|
||||
private navigationService: NavigationService,
|
||||
formBuilder: FormBuilder,
|
||||
private api: SetupService,
|
||||
adapter: DateAdapter<any>) {
|
||||
adapter.setLocale(navigator.language);
|
||||
this.scheduleSettings = formBuilder.group({
|
||||
cron: ['0 */6 * * *', Validators.pattern(/^(\S+\s){4}\S$/)],
|
||||
startTerm: ['', Validators.required]
|
||||
}
|
||||
);
|
||||
@ -52,5 +53,19 @@ export class ScheduleComponent {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
api.scheduleConfiguration().subscribe(x => {
|
||||
if (!x)
|
||||
return;
|
||||
|
||||
this.scheduleSettings.get('startTerm')?.setValue(new DateOnly(x.startTerm).date);
|
||||
this.scheduleSettings.get('cron')?.setValue(x.cronUpdateSchedule);
|
||||
|
||||
this.navigationService.setSkipButtonState(true);
|
||||
this.navigationService.skipButtonAction = () => of(true);
|
||||
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user