diff --git a/uniplanWeb/public/i18n/bg.json b/uniplanWeb/public/i18n/bg.json index 5d3fe91..7ac66ea 100644 --- a/uniplanWeb/public/i18n/bg.json +++ b/uniplanWeb/public/i18n/bg.json @@ -13,7 +13,11 @@ }, "room": { "room-number": "Номер на стая", - "no-rooms": "Няма записани стаи" + "no-rooms": "Няма записани стаи", + "add": { + "title": "Добави стая", + "failed": "Неуспешно добавяне на стаята." + } }, "faculty": { "delete-button": "Изтрий", @@ -25,6 +29,9 @@ }, "edit": { "title": "Редактирай факултет" + }, + "load":{ + "failed":"Неуспешно зареждане на факултети" } }, "major": { @@ -60,7 +67,8 @@ "save-button": "Запази", "delete-button": "Изтрий", "option-all": "Всички", - "confirm-delete": "Потвърдете изтриването" + "confirm-delete": "Потвърдете изтриването", + "close-btn": "Затвори" }, "home": { "title": "Начало!" diff --git a/uniplanWeb/public/i18n/en.json b/uniplanWeb/public/i18n/en.json index bd15631..c44c379 100644 --- a/uniplanWeb/public/i18n/en.json +++ b/uniplanWeb/public/i18n/en.json @@ -13,7 +13,11 @@ }, "room": { "room-number": "Room number", - "no-rooms": "No rooms" + "no-rooms": "No rooms", + "add": { + "title": "Add room", + "failed": "Failed to add room." + } }, "faculty": { "delete-button": "Delete", @@ -25,6 +29,9 @@ }, "edit": { "title": "Edit faculty" + }, + "load":{ + "failed":"Failed to load faculties" } }, "major": { @@ -60,7 +67,8 @@ "save-button": "Save", "delete-button": "Delete", "option-all": "All", - "confirm-delete": "Confirm deletion" + "confirm-delete": "Confirm deletion", + "close-btn": "Close" }, "home": { "title": "Main!" diff --git a/uniplanWeb/src/app/config/endpoints.ts b/uniplanWeb/src/app/config/endpoints.ts index 53a9589..0d9f0d0 100644 --- a/uniplanWeb/src/app/config/endpoints.ts +++ b/uniplanWeb/src/app/config/endpoints.ts @@ -5,4 +5,5 @@ export const API_ENDPOINTS = { courses: `${environment.baseUrl}/courses`, faculties: `${environment.baseUrl}/faculties`, majors: `${environment.baseUrl}/majors`, + rooms: `${environment.baseUrl}/rooms`, }; diff --git a/uniplanWeb/src/app/core/interfaces/course-elm.ts b/uniplanWeb/src/app/core/interfaces/course-elm.ts new file mode 100644 index 0000000..f63c510 --- /dev/null +++ b/uniplanWeb/src/app/core/interfaces/course-elm.ts @@ -0,0 +1,7 @@ +export interface CourseElm { + id: string; + majorId: string; + courseYear: number; + locaticourseTypeon: string; + courseSubtype: string; +} \ No newline at end of file diff --git a/uniplanWeb/src/app/features/faculty/faculty-service.ts b/uniplanWeb/src/app/features/faculty/faculty-service.ts index 9b9c05f..9476fea 100644 --- a/uniplanWeb/src/app/features/faculty/faculty-service.ts +++ b/uniplanWeb/src/app/features/faculty/faculty-service.ts @@ -1,8 +1,8 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { map, Observable, Subject } from 'rxjs'; +import { map, Observable, Subject, tap } from 'rxjs'; import { FacultyElm } from '../../core/interfaces/faculty-elm'; -import {API_ENDPOINTS} from '../../config/endpoints'; +import { API_ENDPOINTS } from '../../config/endpoints'; @Injectable({ providedIn: 'root', @@ -30,9 +30,9 @@ export class FacultyService { universityId: string; facultyName: string; location: string; - }): Observable { - return this.http.post(`${API_ENDPOINTS.faculties}`, faculty).pipe( - map((res) => { + }): Observable { + return this.http.post(`${API_ENDPOINTS.faculties}`, faculty).pipe( + tap((res) => { this.refreshNeeded.next(); return res; }) @@ -46,18 +46,18 @@ export class FacultyService { facultyName: string; location: string; } - ): Observable { - return this.http.put(`${API_ENDPOINTS.faculties}/${id}`, updatedFaculty).pipe( - map((res) => { + ): Observable { + return this.http.put(`${API_ENDPOINTS.faculties}/${id}`, updatedFaculty).pipe( + tap((res) => { this.refreshNeeded.next(); return res; }) ); } - deleteFaculty(id: string): Observable { - return this.http.delete(`${API_ENDPOINTS.faculties}/${id}`).pipe( - map((res) => { + deleteFaculty(id: string): Observable { + return this.http.delete(`${API_ENDPOINTS.faculties}/${id}`).pipe( + tap((res) => { this.refreshNeeded.next(); return res; }) diff --git a/uniplanWeb/src/app/features/major/major-service.ts b/uniplanWeb/src/app/features/major/major-service.ts index f93bdbd..9b3252a 100644 --- a/uniplanWeb/src/app/features/major/major-service.ts +++ b/uniplanWeb/src/app/features/major/major-service.ts @@ -1,8 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { map, Observable, Subject, switchMap } from 'rxjs'; +import { map, Observable, Subject, switchMap, tap } from 'rxjs'; import { MajorElm } from '../../core/interfaces/major-elm'; import {API_ENDPOINTS} from '../../config/endpoints'; +import { CourseElm } from '../../core/interfaces/course-elm'; @Injectable({ providedIn: 'root', @@ -31,8 +32,8 @@ export class MajorService { createMajor(createMajor: { facultyId: string; majorName: string; - }): Observable { - return this.http.post(`${API_ENDPOINTS.majors}`, createMajor).pipe( + }): Observable { + return this.http.post(`${API_ENDPOINTS.majors}`, createMajor).pipe( map((res) => { this.refreshNeeded.next(); return res; @@ -45,25 +46,26 @@ export class MajorService { courseYear: number; courseType: string; courseSubtype: string; - }): Observable { - return this.http.post(`${API_ENDPOINTS.courses}`, course).pipe( + }): Observable { + return this.http.post(`${API_ENDPOINTS.courses}`, course).pipe( map((res) => { this.refreshNeeded.next(); return res; }) ); } + createMajorWithCourse(majorData: { facultyId: string; majorName: string; type: string; subtype: string; - }): Observable { + }): Observable { return this.createMajor({ facultyId: majorData.facultyId, majorName: majorData.majorName, }).pipe( - switchMap((createdMajor: any) => { + switchMap((createdMajor) => { return this.createCourse({ majorId: createdMajor.id, courseType: majorData.type, @@ -74,20 +76,20 @@ export class MajorService { ); } - deleteMajor(id: string): Observable { - return this.http.delete(`${API_ENDPOINTS.majors}/${id}`).pipe( - map((res) => { + deleteMajor(id: string): Observable { + return this.http.delete(`${API_ENDPOINTS.majors}/${id}`).pipe( + tap((res) => { this.refreshNeeded.next(); return res; }) ); } - deleteCourse(courseId: string): Observable { - return this.http.delete(`${API_ENDPOINTS.courses}/${courseId}`); + deleteCourse(courseId: string): Observable { + return this.http.delete(`${API_ENDPOINTS.courses}/${courseId}`); } - deleteMajorWithCourse(major: MajorElm): Observable { + deleteMajorWithCourse(major: MajorElm): Observable { return this.deleteCourse(major.courseId).pipe( switchMap(() => this.deleteMajor(major.id)) ); @@ -96,9 +98,9 @@ export class MajorService { editMajor( id: string, updateMajor: { facultyId: string; majorName: string } - ): Observable { - return this.http.put(`${API_ENDPOINTS.majors}/${id}`, updateMajor).pipe( - map((res) => { + ): Observable { + return this.http.put(`${API_ENDPOINTS.majors}/${id}`, updateMajor).pipe( + tap((res) => { this.refreshNeeded.next(); return res; }) diff --git a/uniplanWeb/src/app/features/room/room-add-form/room-add-form.html b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.html new file mode 100644 index 0000000..c5247a9 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.html @@ -0,0 +1,19 @@ + +
+ + {{ 'room.room-number' | translate }} + + + + + {{ 'features.faculty-label' | translate }} + + @for (faculty of faculties; track faculty.id) { + + {{ faculty.facultyName }} + + } + + +
+
\ No newline at end of file diff --git a/uniplanWeb/src/app/features/room/room-add-form/room-add-form.scss b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.scss new file mode 100644 index 0000000..9b05a56 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.scss @@ -0,0 +1,14 @@ +mat-form-field { + width: 100%; +} + +mat-dialog-title { + font-size: 20px; + font-weight: bold; + color: #3f51b5; +} + +input { + border-radius: 10px; + width: 100%; +} diff --git a/uniplanWeb/src/app/features/room/room-add-form/room-add-form.spec.ts b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.spec.ts new file mode 100644 index 0000000..c121f51 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RoomAddForm } from './room-add-form'; +import { translateTestingProviders } from '@testing/translate-testing'; +import { MatDialogRef } from '@angular/material/dialog'; + +describe('RoomAddForm', () => { + let component: RoomAddForm; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RoomAddForm], + providers: [ + ...translateTestingProviders, + { provide: MatDialogRef, useValue: {} }, + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RoomAddForm); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/uniplanWeb/src/app/features/room/room-add-form/room-add-form.ts b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.ts new file mode 100644 index 0000000..0a4e4b9 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-add-form/room-add-form.ts @@ -0,0 +1,103 @@ +import { Component, OnInit, ChangeDetectionStrategy, inject, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Validators, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { finalize } from 'rxjs'; +import { AddForm } from '../../../core/shared/add-form/add-form'; +import { FacultyService } from '../../faculty/faculty-service'; +import { FacultyElm } from '../../../core/interfaces/faculty-elm'; +import { RoomService } from '../room-service'; + +@Component({ + selector: 'app-room-add-form', + templateUrl: './room-add-form.html', + styleUrl: './room-add-form.scss', + changeDetection: ChangeDetectionStrategy.Eager, + imports: [ + MatDialogModule, + MatFormField, + MatLabel, + MatInputModule, + MatSelectModule, + MatOptionModule, + AddForm, + TranslatePipe, + ReactiveFormsModule + ], +}) + +export class RoomAddForm implements OnInit { + private dialogRef = inject(MatDialogRef); + private facultyService = inject(FacultyService); + private roomService = inject(RoomService) + faculties: FacultyElm[] = []; + private destroyRef = inject(DestroyRef); + private translate = inject(TranslateService); + private isSubmitting = false; + private _snackBar = inject(MatSnackBar); + + addForm = new FormGroup({ + roomNumber: new FormControl('', Validators.required), + facultyId: new FormControl('', Validators.required), + }); + + ngOnInit(): void { + this.facultyService.getFaculties() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (data) => { + this.faculties = data; + }, + error: (err) => { + console.error('Failed to load faculties', err); + this.openSnackBar( + this.translate.instant('faculty.load.failed'), + this.translate.instant('shared.close-btn') + ); + }, + }); + } + + openSnackBar(message: string, action: string): void { + this._snackBar.open(message, action); + } + + save(): void { + if (this.addForm.invalid || this.isSubmitting) { + return; + } + + this.isSubmitting = true; + this.addForm.disable(); + + const newRoom = { + roomNumber: this.addForm.value.roomNumber ?? '', + facultyId: this.addForm.value.facultyId ?? '', + }; + + this.roomService.createRoom(newRoom) + .pipe(takeUntilDestroyed(this.destroyRef), + finalize(() => { + this.isSubmitting = false; + this.addForm.enable(); + })) + .subscribe({ + next: (response) => { + this.dialogRef.close(response); + }, + error: (err) => { + console.error('Failed to add room', err); + this.openSnackBar( + this.translate.instant('room.add.failed'), + this.translate.instant('shared.close-btn') + ); + }, + }); + } +} diff --git a/uniplanWeb/src/app/features/room/room-options/room-options.html b/uniplanWeb/src/app/features/room/room-options/room-options.html new file mode 100644 index 0000000..7425749 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-options/room-options.html @@ -0,0 +1 @@ + diff --git a/uniplanWeb/src/app/features/room/room-options/room-options.scss b/uniplanWeb/src/app/features/room/room-options/room-options.scss new file mode 100644 index 0000000..0c696e5 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-options/room-options.scss @@ -0,0 +1,5 @@ +.add-button { + display: block; + margin-top: 3.3%; + margin-bottom: 1.5%; +} \ No newline at end of file diff --git a/uniplanWeb/src/app/features/room/room-options/room-options.spec.ts b/uniplanWeb/src/app/features/room/room-options/room-options.spec.ts new file mode 100644 index 0000000..0ac38d0 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-options/room-options.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RoomOptions } from './room-options'; +import { translateTestingProviders } from '@testing/translate-testing'; + +describe('RoomOptions', () => { + let component: RoomOptions; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RoomOptions], + providers: [ + ...translateTestingProviders + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RoomOptions); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/uniplanWeb/src/app/features/room/room-options/room-options.ts b/uniplanWeb/src/app/features/room/room-options/room-options.ts new file mode 100644 index 0000000..b0340c1 --- /dev/null +++ b/uniplanWeb/src/app/features/room/room-options/room-options.ts @@ -0,0 +1,28 @@ +import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTableModule } from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; +import { AddButton } from '../../../core/shared/add-button/add-button'; +import { RoomAddForm } from '../room-add-form/room-add-form'; + +@Component({ + selector: 'app-room-options', + templateUrl: './room-options.html', + styleUrl: './room-options.scss', + changeDetection: ChangeDetectionStrategy.Eager, + imports: [ + MatTableModule, + MatIconModule, + MatButtonModule, + AddButton + ], +}) +export class RoomOptions { + private dialog = inject(MatDialog) + openAddForm() : void { + this.dialog.open(RoomAddForm, { + width: '400px', + }); + } +} diff --git a/uniplanWeb/src/app/features/room/room-panel/room-panel.html b/uniplanWeb/src/app/features/room/room-panel/room-panel.html index 5e00afd..da0f289 100644 --- a/uniplanWeb/src/app/features/room/room-panel/room-panel.html +++ b/uniplanWeb/src/app/features/room/room-panel/room-panel.html @@ -1,3 +1,4 @@
- + +
\ No newline at end of file diff --git a/uniplanWeb/src/app/features/room/room-panel/room-panel.ts b/uniplanWeb/src/app/features/room/room-panel/room-panel.ts index 8b82bb7..d9cb97c 100644 --- a/uniplanWeb/src/app/features/room/room-panel/room-panel.ts +++ b/uniplanWeb/src/app/features/room/room-panel/room-panel.ts @@ -1,11 +1,12 @@ import { Component } from '@angular/core'; import { RoomTable } from '../room-table/room-table'; +import { RoomOptions } from '../room-options/room-options'; @Component({ selector: 'app-room-panel', templateUrl: './room-panel.html', styleUrl: './room-panel.scss', - imports: [RoomTable], + imports: [RoomTable, RoomOptions], }) export class RoomPanel { diff --git a/uniplanWeb/src/app/features/room/room-service.ts b/uniplanWeb/src/app/features/room/room-service.ts index d9c07c5..217367e 100644 --- a/uniplanWeb/src/app/features/room/room-service.ts +++ b/uniplanWeb/src/app/features/room/room-service.ts @@ -3,19 +3,20 @@ import { HttpClient } from '@angular/common/http'; import { map, Observable, shareReplay } from 'rxjs'; import { RoomViewModel } from '../../core/interfaces/room-view-model'; import { RoomElm } from '../../core/interfaces/room-elm'; - +import { Subject } from 'rxjs'; +import { API_ENDPOINTS } from '../../config/endpoints'; @Injectable({ providedIn: 'root' }) export class RoomService { - private apiUrl = 'http://localhost:8080/api/rooms'; + refreshNeeded = new Subject(); - http = inject(HttpClient); + private http = inject(HttpClient); getRooms(): Observable { - return this.http.get(this.apiUrl).pipe( + return this.http.get(API_ENDPOINTS.rooms).pipe( map(rooms => rooms ?? []), map(rooms => rooms.map((room, index) => ({ @@ -28,4 +29,16 @@ export class RoomService { shareReplay(1) ); } + + createRoom(room: { + roomNumber: string; + facultyId: string; + }): Observable { + return this.http.post(`${(API_ENDPOINTS.rooms)}`, room).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } } \ No newline at end of file diff --git a/uniplanWeb/src/app/features/room/room-table/room-table.html b/uniplanWeb/src/app/features/room/room-table/room-table.html index 0c9c4df..3747b80 100644 --- a/uniplanWeb/src/app/features/room/room-table/room-table.html +++ b/uniplanWeb/src/app/features/room/room-table/room-table.html @@ -35,7 +35,7 @@ - {{ element.facultyId | facultyNamePipe: facultyMap }} + {{ element.facultyId | facultyNamePipe: facultyMap() }} diff --git a/uniplanWeb/src/app/features/room/room-table/room-table.ts b/uniplanWeb/src/app/features/room/room-table/room-table.ts index 9ccfaec..4c018f5 100644 --- a/uniplanWeb/src/app/features/room/room-table/room-table.ts +++ b/uniplanWeb/src/app/features/room/room-table/room-table.ts @@ -1,4 +1,4 @@ -import { Component, inject, ChangeDetectionStrategy, DestroyRef, OnInit } from '@angular/core'; +import { Component, inject, ChangeDetectionStrategy, DestroyRef, OnInit, signal } from '@angular/core'; import { RoomService } from '../room-service'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common'; import { FacultyService } from '../../faculty/faculty-service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FacultyNamePipe } from '../../../core/shared/pipes/faculty-name-pipe'; +import { startWith, switchMap } from 'rxjs'; @Component({ selector: 'app-room-table', @@ -29,7 +30,7 @@ import { FacultyNamePipe } from '../../../core/shared/pipes/faculty-name-pipe'; export class RoomTable implements OnInit { roomService = inject(RoomService); facultyService = inject(FacultyService); - facultyMap = new Map(); + facultyMap = signal>(new Map()); private destroyRef = inject(DestroyRef); displayedColumns: string[] = [ @@ -39,17 +40,20 @@ export class RoomTable implements OnInit { 'actions', ]; - data$ = this.roomService.getRooms(); - - ngOnInit(): void{ + ngOnInit(): void { this.loadFaculties(); } + data$ = this.roomService.refreshNeeded.pipe( + startWith(void 0), + switchMap(() => this.roomService.getRooms()) + ); + loadFaculties(): void { this.facultyService.getFaculties() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((faculties) => { - this.facultyMap = new Map(faculties.map((f) => [f.id, f.facultyName])); + this.facultyMap.set(new Map(faculties.map((f) => [f.id, f.facultyName]))); }); } }