diff --git a/uniplanWeb/public/i18n/bg.json b/uniplanWeb/public/i18n/bg.json index 5d3fe91..4681fec 100644 --- a/uniplanWeb/public/i18n/bg.json +++ b/uniplanWeb/public/i18n/bg.json @@ -6,6 +6,7 @@ "type-label": "Тип", "study-mode-label": "Форма на обучение", "major-label": "Специалност", + "department-label": "Катедра", "location-label": "Местоположение", "number-label": "No.", "edit": "Редактирай", @@ -13,7 +14,10 @@ }, "room": { "room-number": "Номер на стая", - "no-rooms": "Няма записани стаи" + "no-rooms": "Няма записани стаи", + "department-label": "Катедра", + "number-label": "№" + }, "faculty": { "delete-button": "Изтрий", @@ -40,6 +44,21 @@ "title": "Редактирай специалност" } }, + "department": { + "delete-button": "Изтрий", + "edit-button": "Редактирай", + "uni-label": "Университет", + "delete-confirmation": "Сигурни ли сте, че искате да изтриете катедра с име: ", + "create-error": "Възникна грешка при създаването на катедрата.", + "update-error": "Възникна грешка при редактирането на катедрата.", + "delete-error": "Възникна грешка при изтриването на катедрата.", + "add": { + "title": "Добави катедра" + }, + "edit": { + "title": "Редактирай катедра" + } + }, "student": { "faculty-number-label": "Факултетен номер", "course-label": "Курс", @@ -60,7 +79,8 @@ "save-button": "Запази", "delete-button": "Изтрий", "option-all": "Всички", - "confirm-delete": "Потвърдете изтриването" + "confirm-delete": "Потвърдете изтриването", + "required-field": "Това поле е задължително" }, "home": { "title": "Начало!" @@ -85,4 +105,4 @@ "logout": "Изход", "login": "Вход" } -} \ No newline at end of file +} diff --git a/uniplanWeb/public/i18n/en.json b/uniplanWeb/public/i18n/en.json index bd15631..517d38b 100644 --- a/uniplanWeb/public/i18n/en.json +++ b/uniplanWeb/public/i18n/en.json @@ -7,13 +7,8 @@ "study-mode-label": "Study mode", "major-label": "Major", "location-label": "Location", - "number-label": "No.", - "edit": "Edit", - "delete": "Delete" - }, - "room": { - "room-number": "Room number", - "no-rooms": "No rooms" + "department-label": "Department", + "number-label": "No." }, "faculty": { "delete-button": "Delete", @@ -40,6 +35,21 @@ "title": "Edit major" } }, + "department": { + "delete-button": "Delete", + "edit-button": "Edit", + "uni-label": "University", + "delete-confirmation": "Are you sure you want to delete the department named: ", + "create-error": "Failed to create department.", + "update-error": "Failed to update department.", + "delete-error": "Failed to delete department.", + "add": { + "title": "Add department" + }, + "edit": { + "title": "Edit department" + } + }, "student": { "faculty-number-label": "Student ID", "course-label": "Course", @@ -60,7 +70,8 @@ "save-button": "Save", "delete-button": "Delete", "option-all": "All", - "confirm-delete": "Confirm deletion" + "confirm-delete": "Confirm deletion", + "required-field": "This field is required" }, "home": { "title": "Main!" @@ -85,4 +96,4 @@ "logout": "Logout", "login": "Login" } -} \ No newline at end of file +} diff --git a/uniplanWeb/src/app/app.routes.ts b/uniplanWeb/src/app/app.routes.ts index 69caa0b..4130587 100644 --- a/uniplanWeb/src/app/app.routes.ts +++ b/uniplanWeb/src/app/app.routes.ts @@ -17,6 +17,11 @@ export const routes: Routes = [ loadComponent: () => import('./features/faculty/faculty-panel/faculty-panel').then(m => m.FacultyPanel), }, + { + path: 'department', + loadComponent: () => + import('./features/department/department-panel/department-panel').then(m => m.DepartmentPanel), + }, { path: 'major', loadComponent: () => diff --git a/uniplanWeb/src/app/config/endpoints.ts b/uniplanWeb/src/app/config/endpoints.ts index 53a9589..d11935e 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`, + departments: `${environment.baseUrl}/departments`, }; diff --git a/uniplanWeb/src/app/core/interfaces/department-elm.ts b/uniplanWeb/src/app/core/interfaces/department-elm.ts new file mode 100644 index 0000000..a81bfe5 --- /dev/null +++ b/uniplanWeb/src/app/core/interfaces/department-elm.ts @@ -0,0 +1,6 @@ +export interface DepartmentElm { + id: string; + departmentName: string; + facultyId: string; + position: number; +} diff --git a/uniplanWeb/src/app/core/shared/navmenu-component/navmenu-component.html b/uniplanWeb/src/app/core/shared/navmenu-component/navmenu-component.html index 5cf411d..cbc83aa 100644 --- a/uniplanWeb/src/app/core/shared/navmenu-component/navmenu-component.html +++ b/uniplanWeb/src/app/core/shared/navmenu-component/navmenu-component.html @@ -30,7 +30,9 @@

Logo

  • - {{ 'navmenu.department' | translate }} + + {{ 'navmenu.department' | translate }} +
  • diff --git a/uniplanWeb/src/app/features/department/department-add-form/department-add-form.html b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.html new file mode 100644 index 0000000..f387e91 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.html @@ -0,0 +1,27 @@ + + + + + {{ 'features.name-label' | translate }} + + @if (form.controls.departmentName.hasError('required')) { + {{ 'shared.required-field' | translate }} + } + + + + {{ 'features.faculty-label' | translate }} + + @for (fac of faculties; track fac) { + + {{ fac.facultyName }} + + } + + @if (form.controls.facultyId.hasError('required')) { + {{ 'shared.required-field' | translate }} + } + + + + diff --git a/uniplanWeb/src/app/features/department/department-add-form/department-add-form.scss b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.scss new file mode 100644 index 0000000..9b05a56 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-add-form/department-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/department/department-add-form/department-add-form.spec.ts b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.spec.ts new file mode 100644 index 0000000..77e8e01 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.spec.ts @@ -0,0 +1,18 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DepartmentAddForm } from './department-add-form'; + +describe('DepartmentAddForm', () => { + let component: DepartmentAddForm; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentAddForm], + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentAddForm); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-add-form/department-add-form.ts b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.ts new file mode 100644 index 0000000..722596d --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-add-form/department-add-form.ts @@ -0,0 +1,89 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { AddForm } from '../../../core/shared/add-form/add-form'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { + MatFormField, + MatFormFieldModule, + MatLabel, +} from '@angular/material/form-field'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { DepartmentService } from '../department-service'; +import { FacultyElm } from '../../../core/interfaces/faculty-elm'; +import { FacultyService } from '../../faculty/faculty-service'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'app-department-add-form', + imports: [ + MatDialogModule, + MatFormField, + MatLabel, + ReactiveFormsModule, + MatInputModule, + AddForm, + MatFormFieldModule, + MatSelectModule, + TranslatePipe, + ], + templateUrl: './department-add-form.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './department-add-form.scss', +}) +export class DepartmentAddForm implements OnInit { + protected readonly form = new FormGroup({ + departmentName: new FormControl('', { + nonNullable: true, + validators: [Validators.required], + }), + facultyId: new FormControl('', { + nonNullable: true, + validators: [Validators.required], + }), + }); + + protected faculties: FacultyElm[] = []; + + constructor( + private dialogRef: MatDialogRef, + private departmentService: DepartmentService, + private facultyService: FacultyService, + private translate: TranslateService + ) {} + + ngOnInit(): void { + this.facultyService.getFaculties().subscribe({ + next: (data) => { + this.faculties = data; + }, + error: (err) => console.error('Failed to load faculties', err), + }); + } + + protected save(): void { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + + const { departmentName, facultyId } = this.form.getRawValue(); + + this.departmentService + .createDepartment({ + departmentName: departmentName.trim(), + facultyId, + }) + .subscribe({ + next: () => { + this.dialogRef.close(true); + }, + error: () => alert(this.translate.instant('department.create-error')), + }); + } +} diff --git a/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.html b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.html new file mode 100644 index 0000000..958f186 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.html @@ -0,0 +1,8 @@ + + +

    + {{'department.delete-confirmation' | translate }} + {{ data.departmentName }}? +

    +
    +
    diff --git a/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.scss b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.scss new file mode 100644 index 0000000..e69de29 diff --git a/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.spec.ts b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.spec.ts new file mode 100644 index 0000000..022983d --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.spec.ts @@ -0,0 +1,18 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DepartmentDeleteForm } from './department-delete-form'; + +describe('DepartmentDeleteForm', () => { + let component: DepartmentDeleteForm; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentDeleteForm], + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentDeleteForm); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.ts b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.ts new file mode 100644 index 0000000..2376cd0 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-delete-form/department-delete-form.ts @@ -0,0 +1,36 @@ +import {ChangeDetectionStrategy, Component, Inject} from '@angular/core'; +import {DeleteForm} from '../../../core/shared/delete-form/delete-form'; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef,} from '@angular/material/dialog'; +import {DepartmentService} from '../department-service'; +import {TranslatePipe, TranslateService} from '@ngx-translate/core'; + +@Component({ + selector: 'app-department-delete-form', + imports: [DeleteForm, MatDialogModule, TranslatePipe], + templateUrl: './department-delete-form.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './department-delete-form.scss', +}) +export class DepartmentDeleteForm { + constructor( + private departmentService: DepartmentService, + private dialogRef: MatDialogRef, + private translate: TranslateService, + @Inject(MAT_DIALOG_DATA) + public data: { id: string; departmentName: string } + ) { + } + + protected deleteDepartment(): void { + this.departmentService + .deleteDepartment(this.data.id) + .subscribe({ + next: () => { + this.dialogRef.close(true); + }, + error: () => { + alert(this.translate.instant('department.delete-error')); + }, + }); + } +} diff --git a/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.html b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.html new file mode 100644 index 0000000..01aa3c1 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.html @@ -0,0 +1,29 @@ + + +
    + + + {{ 'features.name-label' | translate }} + + @if (form.controls.departmentName.hasError('required')) { + {{ 'shared.required-field' | translate }} + } + + + + {{ 'features.faculty-label' | translate }} + + @for (fac of faculties; track fac) { + + {{ fac.facultyName }} + + } + + @if (form.controls.facultyId.hasError('required')) { + {{ 'shared.required-field' | translate }} + } + + +
    + +
    diff --git a/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.scss b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.scss new file mode 100644 index 0000000..887baec --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.scss @@ -0,0 +1,13 @@ +mat-form-field { + width: 100%; +} + +mat-dialog-title { + font-size: 20px; + font-weight: bold; + color: #3f51b5; +} + +input { + border-radius: 10px; +} diff --git a/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.spec.ts b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.spec.ts new file mode 100644 index 0000000..38bfd09 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.spec.ts @@ -0,0 +1,18 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DepartmentEditForm } from './department-edit-form'; + +describe('DepartmentEditForm', () => { + let component: DepartmentEditForm; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentEditForm], + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentEditForm); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.ts b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.ts new file mode 100644 index 0000000..536a123 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-edit-form/department-edit-form.ts @@ -0,0 +1,97 @@ +import { Component, Inject, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { EditForm } from '../../../core/shared/edit-form/edit-form'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { DepartmentService } from '../department-service'; +import { FacultyService } from '../../faculty/faculty-service'; +import { FacultyElm } from '../../../core/interfaces/faculty-elm'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'app-department-edit-form', + imports: [ + EditForm, + MatDialogModule, + MatFormField, + MatLabel, + ReactiveFormsModule, + MatInputModule, + MatSelectModule, + MatOptionModule, + TranslatePipe, + ], + templateUrl: './department-edit-form.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './department-edit-form.scss', +}) +export class DepartmentEditForm implements OnInit { + protected readonly form: FormGroup<{ + departmentName: FormControl; + facultyId: FormControl; + }>; + + protected faculties: FacultyElm[] = []; + + constructor( + private dialogRef: MatDialogRef, + private departmentService: DepartmentService, + private facultyService: FacultyService, + private translate: TranslateService, + @Inject(MAT_DIALOG_DATA) + public data: { + id: string; + departmentName: string; + facultyId?: string; + } + ) { + this.form = new FormGroup({ + departmentName: new FormControl(data.departmentName ?? '', { + nonNullable: true, + validators: [Validators.required], + }), + facultyId: new FormControl(data.facultyId ?? '', { + nonNullable: true, + validators: [Validators.required], + }), + }); + } + + ngOnInit(): void { + this.facultyService.getFaculties().subscribe({ + next: (data) => (this.faculties = data), + error: (err) => console.error('Failed to load faculties', err), + }); + } + + protected save(): void { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + + const { departmentName, facultyId } = this.form.getRawValue(); + + this.departmentService + .editDepartment(this.data.id, { + departmentName: departmentName.trim(), + facultyId, + }) + .subscribe({ + next: () => this.dialogRef.close(true), + error: () => alert(this.translate.instant('department.update-error')), + }); + } +} diff --git a/uniplanWeb/src/app/features/department/department-filters/department-filters.html b/uniplanWeb/src/app/features/department/department-filters/department-filters.html new file mode 100644 index 0000000..d59383e --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-filters/department-filters.html @@ -0,0 +1,14 @@ +
    + + + + +
    diff --git a/uniplanWeb/src/app/features/department/department-filters/department-filters.scss b/uniplanWeb/src/app/features/department/department-filters/department-filters.scss new file mode 100644 index 0000000..bcba9e9 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-filters/department-filters.scss @@ -0,0 +1,14 @@ +.filters-panel { + display: flex; + flex-wrap: wrap; + gap: 1%; + justify-content: flex-end; + align-items: flex-end; + + height: auto; + padding: 0 10px; +} + +::ng-deep .mat-form-field-infix { + padding: 4px 0 !important; +} diff --git a/uniplanWeb/src/app/features/department/department-filters/department-filters.spec.ts b/uniplanWeb/src/app/features/department/department-filters/department-filters.spec.ts new file mode 100644 index 0000000..144ffeb --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-filters/department-filters.spec.ts @@ -0,0 +1,18 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DepartmentFilters } from './department-filters'; + +describe('DepartmentFilters', () => { + let component: DepartmentFilters; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentFilters], + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentFilters); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-filters/department-filters.ts b/uniplanWeb/src/app/features/department/department-filters/department-filters.ts new file mode 100644 index 0000000..c66ae38 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-filters/department-filters.ts @@ -0,0 +1,32 @@ +import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; +import { FiltersForm } from '../../../core/shared/filters-form/filters-form'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { InputFilter } from '../../../core/shared/input-filter/input-filter'; +import {TranslatePipe} from '@ngx-translate/core'; + +@Component({ + selector: 'app-department-filters', + imports: [ + FiltersForm, + MatFormFieldModule, + MatInputModule, + MatIconModule, + InputFilter, + TranslatePipe, + ], + templateUrl: './department-filters.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './department-filters.scss', +}) +export class DepartmentFilters { + readonly internalSearchText = input(''); + + readonly faculties = input<{ id: string; name: string }[]>([]); + + readonly selectedFaculty = input(''); + + readonly facultyChange = output(); + readonly searchTextChange = output(); +} diff --git a/uniplanWeb/src/app/features/department/department-options/department-options.html b/uniplanWeb/src/app/features/department/department-options/department-options.html new file mode 100644 index 0000000..1bb70bc --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-options/department-options.html @@ -0,0 +1,2 @@ + diff --git a/uniplanWeb/src/app/features/department/department-options/department-options.scss b/uniplanWeb/src/app/features/department/department-options/department-options.scss new file mode 100644 index 0000000..e170cab --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-options/department-options.scss @@ -0,0 +1,3 @@ +.add-button :hover { + background-color: #e3f2fd; +} diff --git a/uniplanWeb/src/app/features/department/department-options/department-options.spec.ts b/uniplanWeb/src/app/features/department/department-options/department-options.spec.ts new file mode 100644 index 0000000..e13c13a --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-options/department-options.spec.ts @@ -0,0 +1,18 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DepartmentOptions } from './department-options'; + +describe('DepartmentOptions', () => { + let component: DepartmentOptions; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentOptions], + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentOptions); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-options/department-options.ts b/uniplanWeb/src/app/features/department/department-options/department-options.ts new file mode 100644 index 0000000..ad086f1 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-options/department-options.ts @@ -0,0 +1,29 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DepartmentAddForm } from '../department-add-form/department-add-form'; +import { MatTableModule } from '@angular/material/table'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { AddButton } from '../../../core/shared/add-button/add-button'; + +@Component({ + selector: 'app-department-options', + imports: [ + MatTableModule, + MatIconModule, + MatButtonModule, + AddButton, + ], + templateUrl: './department-options.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './department-options.scss', +}) +export class DepartmentOptions { + constructor(private dialog: MatDialog) {} + + protected openAddForm(): void { + this.dialog.open(DepartmentAddForm, { + width: '400px', + }); + } +} diff --git a/uniplanWeb/src/app/features/department/department-panel/department-panel.html b/uniplanWeb/src/app/features/department/department-panel/department-panel.html new file mode 100644 index 0000000..466caeb --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-panel/department-panel.html @@ -0,0 +1,16 @@ +
    +
    + + + +
    + + +
    diff --git a/uniplanWeb/src/app/features/department/department-panel/department-panel.scss b/uniplanWeb/src/app/features/department/department-panel/department-panel.scss new file mode 100644 index 0000000..2d29f1b --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-panel/department-panel.scss @@ -0,0 +1,2 @@ +@use '../../../../styles/panel' as *; +@include panel-styles(); diff --git a/uniplanWeb/src/app/features/department/department-panel/department-panel.ts b/uniplanWeb/src/app/features/department/department-panel/department-panel.ts new file mode 100644 index 0000000..8237fd1 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-panel/department-panel.ts @@ -0,0 +1,65 @@ +import { Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { catchError, combineLatest, map, merge, of, switchMap } from 'rxjs'; + +import { DepartmentElm } from '../../../core/interfaces/department-elm'; +import { FacultyService } from '../../faculty/faculty-service'; +import { DepartmentOptions } from '../department-options/department-options'; +import { DepartmentFilters } from '../department-filters/department-filters'; +import { DepartmentTable } from '../department-table/department-table'; +import { DepartmentService } from '../department-service'; + +@Component({ + selector: 'app-department-panel', + imports: [DepartmentOptions, DepartmentFilters, DepartmentTable], + templateUrl: './department-panel.html', + styleUrl: './department-panel.scss', +}) +export class DepartmentPanel implements OnInit { + searchText: string = ''; + selectedFaculty: string = ''; + + faculties: { id: string; name: string }[] = []; + + private departments: DepartmentElm[] = []; + private facultyMap: Map = new Map(); + private destroyRef: DestroyRef = inject(DestroyRef); + + constructor( + private departmentService: DepartmentService, + private facultyService: FacultyService, + ) {} + + ngOnInit(): void { + const facultyMap$ = merge(of(undefined), this.facultyService.refreshNeeded).pipe( + switchMap(() => + this.facultyService.getFaculties().pipe( + map((faculties) => new Map(faculties.map((f) => [f.id, f.facultyName]))), + catchError(() => of(this.facultyMap)), + ), + ), + ); + + const departments$ = merge(of(undefined), this.departmentService.refreshNeeded).pipe( + switchMap(() => + this.departmentService.getDepartments().pipe( + catchError(() => of(this.departments)), + ), + ), + ); + + combineLatest([facultyMap$, departments$]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([facultyMap, departments]) => { + this.facultyMap = facultyMap; + this.departments = departments; + + const options = DepartmentTable.getFilterOptions(this.departments, this.facultyMap); + this.faculties = options.faculties; + + if (this.selectedFaculty && !this.faculties.some((f) => f.id === this.selectedFaculty)) { + this.selectedFaculty = ''; + } + }); + } +} diff --git a/uniplanWeb/src/app/features/department/department-service.spec.ts b/uniplanWeb/src/app/features/department/department-service.spec.ts new file mode 100644 index 0000000..6342a3c --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { DepartmentService } from './department-service'; + +describe('DepartmentService', () => { + let service: DepartmentService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DepartmentService); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-service.ts b/uniplanWeb/src/app/features/department/department-service.ts new file mode 100644 index 0000000..982d138 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-service.ts @@ -0,0 +1,60 @@ +import { HttpClient } from '@angular/common/http'; +import {inject, Injectable} from '@angular/core'; +import { map, Observable, Subject } from 'rxjs'; +import { DepartmentElm } from '../../core/interfaces/department-elm'; +import { API_ENDPOINTS } from '../../config/endpoints'; + +@Injectable({ + providedIn: 'root', +}) +export class DepartmentService { + refreshNeeded = new Subject(); + + private http = inject(HttpClient); + + getDepartments(): Observable { + return this.http.get(API_ENDPOINTS.departments).pipe( + map((departments) => + departments.map((dept, index) => ({ + id: dept.id, + departmentName: dept.departmentName, + facultyId: dept.facultyId, + position: index + 1, + })) + ) + ); + } + + createDepartment(data: { + departmentName: string; + facultyId: string; + }): Observable { + return this.http.post(`${API_ENDPOINTS.departments}`, data).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } + + editDepartment( + id: string, + data: { departmentName: string; facultyId: string } + ): Observable { + return this.http.put(`${API_ENDPOINTS.departments}/${id}`, data).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } + + deleteDepartment(id: string): Observable { + return this.http.delete(`${API_ENDPOINTS.departments}/${id}`).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } +} diff --git a/uniplanWeb/src/app/features/department/department-table/department-table.html b/uniplanWeb/src/app/features/department/department-table/department-table.html new file mode 100644 index 0000000..5a13a35 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-table/department-table.html @@ -0,0 +1,61 @@ +
    + + + + + + + + + + + + + + + + + + +
    {{ 'features.number-label' | translate }} + {{ 'features.name-label' | translate }} + {{ 'features.faculty-label' | translate }} + {{ 'features.actions-label' | translate }} +
    + +
    + + + + + + + + + + + + + + + + + + +
    + {{element.position}} + {{element.departmentName}} + {{ getFacultyName(element.facultyId) }} + + +
    +
    +
    diff --git a/uniplanWeb/src/app/features/department/department-table/department-table.scss b/uniplanWeb/src/app/features/department/department-table/department-table.scss new file mode 100644 index 0000000..6bafc2a --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-table/department-table.scss @@ -0,0 +1,74 @@ +.table-wrapper { + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + overflow: hidden; + background-color: #fff; +} + +.table-header { + width: 100%; + table-layout: fixed; +} + +.scrollable-body { + max-height: 510px; + overflow-y: auto; + overflow-x: hidden; +} + +@media (min-width: 900px) { + .scrollable-body { + max-height: 510px; + } +} + +@media (min-width: 1800px) { + .scrollable-body { + max-height: 680px; + } +} + +.table-body { + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} + +.table-body tr:hover td { + background-color: #f8f8f8; + cursor: pointer; +} + +th, +td { + text-align: center; + padding: 8px 10px; + word-wrap: break-word; +} + +th { + background-color: #f7f7f7 !important; +} + +td { + background-color: #fff; +} + +.col-position { + width: 80px; + border-right: 1px solid #e0e0e0; +} + +.col-name { + width: 300px; + border-right: 1px solid #e0e0e0; +} + +.col-faculty { + width: 350px; + border-right: 1px solid #e0e0e0; +} + +.col-actions { + width: 150px; +} diff --git a/uniplanWeb/src/app/features/department/department-table/department-table.spec.ts b/uniplanWeb/src/app/features/department/department-table/department-table.spec.ts new file mode 100644 index 0000000..a4b6aa9 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-table/department-table.spec.ts @@ -0,0 +1,18 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DepartmentTable } from './department-table'; + +describe('DepartmentTable', () => { + let component: DepartmentTable; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentTable], + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentTable); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/uniplanWeb/src/app/features/department/department-table/department-table.ts b/uniplanWeb/src/app/features/department/department-table/department-table.ts new file mode 100644 index 0000000..69b43c6 --- /dev/null +++ b/uniplanWeb/src/app/features/department/department-table/department-table.ts @@ -0,0 +1,105 @@ +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTableModule } from '@angular/material/table'; +import { DepartmentElm } from '../../../core/interfaces/department-elm'; +import { DepartmentEditForm } from '../department-edit-form/department-edit-form'; +import { MatDialog } from '@angular/material/dialog'; +import { DepartmentDeleteForm } from '../department-delete-form/department-delete-form'; +import { DepartmentService } from '../department-service'; +import { FacultyService } from '../../faculty/faculty-service'; +import {TranslatePipe} from '@ngx-translate/core'; +import {ChangeDetectionStrategy, Component, input, OnInit, signal} from '@angular/core'; + +@Component({ + selector: 'app-department-table', + imports: [MatTableModule, MatIconModule, MatButtonModule, TranslatePipe], + templateUrl: './department-table.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './department-table.scss', +}) +export class DepartmentTable implements OnInit { + displayedColumns: string[] = ['position', 'name', 'faculty', 'actions']; + + dataSource = signal([]); + facultyMap = signal(new Map()); + + readonly searchText = input(''); + readonly faculty = input(''); + + constructor( + private dialog: MatDialog, + private service: DepartmentService, + private facultyService: FacultyService + ) {} + + ngOnInit(): void { + this.loadDepartments(); + this.loadFaculties(); + + this.service.refreshNeeded.subscribe(() => { + this.loadDepartments(); + this.loadFaculties(); + }); + } + + private loadDepartments(): void { + this.service.getDepartments().subscribe((data) => { + this.dataSource.set(data); + }); + } + + private loadFaculties(): void { + this.facultyService.getFaculties().subscribe((faculties) => { + this.facultyMap.set(new Map(faculties.map((f) => [f.id, f.facultyName]))); + }); + } + + protected getFacultyName(id: string): string { + return this.facultyMap().get(id) || '—'; + } + + protected get filteredDepartments(): DepartmentElm[] { + const faculty = this.faculty(); + const searchText = this.searchText(); + + return this.dataSource().filter((dept) => { + const matchesFaculty = !faculty || dept.facultyId === faculty; + const matchesSearch = + !searchText || + dept.departmentName.toLowerCase().includes(searchText.toLowerCase()); + + return matchesFaculty && matchesSearch; + }); + } + + protected onEdit(element: DepartmentElm): void { + this.dialog.open(DepartmentEditForm, { + data: { + id: element.id, + departmentName: element.departmentName, + facultyId: element.facultyId, + }, + }); + } + + protected onDelete(element: DepartmentElm): void { + this.dialog.open(DepartmentDeleteForm, { + data: { + id: element.id, + departmentName: element.departmentName, + }, + }); + } + + static getFilterOptions( + data: DepartmentElm[], + facultyMap: Map, + ): { faculties: { id: string; name: string }[] } { + const faculties = [...new Set(data.map((e) => e.facultyId))].map((id) => ({ + id, + name: facultyMap.get(id) || '—', + })); + + return { faculties }; + } +} diff --git a/uniplanWeb/src/app/features/faculty/faculty-service.ts b/uniplanWeb/src/app/features/faculty/faculty-service.ts index 1c423f0..65fdb1a 100644 --- a/uniplanWeb/src/app/features/faculty/faculty-service.ts +++ b/uniplanWeb/src/app/features/faculty/faculty-service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { map, Observable, Subject } from 'rxjs'; import { FacultyElm } from '../../core/interfaces/faculty-elm'; -import {API_ENDPOINTS} from '../../config/endpoints'; +import { API_ENDPOINTS } from '../../config/endpoints'; @Injectable({ providedIn: 'root', diff --git a/uniplanWeb/src/app/features/major/major-service.ts b/uniplanWeb/src/app/features/major/major-service.ts index c7a4626..f65d6c4 100644 --- a/uniplanWeb/src/app/features/major/major-service.ts +++ b/uniplanWeb/src/app/features/major/major-service.ts @@ -2,8 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { map, Observable, Subject, switchMap } from 'rxjs'; import { MajorElm } from '../../core/interfaces/major-elm'; -import {environment} from '../../../environments/environment'; -import {API_ENDPOINTS} from '../../config/endpoints'; +import { API_ENDPOINTS } from '../../config/endpoints'; @Injectable({ providedIn: 'root',