Compare commits
	
		
			23 Commits
		
	
	
		
			fcd179166e
			...
			437a3fcc58
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 437a3fcc58 | |||
| 0002371265 | |||
| 0f25d5404c | |||
| e98a0db7ca | |||
| 324c7630ea | |||
| f1f1ed16e1 | |||
| 6fcd68b627 | |||
| d50da4db3e | |||
| 066b1444af | |||
| df4ea723b3 | |||
| 434dec492d | |||
| 24d6b91553 | |||
| 2b988db70d | |||
| a3a19be5a4 | |||
| 9f742cab78 | |||
| c8bcda8da2 | |||
| 1bf2868d00 | |||
| 5b9b67d50c | |||
| 061307447e | |||
| cf09738447 | |||
| 79a992dc69 | |||
| 612da04cbb | |||
| 3d38b49839 | 
| @@ -1,6 +1,6 @@ | |||||||
| # MIREA schedule by Winsomnia | # MIREA schedule by Winsomnia | ||||||
|  |  | ||||||
| [](https://github.com/angular/angular-cli) | [](https://github.com/angular/angular-cli) | ||||||
| [](https://opensource.org/licenses/MIT) | [](https://opensource.org/licenses/MIT) | ||||||
|  |  | ||||||
| This project provides a Web interface for working with the MIREA schedule. | This project provides a Web interface for working with the MIREA schedule. | ||||||
|   | |||||||
							
								
								
									
										2071
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2071
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										30
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								package.json
									
									
									
									
									
								
							| @@ -10,17 +10,17 @@ | |||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular/animations": "^19.0.5", |     "@angular/animations": "^19.1.4", | ||||||
|     "@angular/cdk": "~19.0.4", |     "@angular/cdk": "~19.1.2", | ||||||
|     "@angular/cdk-experimental": "^19.0.4", |     "@angular/cdk-experimental": "^19.1.2", | ||||||
|     "@angular/common": "^19.0.5", |     "@angular/common": "^19.1.4", | ||||||
|     "@angular/compiler": "^19.0.5", |     "@angular/compiler": "^19.1.4", | ||||||
|     "@angular/core": "^19.0.5", |     "@angular/core": "^19.1.4", | ||||||
|     "@angular/forms": "^19.0.5", |     "@angular/forms": "^19.1.4", | ||||||
|     "@angular/material": "~19.0.4", |     "@angular/material": "~19.1.2", | ||||||
|     "@angular/platform-browser": "^19.0.5", |     "@angular/platform-browser": "^19.1.4", | ||||||
|     "@angular/platform-browser-dynamic": "^19.0.5", |     "@angular/platform-browser-dynamic": "^19.1.4", | ||||||
|     "@angular/router": "^19.0.5", |     "@angular/router": "^19.1.4", | ||||||
|     "@progress/kendo-date-math": "^1.5.14", |     "@progress/kendo-date-math": "^1.5.14", | ||||||
|     "ngx-toastr": "^19.0.0", |     "ngx-toastr": "^19.0.0", | ||||||
|     "rxjs": "~7.8.1", |     "rxjs": "~7.8.1", | ||||||
| @@ -28,9 +28,9 @@ | |||||||
|     "zone.js": "^0.15.0" |     "zone.js": "^0.15.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@angular-devkit/build-angular": "^19.0.6", |     "@angular-devkit/build-angular": "^19.1.5", | ||||||
|     "@angular/cli": "^19.0.6", |     "@angular/cli": "^19.1.5", | ||||||
|     "@angular/compiler-cli": "^19.0.5", |     "@angular/compiler-cli": "^19.1.4", | ||||||
|     "@types/jasmine": "~5.1.5", |     "@types/jasmine": "~5.1.5", | ||||||
|     "jasmine-core": "~5.5.0", |     "jasmine-core": "~5.5.0", | ||||||
|     "karma": "~6.4.4", |     "karma": "~6.4.4", | ||||||
| @@ -38,6 +38,6 @@ | |||||||
|     "karma-coverage": "~2.2.1", |     "karma-coverage": "~2.2.1", | ||||||
|     "karma-jasmine": "~5.1.0", |     "karma-jasmine": "~5.1.0", | ||||||
|     "karma-jasmine-html-reporter": "~2.1.0", |     "karma-jasmine-html-reporter": "~2.1.0", | ||||||
|     "typescript": "^5.6.3" |     "typescript": "^5.7.3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,57 +11,44 @@ export interface RequestData { | |||||||
| } | } | ||||||
|  |  | ||||||
| export class RequestBuilder { | export class RequestBuilder { | ||||||
|   private endpoint: string = ''; |   private result: RequestData = Object.create({}); | ||||||
|   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; |  | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setEndpoint(endpoint: string): this { |   public setEndpoint(endpoint: string): this { | ||||||
|     this.endpoint = endpoint; |     this.result.endpoint = endpoint; | ||||||
|     return this; |     return this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setQueryParams(queryParams: Record<string, string | number | boolean | Array<any> | null>): RequestBuilder { |   public setQueryParams(queryParams: Record<string, string | number | boolean | Array<any> | null>): RequestBuilder { | ||||||
|     this.queryParams = queryParams; |     this.result.queryParams = queryParams; | ||||||
|     return this; |     return this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public addHeaders(headers: Record<string, string>): RequestBuilder { |   public addHeaders(headers: Record<string, string>): RequestBuilder { | ||||||
|     Object.keys(headers).forEach(key => { |     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; |     return this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setData(data: any): RequestBuilder { |   public setData(data: any): RequestBuilder { | ||||||
|     this.data = data; |     this.result.data = data; | ||||||
|     return this; |     return this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setSilenceMode(silence: boolean = true): RequestBuilder { |   public setSilenceMode(silence: boolean = true): RequestBuilder { | ||||||
|     this.silenceMode = silence; |     this.result.silenceMode = silence; | ||||||
|     return this; |     return this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setWithCredentials(credentials: boolean = true): RequestBuilder { |   public setWithCredentials(credentials: boolean = true): RequestBuilder { | ||||||
|     this.withCredentials = credentials; |     this.result.withCredentials = credentials; | ||||||
|     return this; |     return this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public get build(): RequestData { |   public get build(): RequestData { | ||||||
|     return { |     return this.result; | ||||||
|       endpoint: this.endpoint, |  | ||||||
|       queryParams: this.queryParams, |  | ||||||
|       httpHeaders: this.httpHeaders, |  | ||||||
|       data: this.data, |  | ||||||
|       silenceMode: this.silenceMode, |  | ||||||
|       withCredentials: this.withCredentials, |  | ||||||
|       needAuth: false |  | ||||||
|     }; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ export default abstract class ApiService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private static combineUrls(...parts: string[]): string { |   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 combinedUrl(request: RequestData) { |   protected combinedUrl(request: RequestData) { | ||||||
| @@ -65,7 +65,7 @@ export default abstract class ApiService { | |||||||
|     return this.http.request<Type>(method, doneEndpoint, { |     return this.http.request<Type>(method, doneEndpoint, { | ||||||
|       withCredentials: request.withCredentials, |       withCredentials: request.withCredentials, | ||||||
|       headers: request.httpHeaders, |       headers: request.httpHeaders, | ||||||
|       body: request.data, |       body: request.data | ||||||
|     }).pipe( |     }).pipe( | ||||||
|       catchError(error => { |       catchError(error => { | ||||||
|         if (request.needAuth && !secondTry && error.status === 401) |         if (request.needAuth && !secondTry && error.status === 401) | ||||||
| @@ -158,7 +158,7 @@ export default abstract class ApiService { | |||||||
|  |  | ||||||
|   private handleError(error: HttpErrorResponse): void { |   private handleError(error: HttpErrorResponse): void { | ||||||
|     // todo: change to Retry-After condition |     // todo: change to Retry-After condition | ||||||
|     if (error.error && error.error.detail.includes("setup")) { |     if (error.error && error.error.detail && error.error.detail.includes("setup")) { | ||||||
|       this.router.navigate(['/setup/']).then(); |       this.router.navigate(['/setup/']).then(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										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[], 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}) | ||||||
|  |       .build; | ||||||
|  |  | ||||||
|  |     return this.addAuth(request).post(request); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| import {ApplicationConfig} from '@angular/core'; | import {ApplicationConfig, LOCALE_ID} from '@angular/core'; | ||||||
| import {provideRouter} from '@angular/router'; | import {provideRouter} from '@angular/router'; | ||||||
|  |  | ||||||
| import {routes} from './app.routes'; | import {routes} from './app.routes'; | ||||||
| import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; | import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; | ||||||
| import {provideHttpClient} from "@angular/common/http"; | import {provideHttpClient} from "@angular/common/http"; | ||||||
| import {provideToastr} from "ngx-toastr"; | import {provideToastr} from "ngx-toastr"; | ||||||
|  | import {MAT_DATE_LOCALE, provideNativeDateAdapter} from "@angular/material/core"; | ||||||
|  |  | ||||||
| export const appConfig: ApplicationConfig = { | export const appConfig: ApplicationConfig = { | ||||||
|   providers: [ |   providers: [ | ||||||
| @@ -22,5 +22,8 @@ export const appConfig: ApplicationConfig = { | |||||||
|       disableTimeOut: false, |       disableTimeOut: false, | ||||||
|       autoDismiss: true, |       autoDismiss: true, | ||||||
|       maxOpened: 5 |       maxOpened: 5 | ||||||
|     })] |     }), | ||||||
|  |     provideNativeDateAdapter(), | ||||||
|  |     { provide: LOCALE_ID, useValue: 'ru' }, | ||||||
|  |     { provide: MAT_DATE_LOCALE, useValue: 'ru' }] | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -11,6 +11,9 @@ import {SummaryComponent} from "@page/setup/summary/summary.component"; | |||||||
| import {LoginComponent} from "@page/login/login.component"; | import {LoginComponent} from "@page/login/login.component"; | ||||||
| import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component"; | import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component"; | ||||||
| import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.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 = [ | export const routes: Routes = [ | ||||||
|   {path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent}, |   {path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent}, | ||||||
| @@ -29,6 +32,14 @@ export const routes: Routes = [ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   {path: 'login', title: 'Вход', component: LoginComponent}, |   {path: 'login', title: 'Вход', component: LoginComponent}, | ||||||
|   /*{path: 'not-found', title: '404 страница не найдена'}, |   { | ||||||
|   {path: '**', redirectTo: '/not-found'}*/ |     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'} | ||||||
|  |     ] | ||||||
|  |   } | ||||||
| ]; | ]; | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core'; | import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core'; | ||||||
| import AuthApiService, {OAuthProviderData} from "@api/v1/authApiService"; | import AuthApiService, {OAuthProviderData} from "@api/v1/authApi.service"; | ||||||
| import {OAuthProvider} from "@model/oAuthProvider"; | import {OAuthProvider} from "@model/oAuthProvider"; | ||||||
| import {ToastrService} from "ngx-toastr"; | import {ToastrService} from "ngx-toastr"; | ||||||
| import { | import { | ||||||
|   | |||||||
| @@ -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,28 @@ | |||||||
|  | <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> | ||||||
|  |       <ul> | ||||||
|  |         @for (file of selectedFiles; track $index) { | ||||||
|  |           <li>{{ file.name }}</li> | ||||||
|  |         } | ||||||
|  |       </ul> | ||||||
|  |     </div> | ||||||
|  |   } | ||||||
|  | </app-configuration-card> | ||||||
| @@ -0,0 +1,57 @@ | |||||||
|  | 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"; | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-schedule-file-upload', | ||||||
|  |   imports: [ | ||||||
|  |     ConfigurationCardComponent, | ||||||
|  |     MatButtonModule, | ||||||
|  |     MatIcon, | ||||||
|  |     DataSpinnerComponent | ||||||
|  |   ], | ||||||
|  |   templateUrl: './schedule-file-upload.component.html', | ||||||
|  |   providers: [ScheduleService] | ||||||
|  | }) | ||||||
|  | export class ScheduleFileUploadComponent { | ||||||
|  |   selectedFiles: File[] = []; | ||||||
|  |   fileLoading: boolean = false; | ||||||
|  |   @ViewChild('fileInput') input!: ElementRef; | ||||||
|  |  | ||||||
|  |   constructor(private dialog: MatDialog, private api: ScheduleService) { | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected saveFunction() { | ||||||
|  |     return () => { | ||||||
|  |       const dialogRef = this.dialog.open(ConfirmDeleteScheduleDialogComponent); | ||||||
|  |  | ||||||
|  |       return dialogRef.afterClosed().pipe(switchMap(result => { | ||||||
|  |         return this.api.uploadScheduleFile(this.selectedFiles, result); | ||||||
|  |       })); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onFileChooseClick() { | ||||||
|  |     this.fileLoading = true; | ||||||
|  |     this.input.nativeElement.click(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onFileSelected(event: any): void { | ||||||
|  |     this.fileLoading = false; | ||||||
|  |     this.selectedFiles = Array.from(event.target.files); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onUpload(data: Observable<any>): void { | ||||||
|  |     data.subscribe(_ => { | ||||||
|  |       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.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); | ||||||
|  | } | ||||||
| @@ -23,6 +23,7 @@ | |||||||
|       <app-other idButton="lecture-button" textButton="Кабинеты" #lecture (retryLoadData)="loadLectureHalls()"/> |       <app-other idButton="lecture-button" textButton="Кабинеты" #lecture (retryLoadData)="loadLectureHalls()"/> | ||||||
|       <app-other idButton="group-button" textButton="Группы" #group (retryLoadData)="loadGroups()"/> |       <app-other idButton="group-button" textButton="Группы" #group (retryLoadData)="loadGroups()"/> | ||||||
|       <app-other idButton="professor-button" textButton="Профессоры" #professor (retryLoadData)="loadProfessors()"/> |       <app-other idButton="professor-button" textButton="Профессоры" #professor (retryLoadData)="loadProfessors()"/> | ||||||
|  |       <app-other idButton="lesson-type-button" textButton="Тип занятия" #lesson_type (retryLoadData)="loadLessonType()"/> | ||||||
|       <section> |       <section> | ||||||
|         <button mat-flat-button (click)="otherFilter()">Отфильтровать</button> |         <button mat-flat-button (click)="otherFilter()">Отфильтровать</button> | ||||||
|       </section> |       </section> | ||||||
|   | |||||||
| @@ -18,6 +18,8 @@ import {AuthRoles} from "@model/authRoles"; | |||||||
| import {HasRoleDirective} from "@/directives/has-role.directive"; | import {HasRoleDirective} from "@/directives/has-role.directive"; | ||||||
| import {TabSelectType, TabStorageService} from "@service/tab-storage.service"; | import {TabSelectType, TabStorageService} from "@service/tab-storage.service"; | ||||||
| import {ScheduleRequest} from "@api/v1/scheduleRequest"; | import {ScheduleRequest} from "@api/v1/scheduleRequest"; | ||||||
|  | import {CampusService} from "@api/v1/campus.service"; | ||||||
|  | import {LessonTypeService} from "@api/v1/lessonType.service"; | ||||||
|  |  | ||||||
| export enum TabsSelect { | export enum TabsSelect { | ||||||
|   Group, |   Group, | ||||||
| @@ -43,7 +45,15 @@ export enum TabsSelect { | |||||||
|   ], |   ], | ||||||
|   templateUrl: './tabs.component.html', |   templateUrl: './tabs.component.html', | ||||||
|   styleUrl: './tabs.component.css', |   styleUrl: './tabs.component.css', | ||||||
|   providers: [ScheduleService, DisciplineService, LectureHallService, GroupService, ProfessorService, TabStorageService] |   providers: [ | ||||||
|  |     ScheduleService, | ||||||
|  |     DisciplineService, | ||||||
|  |     LectureHallService, | ||||||
|  |     GroupService, | ||||||
|  |     ProfessorService, | ||||||
|  |     TabStorageService, | ||||||
|  |     CampusService, | ||||||
|  |     LessonTypeService] | ||||||
| }) | }) | ||||||
|  |  | ||||||
| export class TabsComponent implements AfterViewInit { | export class TabsComponent implements AfterViewInit { | ||||||
| @@ -55,7 +65,9 @@ export class TabsComponent implements AfterViewInit { | |||||||
|               private lectureApi: LectureHallService, |               private lectureApi: LectureHallService, | ||||||
|               private groupApi: GroupService, |               private groupApi: GroupService, | ||||||
|               private professorApi: ProfessorService, |               private professorApi: ProfessorService, | ||||||
|               private tabStorage: TabStorageService) { |               private tabStorage: TabStorageService, | ||||||
|  |               private campusApi: CampusService, | ||||||
|  |               private lessonTypeApi: LessonTypeService) { | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngAfterViewInit(): void { |   ngAfterViewInit(): void { | ||||||
| @@ -142,6 +154,7 @@ export class TabsComponent implements AfterViewInit { | |||||||
|         await this.loadLectureHalls(); |         await this.loadLectureHalls(); | ||||||
|         await this.loadGroups(); |         await this.loadGroups(); | ||||||
|         await this.loadProfessors(); |         await this.loadProfessors(); | ||||||
|  |         await this.loadLessonType(); | ||||||
|         break; |         break; | ||||||
|       default: |       default: | ||||||
|         await this.chooseTabs(0); |         await this.chooseTabs(0); | ||||||
| @@ -159,12 +172,14 @@ export class TabsComponent implements AfterViewInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected async loadLectureHalls() { |   protected async loadLectureHalls() { | ||||||
|  |     this.campusApi.getCampus().subscribe(campus => { | ||||||
|       this.lectureApi.getLectureHalls().subscribe(data => { |       this.lectureApi.getLectureHalls().subscribe(data => { | ||||||
|         this.lectureHallEx.Data = data.map(x => ({ |         this.lectureHallEx.Data = data.map(x => ({ | ||||||
|           id: x.id, |           id: x.id, | ||||||
|         name: x.name |           name: x.name + ` (${campus.find(c => c.id == x.campusId)?.codeName})` | ||||||
|         }) as SelectData); |         }) as SelectData); | ||||||
|       }); |       }); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected async loadGroups() { |   protected async loadGroups() { | ||||||
| @@ -185,6 +200,15 @@ export class TabsComponent implements AfterViewInit { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   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('groupTab') groupTab!: IScheduleTab; | ||||||
|   @ViewChild('professorTab') professorTab!: IScheduleTab; |   @ViewChild('professorTab') professorTab!: IScheduleTab; | ||||||
|   @ViewChild('lectureHallTab') lectureHallTab!: IScheduleTab; |   @ViewChild('lectureHallTab') lectureHallTab!: IScheduleTab; | ||||||
| @@ -193,6 +217,7 @@ export class TabsComponent implements AfterViewInit { | |||||||
|   @ViewChild('lecture') lectureHallEx!: OtherComponent; |   @ViewChild('lecture') lectureHallEx!: OtherComponent; | ||||||
|   @ViewChild('group') groupEx!: OtherComponent; |   @ViewChild('group') groupEx!: OtherComponent; | ||||||
|   @ViewChild('professor') professorEx!: OtherComponent; |   @ViewChild('professor') professorEx!: OtherComponent; | ||||||
|  |   @ViewChild('lesson_type') lessonTypeEx!: OtherComponent; | ||||||
|  |  | ||||||
|   @ViewChild('tabGroup') tabs!: MatTabGroup; |   @ViewChild('tabGroup') tabs!: MatTabGroup; | ||||||
|   protected readonly AuthRoles = AuthRoles; |   protected readonly AuthRoles = AuthRoles; | ||||||
| @@ -202,7 +227,8 @@ export class TabsComponent implements AfterViewInit { | |||||||
|       groups: this.groupEx.selectedIds, |       groups: this.groupEx.selectedIds, | ||||||
|       disciplines: this.disciplineEx.selectedIds, |       disciplines: this.disciplineEx.selectedIds, | ||||||
|       professors: this.professorEx.selectedIds, |       professors: this.professorEx.selectedIds, | ||||||
|       lectureHalls: this.lectureHallEx.selectedIds |       lectureHalls: this.lectureHallEx.selectedIds, | ||||||
|  |       lessonType: this.lessonTypeEx.selectedIds | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.eventResult.emit( |     this.eventResult.emit( | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core'; | import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core'; | ||||||
| import AuthApiService from "@api/v1/authApiService"; | import AuthApiService from "@api/v1/authApi.service"; | ||||||
| import {AuthRoles} from "@model/authRoles"; | import {AuthRoles} from "@model/authRoles"; | ||||||
| import {catchError, of} from "rxjs"; | import {catchError, of} from "rxjs"; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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 { | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ import {MatCard} from "@angular/material/card"; | |||||||
| import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; | import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; | ||||||
| import {FocusNextDirective} from "@/directives/focus-next.directive"; | import {FocusNextDirective} from "@/directives/focus-next.directive"; | ||||||
| import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component"; | import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component"; | ||||||
| import AuthApiService from "@api/v1/authApiService"; | import AuthApiService from "@api/v1/authApi.service"; | ||||||
| import {Router} from "@angular/router"; | import {Router} from "@angular/router"; | ||||||
| import {catchError} from "rxjs"; | import {catchError} from "rxjs"; | ||||||
| import {TwoFactorAuthentication} from "@model/twoFactorAuthentication"; | import {TwoFactorAuthentication} from "@model/twoFactorAuthentication"; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import {Component, LOCALE_ID, ViewChild} from '@angular/core'; | import {Component, ViewChild} from '@angular/core'; | ||||||
| import {AdditionalText, TableHeaderComponent} from "@component/schedule/table-header/table-header.component"; | import {AdditionalText, TableHeaderComponent} from "@component/schedule/table-header/table-header.component"; | ||||||
| import {addDays, weekInYear} from "@progress/kendo-date-math"; | import {addDays, weekInYear} from "@progress/kendo-date-math"; | ||||||
| import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component"; | import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component"; | ||||||
| @@ -38,8 +38,7 @@ import {HasRoleDirective} from "@/directives/has-role.directive"; | |||||||
|   styleUrl: './schedule.component.css', |   styleUrl: './schedule.component.css', | ||||||
|   providers: [ |   providers: [ | ||||||
|     ScheduleService, |     ScheduleService, | ||||||
|     ImportService, |     ImportService | ||||||
|     {provide: LOCALE_ID, useValue: 'ru-RU'} |  | ||||||
|   ] |   ] | ||||||
| }) | }) | ||||||
|  |  | ||||||
| @@ -56,7 +55,11 @@ export class ScheduleComponent { | |||||||
|  |  | ||||||
|   @ViewChild('tableHeader') childComponent!: TableHeaderComponent; |   @ViewChild('tableHeader') childComponent!: TableHeaderComponent; | ||||||
|  |  | ||||||
|   constructor(api: ScheduleService, route: ActivatedRoute, private importApi: ImportService, private notify: ToastrService, public dialog: MatDialog) { |   constructor(api: ScheduleService, | ||||||
|  |               route: ActivatedRoute, | ||||||
|  |               private importApi: ImportService, | ||||||
|  |               private notify: ToastrService, | ||||||
|  |               public dialog: MatDialog) { | ||||||
|     route.queryParams.subscribe(params => { |     route.queryParams.subscribe(params => { | ||||||
|       TabStorageService.selectDataFromQuery(params); |       TabStorageService.selectDataFromQuery(params); | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import {MatInput} from "@angular/material/input"; | |||||||
| import {MatTooltip} from "@angular/material/tooltip"; | import {MatTooltip} from "@angular/material/tooltip"; | ||||||
| import {MatIconButton} from "@angular/material/button"; | import {MatIconButton} from "@angular/material/button"; | ||||||
| import {MatIcon} from "@angular/material/icon"; | import {MatIcon} from "@angular/material/icon"; | ||||||
| import AuthApiService from "@api/v1/authApiService"; | import AuthApiService from "@api/v1/authApi.service"; | ||||||
| import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders"; | import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders"; | ||||||
| import {OAuthProvider} from "@model/oAuthProvider"; | import {OAuthProvider} from "@model/oAuthProvider"; | ||||||
| import {PasswordInputComponent} from "@component/common/password-input/password-input.component"; | import {PasswordInputComponent} from "@component/common/password-input/password-input.component"; | ||||||
|   | |||||||
| @@ -4,4 +4,5 @@ export interface ScheduleRequest { | |||||||
|   disciplines?: Array<number>; |   disciplines?: Array<number>; | ||||||
|   professors?: Array<number>; |   professors?: Array<number>; | ||||||
|   lectureHalls?: Array<number>; |   lectureHalls?: Array<number>; | ||||||
|  |   lessonType?: Array<number>; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,4 @@ | |||||||
|  | export interface CronUpdateScheduleResponse { | ||||||
|  |   cron: string; | ||||||
|  |   nextStart?: Date[]; | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/shared/responses/v1/lessonTypeResponse.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/shared/responses/v1/lessonTypeResponse.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | export interface LessonTypeResponse { | ||||||
|  |   id: number; | ||||||
|  |   name: string; | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								src/shared/structs/cronUpdateSkip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/shared/structs/cronUpdateSkip.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import {DateOnly} from "@model/dateOnly"; | ||||||
|  |  | ||||||
|  | export default interface CronUpdateSkip { | ||||||
|  |   start?: DateOnly; | ||||||
|  |   end?: DateOnly; | ||||||
|  |   date?: DateOnly; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user