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 | ||||
|  | ||||
| [](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. | ||||
|   | ||||
							
								
								
									
										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, | ||||
|   "dependencies": { | ||||
|     "@angular/animations": "^19.0.5", | ||||
|     "@angular/cdk": "~19.0.4", | ||||
|     "@angular/cdk-experimental": "^19.0.4", | ||||
|     "@angular/common": "^19.0.5", | ||||
|     "@angular/compiler": "^19.0.5", | ||||
|     "@angular/core": "^19.0.5", | ||||
|     "@angular/forms": "^19.0.5", | ||||
|     "@angular/material": "~19.0.4", | ||||
|     "@angular/platform-browser": "^19.0.5", | ||||
|     "@angular/platform-browser-dynamic": "^19.0.5", | ||||
|     "@angular/router": "^19.0.5", | ||||
|     "@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", | ||||
| @@ -28,9 +28,9 @@ | ||||
|     "zone.js": "^0.15.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@angular-devkit/build-angular": "^19.0.6", | ||||
|     "@angular/cli": "^19.0.6", | ||||
|     "@angular/compiler-cli": "^19.0.5", | ||||
|     "@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", | ||||
| @@ -38,6 +38,6 @@ | ||||
|     "karma-coverage": "~2.2.1", | ||||
|     "karma-jasmine": "~5.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 { | ||||
|   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 result: RequestData = Object.create({}); | ||||
|  | ||||
|   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 get build(): RequestData { | ||||
|     return { | ||||
|       endpoint: this.endpoint, | ||||
|       queryParams: this.queryParams, | ||||
|       httpHeaders: this.httpHeaders, | ||||
|       data: this.data, | ||||
|       silenceMode: this.silenceMode, | ||||
|       withCredentials: this.withCredentials, | ||||
|       needAuth: false | ||||
|     }; | ||||
|     return this.result; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -52,7 +52,7 @@ export default abstract class ApiService { | ||||
|   } | ||||
|  | ||||
|   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) { | ||||
| @@ -65,7 +65,7 @@ export default abstract class ApiService { | ||||
|     return this.http.request<Type>(method, doneEndpoint, { | ||||
|       withCredentials: request.withCredentials, | ||||
|       headers: request.httpHeaders, | ||||
|       body: request.data, | ||||
|       body: request.data | ||||
|     }).pipe( | ||||
|       catchError(error => { | ||||
|         if (request.needAuth && !secondTry && error.status === 401) | ||||
| @@ -158,7 +158,7 @@ export default abstract class ApiService { | ||||
|  | ||||
|   private handleError(error: HttpErrorResponse): void { | ||||
|     // 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(); | ||||
|       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 {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: [ | ||||
| @@ -22,5 +22,8 @@ export const appConfig: ApplicationConfig = { | ||||
|       disableTimeOut: false, | ||||
|       autoDismiss: true, | ||||
|       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 {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}, | ||||
| @@ -29,6 +32,14 @@ export const routes: Routes = [ | ||||
|     ] | ||||
|   }, | ||||
|   {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 AuthApiService, {OAuthProviderData} from "@api/v1/authApiService"; | ||||
| import AuthApiService, {OAuthProviderData} from "@api/v1/authApi.service"; | ||||
| import {OAuthProvider} from "@model/oAuthProvider"; | ||||
| import {ToastrService} from "ngx-toastr"; | ||||
| 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="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 (click)="otherFilter()">Отфильтровать</button> | ||||
|       </section> | ||||
|   | ||||
| @@ -18,6 +18,8 @@ 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, | ||||
| @@ -43,7 +45,15 @@ export enum TabsSelect { | ||||
|   ], | ||||
|   templateUrl: './tabs.component.html', | ||||
|   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 { | ||||
| @@ -55,7 +65,9 @@ export class TabsComponent implements AfterViewInit { | ||||
|               private lectureApi: LectureHallService, | ||||
|               private groupApi: GroupService, | ||||
|               private professorApi: ProfessorService, | ||||
|               private tabStorage: TabStorageService) { | ||||
|               private tabStorage: TabStorageService, | ||||
|               private campusApi: CampusService, | ||||
|               private lessonTypeApi: LessonTypeService) { | ||||
|   } | ||||
|  | ||||
|   ngAfterViewInit(): void { | ||||
| @@ -142,6 +154,7 @@ export class TabsComponent implements AfterViewInit { | ||||
|         await this.loadLectureHalls(); | ||||
|         await this.loadGroups(); | ||||
|         await this.loadProfessors(); | ||||
|         await this.loadLessonType(); | ||||
|         break; | ||||
|       default: | ||||
|         await this.chooseTabs(0); | ||||
| @@ -159,12 +172,14 @@ export class TabsComponent implements AfterViewInit { | ||||
|   } | ||||
|  | ||||
|   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 | ||||
|           name: x.name + ` (${campus.find(c => c.id == x.campusId)?.codeName})` | ||||
|         }) as SelectData); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   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('professorTab') professorTab!: IScheduleTab; | ||||
|   @ViewChild('lectureHallTab') lectureHallTab!: IScheduleTab; | ||||
| @@ -193,6 +217,7 @@ export class TabsComponent implements AfterViewInit { | ||||
|   @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; | ||||
| @@ -202,7 +227,8 @@ export class TabsComponent implements AfterViewInit { | ||||
|       groups: this.groupEx.selectedIds, | ||||
|       disciplines: this.disciplineEx.selectedIds, | ||||
|       professors: this.professorEx.selectedIds, | ||||
|       lectureHalls: this.lectureHallEx.selectedIds | ||||
|       lectureHalls: this.lectureHallEx.selectedIds, | ||||
|       lessonType: this.lessonTypeEx.selectedIds | ||||
|     }); | ||||
|  | ||||
|     this.eventResult.emit( | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| 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 {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 {FocusNextDirective} from "@/directives/focus-next.directive"; | ||||
| 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 {catchError} from "rxjs"; | ||||
| 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 {addDays, weekInYear} from "@progress/kendo-date-math"; | ||||
| import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component"; | ||||
| @@ -38,8 +38,7 @@ import {HasRoleDirective} from "@/directives/has-role.directive"; | ||||
|   styleUrl: './schedule.component.css', | ||||
|   providers: [ | ||||
|     ScheduleService, | ||||
|     ImportService, | ||||
|     {provide: LOCALE_ID, useValue: 'ru-RU'} | ||||
|     ImportService | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| @@ -56,7 +55,11 @@ export class ScheduleComponent { | ||||
|  | ||||
|   @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 => { | ||||
|       TabStorageService.selectDataFromQuery(params); | ||||
|     }); | ||||
|   | ||||
| @@ -10,7 +10,7 @@ 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/authApiService"; | ||||
| 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"; | ||||
|   | ||||
| @@ -4,4 +4,5 @@ export interface ScheduleRequest { | ||||
|   disciplines?: Array<number>; | ||||
|   professors?: 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