From da9c47848d1ec21d3c30496d8405c041ff11c1fa Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Tue, 24 Feb 2026 12:25:37 +0100 Subject: [PATCH 01/42] fix postgres --- docker/docker-compose.dev.yaml | 2 +- docker/docker-compose.prod.yaml | 2 +- docker/docker-compose.staging.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index d1ced2f6..1572cd21 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -9,7 +9,7 @@ services: ports: - "${DB_PORT}:5432" volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql networks: - module-management-network restart: unless-stopped diff --git a/docker/docker-compose.prod.yaml b/docker/docker-compose.prod.yaml index 294f7cb9..d2712ffe 100644 --- a/docker/docker-compose.prod.yaml +++ b/docker/docker-compose.prod.yaml @@ -26,7 +26,7 @@ services: ports: - "${DB_PORT}:5432" volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql networks: - module-management-network restart: unless-stopped diff --git a/docker/docker-compose.staging.yaml b/docker/docker-compose.staging.yaml index 13ceced0..c62d682f 100644 --- a/docker/docker-compose.staging.yaml +++ b/docker/docker-compose.staging.yaml @@ -27,7 +27,7 @@ services: ports: - "${DB_PORT}:5432" volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql networks: - module-management-network restart: unless-stopped From ea0ecb156d645615e31415cddfad93086109ad4d Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 1 Mar 2026 17:15:12 +0100 Subject: [PATCH 02/42] managing degree programs and specializations --- Client/environments/environment.ts | 2 +- Client/src/app/app.routes.ts | 11 +- .../breadcrumb/breadcrumb-labels.service.ts | 14 + .../breadcrumb/breadcrumb.component.ts | 49 +- .../create-edit-base.component.html | 2 +- .../side-bar/side-bar.component.html | 8 + .../users-select/users-select.component.html | 26 + .../users-select/users-select.component.ts | 112 ++++ .../modules/openapi/.openapi-generator/FILES | 12 + .../src/app/core/modules/openapi/api/api.ts | 8 +- ...gram-specializations-controller.service.ts | 371 +++++++++++ ...ializations-controller.serviceInterface.ts | 55 ++ .../api/degree-programs-controller.service.ts | 580 ++++++++++++++++++ ...ee-programs-controller.serviceInterface.ts | 79 +++ ...d-specializations-to-degree-program-dto.ts | 15 + .../model/create-degree-program-dto.ts | 16 + ...reate-degree-program-specialization-dto.ts | 16 + .../openapi/model/degree-program-dto.ts | 20 + .../degree-program-specialization-dto.ts | 18 + .../app/core/modules/openapi/model/models.ts | 8 + .../openapi/model/responsible-user-dto.ts | 18 + .../model/update-degree-program-dto.ts | 16 + ...pdate-degree-program-specialization-dto.ts | 16 + .../pages/admin/admin-layout.component.html | 29 - .../app/pages/admin/admin-layout.component.ts | 18 - .../all-specializations-page.component.html | 63 ++ .../all-specializations-page.component.ts | 109 ++++ .../all-degree-programs-page.component.html | 63 ++ .../all-degree-programs-page.component.ts | 88 +++ ...degree-program-details-page.component.html | 109 ++++ .../degree-program-details-page.component.ts | 192 ++++++ ...mponent.html => users-page.component.html} | 5 +- ...e.component.ts => users-page.component.ts} | 22 +- .../feedback-view/feedback-view.component.ts | 15 +- .../src/app/pages/index/index.component.html | 4 + .../module-version-edit.component.ts | 14 +- .../module-version-view.component.ts | 18 +- .../proposal-view.component.html | 2 +- .../proposal-view/proposal-view.component.ts | 16 +- ...egreeProgramSpecializationsController.java | 45 ++ .../controllers/DegreeProgramsController.java | 64 ++ .../AddSpecializationsToDegreeProgramDTO.java | 19 + .../ls1/dtos/CreateDegreeProgramDTO.java | 15 + .../CreateDegreeProgramSpecializationDTO.java | 15 + .../ls1/dtos/DegreeProgramDTO.java | 34 + .../dtos/DegreeProgramSpecializationDTO.java | 25 + .../ls1/dtos/ResponsibleUserDTO.java | 23 + .../ls1/dtos/UpdateDegreeProgramDTO.java | 11 + .../UpdateDegreeProgramSpecializationDTO.java | 11 + .../ls1/models/DegreeProgram.java | 32 + .../models/DegreeProgramSpecialization.java | 30 + .../repositories/DegreeProgramRepository.java | 22 + ...DegreeProgramSpecializationRepository.java | 23 + .../ls1/services/AdminUserService.java | 1 - .../ls1/services/AuthenticationService.java | 2 - .../ls1/services/DegreeProgramService.java | 106 ++++ .../DegreeProgramSpecializationService.java | 60 ++ .../0008_degree_program_and_area_tables.yaml | 83 +++ .../main/resources/db/changelog/master.yaml | 3 + 59 files changed, 2776 insertions(+), 87 deletions(-) create mode 100644 Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts create mode 100644 Client/src/app/components/users-select/users-select.component.html create mode 100644 Client/src/app/components/users-select/users-select.component.ts create mode 100644 Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.service.ts create mode 100644 Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.serviceInterface.ts create mode 100644 Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts create mode 100644 Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts create mode 100644 Client/src/app/core/modules/openapi/model/add-specializations-to-degree-program-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/create-degree-program-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/create-degree-program-specialization-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/degree-program-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/degree-program-specialization-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/responsible-user-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/update-degree-program-specialization-dto.ts delete mode 100644 Client/src/app/pages/admin/admin-layout.component.html delete mode 100644 Client/src/app/pages/admin/admin-layout.component.ts create mode 100644 Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.html create mode 100644 Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.ts create mode 100644 Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.html create mode 100644 Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.ts create mode 100644 Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html create mode 100644 Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts rename Client/src/app/pages/admin/users/{admin-users-page.component.html => users-page.component.html} (93%) rename Client/src/app/pages/admin/users/{admin-users-page.component.ts => users-page.component.ts} (83%) create mode 100644 Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramSpecializationsController.java create mode 100644 Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/AddSpecializationsToDegreeProgramDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramSpecializationDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramSpecializationDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/ResponsibleUserDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramSpecializationDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java create mode 100644 Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java create mode 100644 Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java create mode 100644 Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java create mode 100644 Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java create mode 100644 Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java create mode 100644 Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml diff --git a/Client/environments/environment.ts b/Client/environments/environment.ts index 1001a4a4..d066a11b 100644 --- a/Client/environments/environment.ts +++ b/Client/environments/environment.ts @@ -3,7 +3,7 @@ export const environment = { redirect: 'https://module.aet.cit.tum.de', serverUrl: 'https://module.aet.cit.tum.de', keycloak: { - url: ' https://keycloak.ase.in.tum.de/', + url: 'https://keycloak.ase.in.tum.de', realm: 'tum', clientId: 'module-management' } diff --git a/Client/src/app/app.routes.ts b/Client/src/app/app.routes.ts index 64f2117e..fac7c6c4 100644 --- a/Client/src/app/app.routes.ts +++ b/Client/src/app/app.routes.ts @@ -13,8 +13,10 @@ import { SimilarModulesPage } from './pages/similar-modules/similar-modules.comp import { AccountLayoutComponent } from './pages/account-management/account-layout/account-layout.component'; import { AccountInformationComponent } from './pages/account-management/account-information/account-information.component'; import { AccountPasskeysComponent } from './pages/account-management/passkeys/account-passkeys.component'; -import { AdminUsersPageComponent } from './pages/admin/users/admin-users-page.component'; - +import { UsersPageComponent } from './pages/admin/users/users-page.component'; +import { AllDegreeProgramsPageComponent } from './pages/admin/degree-programs/all-degree-programs-page.component'; +import { DegreeProgramDetailsPageComponent } from './pages/admin/degree-programs/degree-program-details-page.component'; +import { AllSpecializationsPageComponent } from './pages/admin/degree-program-specializations/all-specializations-page.component'; export const routes: Routes = [ { path: '', component: IndexComponent }, { @@ -52,7 +54,10 @@ export const routes: Routes = [ path: 'admin', canActivate: [AuthGuard, AdminGuard], children: [ - { path: 'users', component: AdminUsersPageComponent }, + { path: 'users', component: UsersPageComponent }, + { path: 'degree-programs/specializations', component: AllSpecializationsPageComponent }, + { path: 'degree-programs/:id', component: DegreeProgramDetailsPageComponent }, + { path: 'degree-programs', component: AllDegreeProgramsPageComponent }, { path: '', redirectTo: 'users', pathMatch: 'full' } ] } diff --git a/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts b/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts new file mode 100644 index 00000000..071fb279 --- /dev/null +++ b/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts @@ -0,0 +1,14 @@ +import { Injectable, signal } from '@angular/core'; + +/** Segment-specific breadcrumb labels. Pages set the relevant signal when they load data and set to null on destroy. */ +@Injectable({ providedIn: 'root' }) +export class BreadcrumbLabelsService { + /** Degree program details page: program name. */ + readonly degreeProgramName = signal(null); + /** Proposal/view segment: module title (e.g. from latestModuleVersion.titleEng). */ + readonly proposalTitle = signal(null); + /** Version segment: e.g. "Version 2" from moduleVersion.version. */ + readonly versionLabel = signal(null); + /** Feedback view segment: e.g. module title from the feedback's module version. */ + readonly feedbackLabel = signal(null); +} diff --git a/Client/src/app/components/breadcrumb/breadcrumb.component.ts b/Client/src/app/components/breadcrumb/breadcrumb.component.ts index bfe558dc..50e4b2f2 100644 --- a/Client/src/app/components/breadcrumb/breadcrumb.component.ts +++ b/Client/src/app/components/breadcrumb/breadcrumb.component.ts @@ -3,7 +3,7 @@ import { Router, RouterModule, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs/operators'; import { BreadcrumbModule } from 'primeng/breadcrumb'; import type { MenuItem } from 'primeng/api'; -import { SecurityStore } from '../../core/security/security-store.service'; +import { BreadcrumbLabelsService } from './breadcrumb-labels.service'; @Component({ selector: 'app-breadcrumb', @@ -13,7 +13,7 @@ import { SecurityStore } from '../../core/security/security-store.service'; }) export class BreadcrumbComponent { private router = inject(Router); - private securityStore = inject(SecurityStore); + private breadcrumbLabels = inject(BreadcrumbLabelsService); private url = signal(this.router.url); @@ -27,15 +27,49 @@ export class BreadcrumbComponent { showBreadcrumb = computed(() => { const u = this.url(); - return u.startsWith('/proposals') || u.startsWith('/feedbacks'); + return u.startsWith('/proposals') || u.startsWith('/feedbacks') || u.startsWith('/admin'); }); private buildItems(url: string): MenuItem[] { if (url.startsWith('/proposals')) return this.buildProposalItems(url); if (url.startsWith('/feedbacks')) return this.buildFeedbackItems(url); + if (url.startsWith('/admin')) return this.buildAdminItems(url); return []; } + private buildAdminItems(url: string): MenuItem[] { + const segments = url.split('/').filter(Boolean); // ['admin', 'degree-programs', ...] + const items: MenuItem[] = []; + + if (segments.length < 2) return items; + + if (segments[1] === 'users') { + items.push({ label: 'Users', routerLink: ['/admin/users'] }); + return items; + } + + if (segments[1] === 'degree-programs') { + items.push({ label: 'Degree Programs', routerLink: ['/admin/degree-programs'] }); + if (segments.length <= 2) return items; + + if (segments[2] === 'specializations') { + items.push({ label: 'All areas of specializations', routerLink: ['/admin/degree-programs/specializations'] }); + return items; + } + + // degree-programs/:id (program details page) – label from details page when it loads the program + const programId = segments[2]; + const name = (this.breadcrumbLabels.degreeProgramName() ?? '').trim() || `Program ${programId}`; + items.push({ + label: name, + routerLink: ['/admin/degree-programs', programId] + }); + return items; + } + + return items; + } + private buildProposalItems(url: string): MenuItem[] { const segments = url.split('/').filter(Boolean); // ['proposals', ...] const items: MenuItem[] = []; @@ -53,8 +87,9 @@ export class BreadcrumbComponent { if (segments[1] === 'view' && segments[2]) { const proposalId = segments[2]; + const proposalLabel = (this.breadcrumbLabels.proposalTitle() ?? '').trim() || `Proposal ${proposalId}`; items.push({ - label: 'Proposal ' + proposalId, + label: proposalLabel, routerLink: ['/proposals/view', proposalId] }); @@ -62,8 +97,9 @@ export class BreadcrumbComponent { if (segments[3] === 'version' && segments[4]) { const versionId = segments[4]; + const versionSegmentLabel = (this.breadcrumbLabels.versionLabel() ?? '').trim() || `Version ${versionId}`; items.push({ - label: 'Version ' + versionId, + label: versionSegmentLabel, routerLink: ['/proposals/view', proposalId, 'version', versionId] }); @@ -97,7 +133,8 @@ export class BreadcrumbComponent { } if (segments[1] === 'view' && segments[2]) { - items.push({ label: 'Feedback ' + segments[2], routerLink: ['/feedbacks/view', segments[2]] }); + const label = (this.breadcrumbLabels.feedbackLabel() ?? '').trim() || `Feedback ${segments[2]}`; + items.push({ label, routerLink: ['/feedbacks/view', segments[2]] }); } if (segments[3] === 'overlap' && segments[4]) { diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index dc4fcac5..d89c2ef4 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -511,7 +511,7 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '
Cancel - + {{ loading() ? 'Saving...' : moduleVersionDto() ? 'Update Proposal' : 'Create Proposal' }}
diff --git a/Client/src/app/components/side-bar/side-bar.component.html b/Client/src/app/components/side-bar/side-bar.component.html index e6fb7ffc..f7007f4f 100644 --- a/Client/src/app/components/side-bar/side-bar.component.html +++ b/Client/src/app/components/side-bar/side-bar.component.html @@ -9,6 +9,14 @@ [style]="{ width: '100%' }" styleClass="sidebar-btn justify-start" /> + } @if (isProfessor()) { + + @if (loading()) { +
+ +
+ } +
+ diff --git a/Client/src/app/components/users-select/users-select.component.ts b/Client/src/app/components/users-select/users-select.component.ts new file mode 100644 index 00000000..3630113f --- /dev/null +++ b/Client/src/app/components/users-select/users-select.component.ts @@ -0,0 +1,112 @@ +import { Component, inject, signal, computed, input } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { SelectModule, SelectLazyLoadEvent } from 'primeng/select'; +import { Subject, debounceTime, distinctUntilChanged, firstValueFrom } from 'rxjs'; +import { AdminUserControllerService } from '../../core/modules/openapi'; +import type { UserDTO } from '../../core/modules/openapi/model/user-dto'; +import type { ResponsibleUserDTO } from '../../core/modules/openapi/model/responsible-user-dto'; + +const USER_PAGE_SIZE = 20; +const FILTER_DEBOUNCE_MS = 500; + +@Component({ + selector: 'app-users-select', + standalone: true, + imports: [FormsModule, ProgressSpinnerModule, SelectModule], + templateUrl: './users-select.component.html', + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: UsersSelectComponent, multi: true }] +}) +export class UsersSelectComponent implements ControlValueAccessor { + private readonly adminUserControllerService = inject(AdminUserControllerService); + + private readonly filter$ = new Subject(); + + styleClass = input(''); + /** When the selected user is not yet in the loaded list, pass this so they are displayed like other options. */ + selectedUser = input(null); + + constructor() { + this.loadUsers(); + this.filter$.pipe(debounceTime(FILTER_DEBOUNCE_MS), distinctUntilChanged()).subscribe((term) => { + this.searchTerm.set(term); + this.loadUsers(0); + }); + } + + value = signal(null); + users = signal([]); + totalRecords = signal(0); + loading = signal(false); + searchTerm = signal(''); + + private onTouched = () => {}; + private onChange: (v: string | null) => void = () => {}; + + formatUserLabel(u: { firstName?: string; lastName?: string; email?: string; userId?: string }): string { + return `${u.firstName ?? ''} ${u.lastName ?? ''} (${u.email ?? u.userId ?? ''})`.trim() || String(u.userId ?? ''); + } + + userOptions = computed(() => { + const list = this.users().map((u) => ({ + label: this.formatUserLabel(u), + value: u.userId + })); + const current = this.value(); + if (current && !list.some((o) => o.value === current)) { + const u = this.selectedUser(); + const label = u ? this.formatUserLabel(u) : current; + return [{ label, value: current }, ...list]; + } + return list; + }); + + writeValue(v: string | null): void { + this.value.set(v); + } + + registerOnChange(fn: (v: string | null) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + onValueChange(v: string | null): void { + this.value.set(v); + this.onChange(v); + this.onTouched(); + } + + async loadUsers(page = 0) { + if (this.loading()) return; + this.loading.set(true); + try { + const res = await firstValueFrom(this.adminUserControllerService.getUsers(page, USER_PAGE_SIZE, this.searchTerm().trim() || undefined)); + this.totalRecords.set(res.totalElements ?? 0); + if (page === 0) { + this.users.set(res.content ?? []); + } else { + this.users.update((prev) => [...prev, ...(res.content ?? [])]); + } + } catch (e) { + this.users.set([]); + this.totalRecords.set(0); + } finally { + this.loading.set(false); + } + } + + async onLazyLoad(event: SelectLazyLoadEvent): Promise { + if (this.totalRecords() > 0 && this.users().length >= this.totalRecords()) return; + if (event.last < this.users().length) return; + + const page = Math.floor(this.users().length / USER_PAGE_SIZE); + await this.loadUsers(page); + } + + onFilter(event: { filter: string }): void { + this.filter$.next(event.filter ?? ''); + } +} diff --git a/Client/src/app/core/modules/openapi/.openapi-generator/FILES b/Client/src/app/core/modules/openapi/.openapi-generator/FILES index b45a5429..da40739c 100644 --- a/Client/src/app/core/modules/openapi/.openapi-generator/FILES +++ b/Client/src/app/core/modules/openapi/.openapi-generator/FILES @@ -5,6 +5,10 @@ api.module.ts api/admin-user-controller.service.ts api/admin-user-controller.serviceInterface.ts api/api.ts +api/degree-program-specializations-controller.service.ts +api/degree-program-specializations-controller.serviceInterface.ts +api/degree-programs-controller.service.ts +api/degree-programs-controller.serviceInterface.ts api/feedback-controller.service.ts api/feedback-controller.serviceInterface.ts api/module-version-controller.service.ts @@ -18,8 +22,13 @@ encoder.ts git_push.sh index.ts model/add-module-version-dto.ts +model/add-specializations-to-degree-program-dto.ts model/completion-service-request-dto.ts model/completion-service-response-dto.ts +model/create-degree-program-dto.ts +model/create-degree-program-specialization-dto.ts +model/degree-program-dto.ts +model/degree-program-specialization-dto.ts model/feedback-dto.ts model/feedback-list-item-dto.ts model/feedback.ts @@ -36,7 +45,10 @@ model/proposal-request-dto.ts model/proposal-view-dto.ts model/proposal.ts model/proposals-compact-dto.ts +model/responsible-user-dto.ts model/similar-module-dto.ts +model/update-degree-program-dto.ts +model/update-degree-program-specialization-dto.ts model/update-user-role-dto.ts model/user-dto.ts model/user.ts diff --git a/Client/src/app/core/modules/openapi/api/api.ts b/Client/src/app/core/modules/openapi/api/api.ts index e3480da6..80159f7c 100644 --- a/Client/src/app/core/modules/openapi/api/api.ts +++ b/Client/src/app/core/modules/openapi/api/api.ts @@ -1,6 +1,12 @@ export * from './admin-user-controller.service'; import { AdminUserControllerService } from './admin-user-controller.service'; export * from './admin-user-controller.serviceInterface'; +export * from './degree-program-specializations-controller.service'; +import { DegreeProgramSpecializationsControllerService } from './degree-program-specializations-controller.service'; +export * from './degree-program-specializations-controller.serviceInterface'; +export * from './degree-programs-controller.service'; +import { DegreeProgramsControllerService } from './degree-programs-controller.service'; +export * from './degree-programs-controller.serviceInterface'; export * from './feedback-controller.service'; import { FeedbackControllerService } from './feedback-controller.service'; export * from './feedback-controller.serviceInterface'; @@ -13,4 +19,4 @@ export * from './proposal-controller.serviceInterface'; export * from './user-controller.service'; import { UserControllerService } from './user-controller.service'; export * from './user-controller.serviceInterface'; -export const APIS = [AdminUserControllerService, FeedbackControllerService, ModuleVersionControllerService, ProposalControllerService, UserControllerService]; +export const APIS = [AdminUserControllerService, DegreeProgramSpecializationsControllerService, DegreeProgramsControllerService, FeedbackControllerService, ModuleVersionControllerService, ProposalControllerService, UserControllerService]; diff --git a/Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.service.ts b/Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.service.ts new file mode 100644 index 00000000..babd8939 --- /dev/null +++ b/Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.service.ts @@ -0,0 +1,371 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { CreateDegreeProgramSpecializationDTO } from '../model/create-degree-program-specialization-dto'; +// @ts-ignore +import { DegreeProgramSpecializationDTO } from '../model/degree-program-specialization-dto'; +// @ts-ignore +import { UpdateDegreeProgramSpecializationDTO } from '../model/update-degree-program-specialization-dto'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + DegreeProgramSpecializationsControllerServiceInterface +} from './degree-program-specializations-controller.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class DegreeProgramSpecializationsControllerService implements DegreeProgramSpecializationsControllerServiceInterface { + + protected basePath = 'http://localhost:8080'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param createDegreeProgramSpecializationDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createDegreeProgramSpecialization(createDegreeProgramSpecializationDTO: CreateDegreeProgramSpecializationDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createDegreeProgramSpecialization(createDegreeProgramSpecializationDTO: CreateDegreeProgramSpecializationDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createDegreeProgramSpecialization(createDegreeProgramSpecializationDTO: CreateDegreeProgramSpecializationDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createDegreeProgramSpecialization(createDegreeProgramSpecializationDTO: CreateDegreeProgramSpecializationDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (createDegreeProgramSpecializationDTO === null || createDegreeProgramSpecializationDTO === undefined) { + throw new Error('Required parameter createDegreeProgramSpecializationDTO was null or undefined when calling createDegreeProgramSpecialization.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-program-specializations`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: createDegreeProgramSpecializationDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteDegreeProgramSpecialization(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public deleteDegreeProgramSpecialization(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteDegreeProgramSpecialization(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteDegreeProgramSpecialization(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteDegreeProgramSpecialization.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-program-specializations/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getAllDegreeProgramSpecializations(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAllDegreeProgramSpecializations(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllDegreeProgramSpecializations(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllDegreeProgramSpecializations(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-program-specializations`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param updateDegreeProgramSpecializationDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateDegreeProgramSpecialization(id: number, updateDegreeProgramSpecializationDTO: UpdateDegreeProgramSpecializationDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateDegreeProgramSpecialization(id: number, updateDegreeProgramSpecializationDTO: UpdateDegreeProgramSpecializationDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateDegreeProgramSpecialization(id: number, updateDegreeProgramSpecializationDTO: UpdateDegreeProgramSpecializationDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateDegreeProgramSpecialization(id: number, updateDegreeProgramSpecializationDTO: UpdateDegreeProgramSpecializationDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateDegreeProgramSpecialization.'); + } + if (updateDegreeProgramSpecializationDTO === null || updateDegreeProgramSpecializationDTO === undefined) { + throw new Error('Required parameter updateDegreeProgramSpecializationDTO was null or undefined when calling updateDegreeProgramSpecialization.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-program-specializations/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('patch', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: updateDegreeProgramSpecializationDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.serviceInterface.ts new file mode 100644 index 00000000..3411aa50 --- /dev/null +++ b/Client/src/app/core/modules/openapi/api/degree-program-specializations-controller.serviceInterface.ts @@ -0,0 +1,55 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { CreateDegreeProgramSpecializationDTO } from '../model/models'; +import { DegreeProgramSpecializationDTO } from '../model/models'; +import { UpdateDegreeProgramSpecializationDTO } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface DegreeProgramSpecializationsControllerServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + * @param createDegreeProgramSpecializationDTO + */ + createDegreeProgramSpecialization(createDegreeProgramSpecializationDTO: CreateDegreeProgramSpecializationDTO, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param id + */ + deleteDegreeProgramSpecialization(id: number, extraHttpRequestParams?: any): Observable<{}>; + + /** + * + * + */ + getAllDegreeProgramSpecializations(extraHttpRequestParams?: any): Observable>; + + /** + * + * + * @param id + * @param updateDegreeProgramSpecializationDTO + */ + updateDegreeProgramSpecialization(id: number, updateDegreeProgramSpecializationDTO: UpdateDegreeProgramSpecializationDTO, extraHttpRequestParams?: any): Observable; + +} diff --git a/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts b/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts new file mode 100644 index 00000000..01150472 --- /dev/null +++ b/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts @@ -0,0 +1,580 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { AddSpecializationsToDegreeProgramDTO } from '../model/add-specializations-to-degree-program-dto'; +// @ts-ignore +import { CreateDegreeProgramDTO } from '../model/create-degree-program-dto'; +// @ts-ignore +import { DegreeProgramDTO } from '../model/degree-program-dto'; +// @ts-ignore +import { UpdateDegreeProgramDTO } from '../model/update-degree-program-dto'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + DegreeProgramsControllerServiceInterface +} from './degree-programs-controller.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class DegreeProgramsControllerService implements DegreeProgramsControllerServiceInterface { + + protected basePath = 'http://localhost:8080'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param degreeProgramId + * @param addSpecializationsToDegreeProgramDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public addSpecializationsToDegreeProgram(degreeProgramId: number, addSpecializationsToDegreeProgramDTO: AddSpecializationsToDegreeProgramDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public addSpecializationsToDegreeProgram(degreeProgramId: number, addSpecializationsToDegreeProgramDTO: AddSpecializationsToDegreeProgramDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public addSpecializationsToDegreeProgram(degreeProgramId: number, addSpecializationsToDegreeProgramDTO: AddSpecializationsToDegreeProgramDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public addSpecializationsToDegreeProgram(degreeProgramId: number, addSpecializationsToDegreeProgramDTO: AddSpecializationsToDegreeProgramDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (degreeProgramId === null || degreeProgramId === undefined) { + throw new Error('Required parameter degreeProgramId was null or undefined when calling addSpecializationsToDegreeProgram.'); + } + if (addSpecializationsToDegreeProgramDTO === null || addSpecializationsToDegreeProgramDTO === undefined) { + throw new Error('Required parameter addSpecializationsToDegreeProgramDTO was null or undefined when calling addSpecializationsToDegreeProgram.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs/${this.configuration.encodeParam({name: "degreeProgramId", value: degreeProgramId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/degree-program-specializations/batch`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: addSpecializationsToDegreeProgramDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param createDegreeProgramDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createDegreeProgram(createDegreeProgramDTO: CreateDegreeProgramDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createDegreeProgram(createDegreeProgramDTO: CreateDegreeProgramDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createDegreeProgram(createDegreeProgramDTO: CreateDegreeProgramDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createDegreeProgram(createDegreeProgramDTO: CreateDegreeProgramDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (createDegreeProgramDTO === null || createDegreeProgramDTO === undefined) { + throw new Error('Required parameter createDegreeProgramDTO was null or undefined when calling createDegreeProgram.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: createDegreeProgramDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteDegreeProgram(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public deleteDegreeProgram(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteDegreeProgram(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteDegreeProgram(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteDegreeProgram.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getAllDegreePrograms(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAllDegreePrograms(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllDegreePrograms(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllDegreePrograms(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getDegreeProgram(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getDegreeProgram(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getDegreeProgram(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getDegreeProgram(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getDegreeProgram.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param degreeProgramId + * @param degreeProgramSpecializationId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public removeSpecializationFromDegreeProgram(degreeProgramId: number, degreeProgramSpecializationId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public removeSpecializationFromDegreeProgram(degreeProgramId: number, degreeProgramSpecializationId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public removeSpecializationFromDegreeProgram(degreeProgramId: number, degreeProgramSpecializationId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public removeSpecializationFromDegreeProgram(degreeProgramId: number, degreeProgramSpecializationId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (degreeProgramId === null || degreeProgramId === undefined) { + throw new Error('Required parameter degreeProgramId was null or undefined when calling removeSpecializationFromDegreeProgram.'); + } + if (degreeProgramSpecializationId === null || degreeProgramSpecializationId === undefined) { + throw new Error('Required parameter degreeProgramSpecializationId was null or undefined when calling removeSpecializationFromDegreeProgram.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs/${this.configuration.encodeParam({name: "degreeProgramId", value: degreeProgramId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/degree-program-specializations/${this.configuration.encodeParam({name: "degreeProgramSpecializationId", value: degreeProgramSpecializationId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param updateDegreeProgramDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateDegreeProgram(id: number, updateDegreeProgramDTO: UpdateDegreeProgramDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateDegreeProgram(id: number, updateDegreeProgramDTO: UpdateDegreeProgramDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateDegreeProgram(id: number, updateDegreeProgramDTO: UpdateDegreeProgramDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateDegreeProgram(id: number, updateDegreeProgramDTO: UpdateDegreeProgramDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateDegreeProgram.'); + } + if (updateDegreeProgramDTO === null || updateDegreeProgramDTO === undefined) { + throw new Error('Required parameter updateDegreeProgramDTO was null or undefined when calling updateDegreeProgram.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('patch', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: updateDegreeProgramDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts new file mode 100644 index 00000000..70fe8197 --- /dev/null +++ b/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts @@ -0,0 +1,79 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { AddSpecializationsToDegreeProgramDTO } from '../model/models'; +import { CreateDegreeProgramDTO } from '../model/models'; +import { DegreeProgramDTO } from '../model/models'; +import { UpdateDegreeProgramDTO } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface DegreeProgramsControllerServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + * @param degreeProgramId + * @param addSpecializationsToDegreeProgramDTO + */ + addSpecializationsToDegreeProgram(degreeProgramId: number, addSpecializationsToDegreeProgramDTO: AddSpecializationsToDegreeProgramDTO, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param createDegreeProgramDTO + */ + createDegreeProgram(createDegreeProgramDTO: CreateDegreeProgramDTO, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param id + */ + deleteDegreeProgram(id: number, extraHttpRequestParams?: any): Observable<{}>; + + /** + * + * + */ + getAllDegreePrograms(extraHttpRequestParams?: any): Observable>; + + /** + * + * + * @param id + */ + getDegreeProgram(id: number, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param degreeProgramId + * @param degreeProgramSpecializationId + */ + removeSpecializationFromDegreeProgram(degreeProgramId: number, degreeProgramSpecializationId: number, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param id + * @param updateDegreeProgramDTO + */ + updateDegreeProgram(id: number, updateDegreeProgramDTO: UpdateDegreeProgramDTO, extraHttpRequestParams?: any): Observable; + +} diff --git a/Client/src/app/core/modules/openapi/model/add-specializations-to-degree-program-dto.ts b/Client/src/app/core/modules/openapi/model/add-specializations-to-degree-program-dto.ts new file mode 100644 index 00000000..b0535b59 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/add-specializations-to-degree-program-dto.ts @@ -0,0 +1,15 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface AddSpecializationsToDegreeProgramDTO { + degreeProgramSpecializationIds: Array; +} + diff --git a/Client/src/app/core/modules/openapi/model/create-degree-program-dto.ts b/Client/src/app/core/modules/openapi/model/create-degree-program-dto.ts new file mode 100644 index 00000000..da13df1f --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/create-degree-program-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface CreateDegreeProgramDTO { + name: string; + responsibleUserId: string; +} + diff --git a/Client/src/app/core/modules/openapi/model/create-degree-program-specialization-dto.ts b/Client/src/app/core/modules/openapi/model/create-degree-program-specialization-dto.ts new file mode 100644 index 00000000..ac2b92bd --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/create-degree-program-specialization-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface CreateDegreeProgramSpecializationDTO { + name: string; + responsibleUserId: string; +} + diff --git a/Client/src/app/core/modules/openapi/model/degree-program-dto.ts b/Client/src/app/core/modules/openapi/model/degree-program-dto.ts new file mode 100644 index 00000000..e05d448f --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/degree-program-dto.ts @@ -0,0 +1,20 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { DegreeProgramSpecializationDTO } from './degree-program-specialization-dto'; +import { ResponsibleUserDTO } from './responsible-user-dto'; + + +export interface DegreeProgramDTO { + degreeProgramId: number; + name: string; + responsibleUser: ResponsibleUserDTO; + degreeProgramSpecializations?: Array; +} + diff --git a/Client/src/app/core/modules/openapi/model/degree-program-specialization-dto.ts b/Client/src/app/core/modules/openapi/model/degree-program-specialization-dto.ts new file mode 100644 index 00000000..01fd09af --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/degree-program-specialization-dto.ts @@ -0,0 +1,18 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { ResponsibleUserDTO } from './responsible-user-dto'; + + +export interface DegreeProgramSpecializationDTO { + degreeProgramSpecializationId: number; + name: string; + responsibleUser: ResponsibleUserDTO; +} + diff --git a/Client/src/app/core/modules/openapi/model/models.ts b/Client/src/app/core/modules/openapi/model/models.ts index 45fb6363..0ca448a1 100644 --- a/Client/src/app/core/modules/openapi/model/models.ts +++ b/Client/src/app/core/modules/openapi/model/models.ts @@ -1,6 +1,11 @@ export * from './add-module-version-dto'; +export * from './add-specializations-to-degree-program-dto'; export * from './completion-service-request-dto'; export * from './completion-service-response-dto'; +export * from './create-degree-program-dto'; +export * from './create-degree-program-specialization-dto'; +export * from './degree-program-dto'; +export * from './degree-program-specialization-dto'; export * from './feedback'; export * from './feedback-dto'; export * from './feedback-list-item-dto'; @@ -16,7 +21,10 @@ export * from './proposal'; export * from './proposal-request-dto'; export * from './proposal-view-dto'; export * from './proposals-compact-dto'; +export * from './responsible-user-dto'; export * from './similar-module-dto'; +export * from './update-degree-program-dto'; +export * from './update-degree-program-specialization-dto'; export * from './update-user-role-dto'; export * from './user'; export * from './user-dto'; diff --git a/Client/src/app/core/modules/openapi/model/responsible-user-dto.ts b/Client/src/app/core/modules/openapi/model/responsible-user-dto.ts new file mode 100644 index 00000000..155263f6 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/responsible-user-dto.ts @@ -0,0 +1,18 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ResponsibleUserDTO { + userId?: string; + firstName?: string; + lastName?: string; + email?: string; +} + diff --git a/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts b/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts new file mode 100644 index 00000000..34094e90 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface UpdateDegreeProgramDTO { + name?: string; + responsibleUserId?: string; +} + diff --git a/Client/src/app/core/modules/openapi/model/update-degree-program-specialization-dto.ts b/Client/src/app/core/modules/openapi/model/update-degree-program-specialization-dto.ts new file mode 100644 index 00000000..74f859bb --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/update-degree-program-specialization-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface UpdateDegreeProgramSpecializationDTO { + name?: string; + responsibleUserId?: string; +} + diff --git a/Client/src/app/pages/admin/admin-layout.component.html b/Client/src/app/pages/admin/admin-layout.component.html deleted file mode 100644 index 98358abe..00000000 --- a/Client/src/app/pages/admin/admin-layout.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-

Admin Panel

- -
- -
- -
- - - -
-
-
- - -
- -
-
-
diff --git a/Client/src/app/pages/admin/admin-layout.component.ts b/Client/src/app/pages/admin/admin-layout.component.ts deleted file mode 100644 index e00e40d6..00000000 --- a/Client/src/app/pages/admin/admin-layout.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { Router, RouterModule } from '@angular/router'; -import { PanelModule } from 'primeng/panel'; -import { ButtonModule } from 'primeng/button'; - -@Component({ - selector: 'app-admin-layout', - standalone: true, - imports: [PanelModule, ButtonModule, RouterModule], - templateUrl: './admin-layout.component.html' -}) -export class AdminLayoutComponent { - private router = inject(Router); - - isActive(path: string): boolean { - return this.router.url.startsWith(path); - } -} diff --git a/Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.html b/Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.html new file mode 100644 index 00000000..6fe0ae13 --- /dev/null +++ b/Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.html @@ -0,0 +1,63 @@ + + +

All specializations

+

Create and manage the full list of areas of specializations for all degree programs. They can be assigned to any degree program in each program's page.

+ +
+ +
+ + + + + Name + Responsible user + Actions + + + + + {{ spec.name }} + {{ userLabel(spec) }} + + + + + + + + + No specializations yet. Add one to get started. + + + + + +
+
+ + +
+
+ + +
+
+ + + + +
diff --git a/Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.ts b/Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.ts new file mode 100644 index 00000000..a6922f9d --- /dev/null +++ b/Client/src/app/pages/admin/degree-program-specializations/all-specializations-page.component.ts @@ -0,0 +1,109 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TableModule } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { DialogModule } from 'primeng/dialog'; +import { MessageService } from 'primeng/api'; +import { ToastModule } from 'primeng/toast'; +import { firstValueFrom } from 'rxjs'; +import { + DegreeProgramSpecializationsControllerService, + type CreateDegreeProgramSpecializationDTO, + type DegreeProgramSpecializationDTO, + type UpdateDegreeProgramSpecializationDTO +} from '../../../core/modules/openapi'; +import { UsersSelectComponent } from '../../../components/users-select/users-select.component'; + +@Component({ + selector: 'app-all-specializations-page', + standalone: true, + imports: [FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, ToastModule, UsersSelectComponent], + templateUrl: './all-specializations-page.component.html' +}) +export class AllSpecializationsPageComponent { + private readonly specializationService = inject(DegreeProgramSpecializationsControllerService); + private readonly messageService = inject(MessageService); + + specializations = signal([]); + loading = signal(false); + dialogVisible = signal(false); + selectedSpecialization = signal(null); + editMode = signal(false); + + name = signal(''); + responsibleUserId = signal(null); + + constructor() { + this.loadSpecializations(); + } + + async loadSpecializations() { + this.loading.set(true); + try { + const list = await firstValueFrom(this.specializationService.getAllDegreeProgramSpecializations()); + this.specializations.set(list ?? []); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to load degree program specializations.' }); + this.specializations.set([]); + } finally { + this.loading.set(false); + } + } + + userLabel(spec: DegreeProgramSpecializationDTO): string { + const u = spec?.responsibleUser; + return ([u.firstName, u.lastName].filter(Boolean).join(' ').trim() || u.email) ?? '—'; + } + + openCreate() { + this.editMode.set(false); + this.selectedSpecialization.set(null); + this.name.set(''); + this.responsibleUserId.set(null); + this.dialogVisible.set(true); + } + + openEdit(spec: DegreeProgramSpecializationDTO) { + this.editMode.set(true); + this.selectedSpecialization.set(spec); + this.name.set(spec.name); + this.responsibleUserId.set(spec.responsibleUser?.userId ?? null); + this.dialogVisible.set(true); + } + + async saveSpecialization() { + const id = this.selectedSpecialization()?.degreeProgramSpecializationId; + const nameVal = this.name().trim(); + const userIdVal = this.responsibleUserId(); + if (!nameVal || !userIdVal) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Name and responsible user are required.' }); + return; + } + const dto: CreateDegreeProgramSpecializationDTO & UpdateDegreeProgramSpecializationDTO = { name: nameVal, responsibleUserId: userIdVal }; + try { + if (this.editMode() && id != null) { + await firstValueFrom(this.specializationService.updateDegreeProgramSpecialization(id, dto)); + this.messageService.add({ severity: 'success', summary: 'Updated', detail: 'Degree program specialization updated.' }); + } else { + await firstValueFrom(this.specializationService.createDegreeProgramSpecialization(dto)); + this.messageService.add({ severity: 'success', summary: 'Created', detail: 'Degree program specialization created.' }); + } + this.dialogVisible.set(false); + await this.loadSpecializations(); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save degree program specialization.' }); + } + } + + async deleteSpecialization(spec: DegreeProgramSpecializationDTO) { + if (!confirm(`Delete "${spec.name}"? It will be removed from all degree programs that use it.`)) return; + try { + await firstValueFrom(this.specializationService.deleteDegreeProgramSpecialization(spec.degreeProgramSpecializationId)); + this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Degree program specialization deleted.' }); + await this.loadSpecializations(); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to delete degree program specialization.' }); + } + } +} diff --git a/Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.html b/Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.html new file mode 100644 index 00000000..0de707a2 --- /dev/null +++ b/Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.html @@ -0,0 +1,63 @@ + + +

Degree Programs

+

Create and manage degree programs. Assign areas of specializations to each program from the program's page.

+ +
+ + + + +
+ + + + + Name + Responsible user + Actions + + + + + {{ program.name }} + {{ userLabel(program) }} + + + + + + + + + + + No degree programs yet. Add one to get started. + + + + + +
+
+ + +
+
+ + +
+
+ + + + +
diff --git a/Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.ts b/Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.ts new file mode 100644 index 00000000..623e5763 --- /dev/null +++ b/Client/src/app/pages/admin/degree-programs/all-degree-programs-page.component.ts @@ -0,0 +1,88 @@ +import { Component, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { TableModule } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { DialogModule } from 'primeng/dialog'; +import { TooltipModule } from 'primeng/tooltip'; +import { MessageService } from 'primeng/api'; +import { ToastModule } from 'primeng/toast'; +import { firstValueFrom } from 'rxjs'; +import { DegreeProgramsControllerService, type DegreeProgramDTO } from '../../../core/modules/openapi'; +import { UsersSelectComponent } from '../../../components/users-select/users-select.component'; + +@Component({ + selector: 'app-all-degree-programs-page', + standalone: true, + imports: [RouterLink, FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, TooltipModule, ToastModule, UsersSelectComponent], + templateUrl: './all-degree-programs-page.component.html' +}) +export class AllDegreeProgramsPageComponent { + private readonly degreeProgramService = inject(DegreeProgramsControllerService); + private readonly messageService = inject(MessageService); + + programs = signal([]); + loading = signal(false); + dialogVisible = signal(false); + + name = signal(''); + responsibleUserId = signal(null); + + constructor() { + this.loadPrograms(); + } + + async loadPrograms() { + this.loading.set(true); + try { + const list = await firstValueFrom(this.degreeProgramService.getAllDegreePrograms()); + this.programs.set(list ?? []); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to load degree programs.' }); + this.programs.set([]); + } finally { + this.loading.set(false); + } + } + + userLabel(program: { responsibleUserId?: string | null; responsibleUser?: { firstName?: string; lastName?: string; email?: string } }): string { + const u = program?.responsibleUser; + if (!u) return program?.responsibleUserId ?? '—'; + return ([u.firstName, u.lastName].filter(Boolean).join(' ').trim() || u.email) ?? program?.responsibleUserId ?? '—'; + } + + openCreate() { + this.name.set(''); + this.responsibleUserId.set(null); + this.dialogVisible.set(true); + } + + async saveProgram() { + const nameVal = this.name().trim(); + const userIdVal = this.responsibleUserId(); + if (!nameVal || !userIdVal) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Name and responsible user are required.' }); + return; + } + try { + await firstValueFrom(this.degreeProgramService.createDegreeProgram({ name: nameVal, responsibleUserId: userIdVal })); + this.messageService.add({ severity: 'success', summary: 'Created', detail: 'Degree program created.' }); + this.dialogVisible.set(false); + await this.loadPrograms(); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save degree program.' }); + } + } + + async deleteProgram(program: DegreeProgramDTO) { + if (!confirm(`Delete degree program "${program.name}"?`)) return; + try { + await firstValueFrom(this.degreeProgramService.deleteDegreeProgram(program.degreeProgramId)); + this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Degree program deleted.' }); + await this.loadPrograms(); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to delete degree program.' }); + } + } +} diff --git a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html new file mode 100644 index 00000000..dd096001 --- /dev/null +++ b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html @@ -0,0 +1,109 @@ + + +@if (program(); as prog) { +

Program details – {{ prog.name }}

+ +
+

Program details

+
+
+ + +
+
+ + +
+
+ +
+ +

Manage which specializations are assigned to this degree program. Add from the list below or create and assign a new one.

+ +
+
+ +
+ + +
+
+ +
+ +

Currently assigned to this program

+ @if (prog.degreeProgramSpecializations && prog.degreeProgramSpecializations.length > 0) { + + + + Name + Responsible user + Actions + + + + + {{ spec.name }} + {{ userLabel(spec) }} + + + + + + + } @else { +

No specializations assigned to this program yet. Add from the list above or create and assign a new one.

+ } +} @else if (!loading()) { +

Degree program not found.

+ +} @else { +

Loading…

+} + + +
+
+ + +
+
+ + +
+
+ + + + +
diff --git a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts new file mode 100644 index 00000000..a5340925 --- /dev/null +++ b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts @@ -0,0 +1,192 @@ +import { Component, inject, signal, computed, OnDestroy } from '@angular/core'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { TableModule } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { DialogModule } from 'primeng/dialog'; +import { MultiSelectModule } from 'primeng/multiselect'; +import { TooltipModule } from 'primeng/tooltip'; +import { MessageService } from 'primeng/api'; +import { ToastModule } from 'primeng/toast'; +import { firstValueFrom } from 'rxjs'; +import { + DegreeProgramSpecializationsControllerService, + DegreeProgramsControllerService, + type CreateDegreeProgramSpecializationDTO, + type DegreeProgramDTO, + type DegreeProgramSpecializationDTO +} from '../../../core/modules/openapi'; +import { UsersSelectComponent } from '../../../components/users-select/users-select.component'; +import { BreadcrumbLabelsService } from '../../../components/breadcrumb/breadcrumb-labels.service'; + +@Component({ + selector: 'app-degree-program-details-page', + standalone: true, + imports: [RouterLink, FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, MultiSelectModule, TooltipModule, ToastModule, UsersSelectComponent], + templateUrl: './degree-program-details-page.component.html' +}) +export class DegreeProgramDetailsPageComponent implements OnDestroy { + private readonly route = inject(ActivatedRoute); + private readonly degreeProgramsService = inject(DegreeProgramsControllerService); + private readonly specializationsService = inject(DegreeProgramSpecializationsControllerService); + private readonly messageService = inject(MessageService); + private readonly breadcrumbLabels = inject(BreadcrumbLabelsService); + + program = signal(null); + allSpecializations = signal([]); + loading = signal(true); + savingProgram = signal(false); + createDialogVisible = signal(false); + specializationsToAdd: number[] = []; + + programName = signal(''); + programResponsibleUserId = signal(null); + + newSpecName = signal(''); + newSpecResponsibleUserId = signal(null); + + specializationOptions = computed(() => { + const all = this.allSpecializations(); + const programSpecs = this.program()?.degreeProgramSpecializations ?? []; + const assignedIds = new Set(programSpecs.map((s) => s.degreeProgramSpecializationId)); + return all.filter((s) => !assignedIds.has(s.degreeProgramSpecializationId)).map((s) => ({ label: s.name, value: s.degreeProgramSpecializationId })); + }); + + constructor() { + this.route.params.subscribe((params) => { + const id = params['id']; + if (id != null) { + this.loadProgram(Number(id)); + } + }); + } + + ngOnDestroy(): void { + this.breadcrumbLabels.degreeProgramName.set(null); + } + + async loadProgram(id: number) { + this.loading.set(true); + try { + const program = await firstValueFrom(this.degreeProgramsService.getDegreeProgram(id)); + this.program.set(program); + this.programName.set(program.name ?? ''); + this.programResponsibleUserId.set(program.responsibleUser.userId ?? null); + this.breadcrumbLabels.degreeProgramName.set(program.name ?? null); + await this.loadAllSpecializations(); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Degree program not found.' }); + this.program.set(null); + this.breadcrumbLabels.degreeProgramName.set(null); + } finally { + this.loading.set(false); + } + } + + async saveProgramDetails() { + const prog = this.program(); + const nameVal = this.programName().trim(); + const userIdVal = this.programResponsibleUserId(); + if (!prog || !nameVal || !userIdVal) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Name and responsible user are required.' }); + return; + } + this.savingProgram.set(true); + try { + await firstValueFrom(this.degreeProgramsService.updateDegreeProgram(prog.degreeProgramId, { name: nameVal, responsibleUserId: userIdVal })); + this.messageService.add({ severity: 'success', summary: 'Updated', detail: 'Program details saved.' }); + await this.loadProgram(prog.degreeProgramId); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save program details.' }); + } finally { + this.savingProgram.set(false); + } + } + + async loadAllSpecializations() { + try { + const list = await firstValueFrom(this.specializationsService.getAllDegreeProgramSpecializations()); + this.allSpecializations.set(list ?? []); + } catch (e) { + this.allSpecializations.set([]); + } + } + + userLabel(spec: { responsibleUserId?: string | null; responsibleUser?: { firstName?: string; lastName?: string; email?: string } }): string { + const u = spec?.responsibleUser; + if (!u) return spec?.responsibleUserId ?? '—'; + return ([u.firstName, u.lastName].filter(Boolean).join(' ').trim() || u.email) ?? spec?.responsibleUserId ?? '—'; + } + + async addSpecializations() { + const prog = this.program(); + if (!prog || this.specializationsToAdd.length === 0) return; + try { + await firstValueFrom( + this.degreeProgramsService.addSpecializationsToDegreeProgram(prog.degreeProgramId, { + degreeProgramSpecializationIds: this.specializationsToAdd + }) + ); + this.messageService.add({ + severity: 'success', + summary: 'Specializations added', + detail: `${this.specializationsToAdd.length} specialization(s) assigned.` + }); + this.specializationsToAdd = []; + await this.loadProgram(prog.degreeProgramId); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to add specializations.' }); + } + } + + async removeSpecialization(degreeProgramSpecializationId: number) { + const prog = this.program(); + if (!prog) return; + try { + await firstValueFrom(this.degreeProgramsService.removeSpecializationFromDegreeProgram(prog.degreeProgramId, degreeProgramSpecializationId)); + this.messageService.add({ + severity: 'success', + summary: 'Specialization removed', + detail: 'Specialization unassigned from program.' + }); + await this.loadProgram(prog.degreeProgramId); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to remove specialization.' }); + } + } + + openCreateSpec() { + this.newSpecName.set(''); + this.newSpecResponsibleUserId.set(null); + this.createDialogVisible.set(true); + } + + async createSpecAndAssign() { + const nameVal = this.newSpecName().trim(); + const userIdVal = this.newSpecResponsibleUserId(); + const prog = this.program(); + if (!nameVal || !userIdVal || !prog) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Name and responsible user are required.' }); + return; + } + const dto: CreateDegreeProgramSpecializationDTO = { name: nameVal, responsibleUserId: userIdVal }; + try { + const newSpec = await firstValueFrom(this.specializationsService.createDegreeProgramSpecialization(dto)); + await firstValueFrom( + this.degreeProgramsService.addSpecializationsToDegreeProgram(prog.degreeProgramId, { + degreeProgramSpecializationIds: [newSpec.degreeProgramSpecializationId] + }) + ); + this.messageService.add({ + severity: 'success', + summary: 'Specialization created', + detail: 'New specialization created and assigned to this program.' + }); + this.createDialogVisible.set(false); + await this.loadProgram(prog.degreeProgramId); + } catch (e) { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to create specialization.' }); + } + } +} diff --git a/Client/src/app/pages/admin/users/admin-users-page.component.html b/Client/src/app/pages/admin/users/users-page.component.html similarity index 93% rename from Client/src/app/pages/admin/users/admin-users-page.component.html rename to Client/src/app/pages/admin/users/users-page.component.html index 1d207d49..b7ac2750 100644 --- a/Client/src/app/pages/admin/users/admin-users-page.component.html +++ b/Client/src/app/pages/admin/users/users-page.component.html @@ -1,9 +1,10 @@ -

Users & Roles

+ +

Users & Roles

View all users and assign or change their roles.

- +
diff --git a/Client/src/app/pages/admin/users/admin-users-page.component.ts b/Client/src/app/pages/admin/users/users-page.component.ts similarity index 83% rename from Client/src/app/pages/admin/users/admin-users-page.component.ts rename to Client/src/app/pages/admin/users/users-page.component.ts index 9acdf75c..041eb43e 100644 --- a/Client/src/app/pages/admin/users/admin-users-page.component.ts +++ b/Client/src/app/pages/admin/users/users-page.component.ts @@ -11,12 +11,12 @@ import { ToastModule } from 'primeng/toast'; import { firstValueFrom } from 'rxjs'; @Component({ - selector: 'app-admin-users-page', + selector: 'app-users-page', standalone: true, imports: [FormsModule, TableModule, MultiSelectModule, InputTextModule, ButtonModule, ToastModule], - templateUrl: './admin-users-page.component.html' + templateUrl: './users-page.component.html' }) -export class AdminUsersPageComponent { +export class UsersPageComponent { private readonly adminUserControllerService = inject(AdminUserControllerService); private readonly messageService = inject(MessageService); @@ -24,6 +24,7 @@ export class AdminUsersPageComponent { totalRecords = signal(0); loading = signal(false); savingUserId = signal(null); + searchField = ''; searchQuery = ''; currentPageSize = signal(10); firstRowIndex = signal(0); @@ -33,27 +34,26 @@ export class AdminUsersPageComponent { })); constructor() { - this.loadUsers(0, this.currentPageSize(), this.searchQuery); + this.loadUsers(); } async pageChange(event: TablePageEvent) { const first = event.first ?? 0; - const rows = event.rows ?? this.currentPageSize(); - this.currentPageSize.set(rows); this.firstRowIndex.set(first); - const page = rows > 0 ? Math.floor(first / rows) : 0; - await this.loadUsers(page, rows, this.searchQuery); + const page = this.currentPageSize() > 0 ? Math.floor(first / this.currentPageSize()) : 0; + await this.loadUsers(page); } runSearch() { this.firstRowIndex.set(0); - this.loadUsers(0, this.currentPageSize(), this.searchQuery); + this.searchQuery = this.searchField; + this.loadUsers(0); } - async loadUsers(page = 0, size = 10, search?: string) { + async loadUsers(page = 0) { this.loading.set(true); try { - const res = await firstValueFrom(this.adminUserControllerService.getUsers(page, size, search?.trim() || undefined)); + const res = await firstValueFrom(this.adminUserControllerService.getUsers(page, this.currentPageSize(), this.searchQuery?.trim() || undefined)); this.users.set(res.content ?? []); this.totalRecords.set(res.totalElements ?? 0); } catch (e) { diff --git a/Client/src/app/pages/feedback-view/feedback-view.component.ts b/Client/src/app/pages/feedback-view/feedback-view.component.ts index a8302b41..ad5dc590 100644 --- a/Client/src/app/pages/feedback-view/feedback-view.component.ts +++ b/Client/src/app/pages/feedback-view/feedback-view.component.ts @@ -1,7 +1,8 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { Component, inject, signal } from '@angular/core'; +import { Component, inject, signal, OnDestroy } from '@angular/core'; import { FeedbackControllerService, ModuleVersionUpdateRequestDTO, FeedbackDTO, GiveFeedbackDTO, ModuleVersionControllerService } from '../../core/modules/openapi'; +import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { FormBuilder, FormGroup, FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { MessageService } from 'primeng/api'; @@ -19,11 +20,12 @@ import { MessageModule } from 'primeng/message'; imports: [FormsModule, RouterModule, ButtonModule, TextareaModule, DialogModule, ToastModule, ProgressSpinnerModule, TooltipModule, MessageModule], templateUrl: './feedback-view.component.html' }) -export class FeedbackViewComponent { +export class FeedbackViewComponent implements OnDestroy { router = inject(Router); feedbackService = inject(FeedbackControllerService); messageService = inject(MessageService); moduleVersionService = inject(ModuleVersionControllerService); + private breadcrumbLabels = inject(BreadcrumbLabelsService); feedbackForm: FormGroup; feedbackId: number | null = null; moduleVersion = signal(null); @@ -121,7 +123,10 @@ export class FeedbackViewComponent { this.loading.set(true); if (feedbackId) { this.feedbackService.getModuleVersionOfFeedback(feedbackId).subscribe({ - next: (response: ModuleVersionUpdateRequestDTO) => this.moduleVersion.set(response), + next: (response: ModuleVersionUpdateRequestDTO) => { + this.moduleVersion.set(response); + this.breadcrumbLabels.feedbackLabel.set(response?.titleEng ?? null); + }, error: (err: HttpErrorResponse) => this.error.set(err.error), complete: () => this.loading.set(false) }); @@ -224,6 +229,10 @@ export class FeedbackViewComponent { } } + ngOnDestroy(): void { + this.breadcrumbLabels.feedbackLabel.set(null); + } + giveFeedback() { if (this.feedbackId) { const feedbackDTO: FeedbackDTO = { diff --git a/Client/src/app/pages/index/index.component.html b/Client/src/app/pages/index/index.component.html index 42d854f1..24a3f946 100644 --- a/Client/src/app/pages/index/index.component.html +++ b/Client/src/app/pages/index/index.component.html @@ -34,6 +34,10 @@

Welcome, {{ user()?.firstName }} {{ user

Manage users and assign roles.

+ +

Manage degree programs and their areas of specialization.

+ +
} @if (isProfessor()) { diff --git a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts index 4c32d6c3..89434af8 100644 --- a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts +++ b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnDestroy } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, RouterModule } from '@angular/router'; @@ -6,6 +6,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { ProposalBaseComponent } from '../../components/create-edit-base/create-edit-base.component'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; import { ModuleVersionUpdateRequestDTO, ModuleVersionUpdateResponseDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; +import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { ToggleButtonGroupComponent } from '../../components/toggle-button-group/toggle-button-group.component'; import { ButtonModule } from 'primeng/button'; import { InputTextModule } from 'primeng/inputtext'; @@ -30,10 +31,11 @@ import { MessageModule } from 'primeng/message'; ], templateUrl: '../../components/create-edit-base/create-edit-base.component.html' }) -export class ModuleVersionEditComponent extends ProposalBaseComponent { +export class ModuleVersionEditComponent extends ProposalBaseComponent implements OnDestroy { override moduleVersionId: number; moduleLoading = false; feedbackLoading = false; + private breadcrumbLabels = inject(BreadcrumbLabelsService); constructor(route: ActivatedRoute) { super(); @@ -42,12 +44,20 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent { this.fetchPreviousModuleVersionFeedback(this.moduleVersionId); } + ngOnDestroy(): void { + this.breadcrumbLabels.proposalTitle.set(null); + this.breadcrumbLabels.versionLabel.set(null); + } + fetchModuleVersion(moduleVersionId: number) { this.moduleLoading = true; this.moduleVersionService.getModuleVersionUpdateDtoFromId(moduleVersionId).subscribe({ next: (response: ModuleVersionUpdateRequestDTO) => { this.proposalForm.patchValue(response); this.moduleVersionDto.set(response); + const version = response?.version; + this.breadcrumbLabels.proposalTitle.set(response?.titleEng ?? null); + this.breadcrumbLabels.versionLabel.set(version != null ? `Version ${version}` : null); }, error: (err: HttpErrorResponse) => this.error.set(err.error), complete: () => { diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.ts b/Client/src/app/pages/module-version-view/module-version-view.component.ts index 4457e118..0fa5aca6 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.ts +++ b/Client/src/app/pages/module-version-view/module-version-view.component.ts @@ -1,8 +1,9 @@ import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; -import { Component, inject, signal } from '@angular/core'; +import { Component, inject, signal, OnDestroy } from '@angular/core'; import { RouterModule, ActivatedRoute } from '@angular/router'; import { ModuleVersionControllerService, ModuleVersionViewDTO, ModuleVersion, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; +import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; import { ModuleVersionStatusPipe } from '../../pipes/moduleVersionStatus.pipe'; @@ -40,9 +41,10 @@ export interface ModuleField { ], templateUrl: './module-version-view.component.html' }) -export class ModuleVersionViewComponent { +export class ModuleVersionViewComponent implements OnDestroy { route = inject(ActivatedRoute); moduleVersionService = inject(ModuleVersionControllerService); + private breadcrumbLabels = inject(BreadcrumbLabelsService); proposalId: string | null = null; moduleVersionId: number | null = null; loading = signal(true); @@ -98,12 +100,22 @@ export class ModuleVersionViewComponent { private fetchModuleVersionViewDto(moduleVersionId: number) { this.loading.set(true); this.moduleVersionService.getModuleVersionViewDto(moduleVersionId).subscribe({ - next: (data: ModuleVersionViewDTO) => this.moduleVersionDto.set(data), + next: (data: ModuleVersionViewDTO) => { + this.moduleVersionDto.set(data); + const version = data?.version; + this.breadcrumbLabels.proposalTitle.set(data?.titleEng ?? null); + this.breadcrumbLabels.versionLabel.set(version != null ? `Version ${version}` : null); + }, error: (err: HttpErrorResponse) => this.error.set(err.error), complete: () => this.loading.set(false) }); } + ngOnDestroy(): void { + this.breadcrumbLabels.proposalTitle.set(null); + this.breadcrumbLabels.versionLabel.set(null); + } + pdfExport() { const mvid = this.moduleVersionId; if (!mvid) { diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.html b/Client/src/app/pages/proposal-view/proposal-view.component.html index 0c9c9332..a3acfd93 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.html +++ b/Client/src/app/pages/proposal-view/proposal-view.component.html @@ -22,7 +22,7 @@

Module Proposal '{{ proposal.latestModuleVersion?.titl

{{ proposal.proposalId }}

-

Latest Version

+

Total versions

{{ proposal.latestVersion }}

diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.ts b/Client/src/app/pages/proposal-view/proposal-view.component.ts index 5afeac86..47a1baf8 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.ts +++ b/Client/src/app/pages/proposal-view/proposal-view.component.ts @@ -1,5 +1,6 @@ -import { Component, inject, signal } from '@angular/core'; +import { Component, inject, signal, OnDestroy } from '@angular/core'; import { AddModuleVersionDTO, ModuleVersion, Proposal, ProposalControllerService, ProposalViewDTO } from '../../core/modules/openapi'; +import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; @@ -39,10 +40,11 @@ import { ProgressSpinnerModule } from 'primeng/progressspinner'; }, templateUrl: './proposal-view.component.html' }) -export class ProposalViewComponent { +export class ProposalViewComponent implements OnDestroy { router = inject(Router); route = inject(ActivatedRoute); proposalService = inject(ProposalControllerService); + private breadcrumbLabels = inject(BreadcrumbLabelsService); loading = signal(true); error = signal(null); proposal = signal(null); @@ -59,7 +61,11 @@ export class ProposalViewComponent { if (proposalId) { this.loading.set(true); this.proposalService.getProposalView(proposalId).subscribe({ - next: (data: ProposalViewDTO) => this.proposal.set(data), + next: (data: ProposalViewDTO) => { + this.proposal.set(data); + const title = data?.latestModuleVersion?.titleEng ?? null; + this.breadcrumbLabels.proposalTitle.set(title); + }, error: (err: HttpErrorResponse) => this.error.set(err.error), complete: () => this.loading.set(false) }); @@ -103,4 +109,8 @@ export class ProposalViewComponent { }); } } + + ngOnDestroy(): void { + this.breadcrumbLabels.proposalTitle.set(null); + } } diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramSpecializationsController.java b/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramSpecializationsController.java new file mode 100644 index 00000000..d3948993 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramSpecializationsController.java @@ -0,0 +1,45 @@ +package modulemanagement.ls1.controllers; + +import jakarta.validation.Valid; +import modulemanagement.ls1.dtos.*; +import modulemanagement.ls1.services.DegreeProgramSpecializationService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/degree-program-specializations") +@PreAuthorize("hasRole('ADMIN')") +public class DegreeProgramSpecializationsController { + + private final DegreeProgramSpecializationService degreeProgramSpecializationService; + + public DegreeProgramSpecializationsController(DegreeProgramSpecializationService degreeProgramSpecializationService) { + this.degreeProgramSpecializationService = degreeProgramSpecializationService; + } + + @GetMapping + public ResponseEntity> getAllDegreeProgramSpecializations() { + return ResponseEntity.ok(degreeProgramSpecializationService.getAllDegreeProgramSpecializations()); + } + + @PostMapping + public ResponseEntity createDegreeProgramSpecialization(@Valid @RequestBody CreateDegreeProgramSpecializationDTO dto) { + return ResponseEntity.ok(degreeProgramSpecializationService.createDegreeProgramSpecialization(dto)); + } + + @PatchMapping("/{id}") + public ResponseEntity updateDegreeProgramSpecialization( + @PathVariable Long id, + @Valid @RequestBody UpdateDegreeProgramSpecializationDTO dto) { + return ResponseEntity.ok(degreeProgramSpecializationService.updateDegreeProgramSpecialization(id, dto)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteDegreeProgramSpecialization(@PathVariable Long id) { + degreeProgramSpecializationService.deleteDegreeProgramSpecialization(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java b/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java new file mode 100644 index 00000000..44d8bcbf --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java @@ -0,0 +1,64 @@ +package modulemanagement.ls1.controllers; + +import jakarta.validation.Valid; +import modulemanagement.ls1.dtos.*; +import modulemanagement.ls1.services.DegreeProgramService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/degree-programs") +@PreAuthorize("hasRole('ADMIN')") +public class DegreeProgramsController { + + private final DegreeProgramService degreeProgramService; + + public DegreeProgramsController(DegreeProgramService degreeProgramService) { + this.degreeProgramService = degreeProgramService; + } + + @GetMapping + public ResponseEntity> getAllDegreePrograms() { + return ResponseEntity.ok(degreeProgramService.getAllDegreePrograms()); + } + + @GetMapping("/{id}") + public ResponseEntity getDegreeProgram(@PathVariable Long id) { + return ResponseEntity.ok(degreeProgramService.getDegreeProgram(id)); + } + + @PostMapping + public ResponseEntity createDegreeProgram(@Valid @RequestBody CreateDegreeProgramDTO dto) { + return ResponseEntity.ok(degreeProgramService.createDegreeProgram(dto)); + } + + @PatchMapping("/{id}") + public ResponseEntity updateDegreeProgram( + @PathVariable Long id, + @Valid @RequestBody UpdateDegreeProgramDTO dto) { + return ResponseEntity.ok(degreeProgramService.updateDegreeProgram(id, dto)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteDegreeProgram(@PathVariable Long id) { + degreeProgramService.deleteDegreeProgram(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{degreeProgramId}/degree-program-specializations/batch") + public ResponseEntity addSpecializationsToDegreeProgram( + @PathVariable Long degreeProgramId, + @Valid @RequestBody AddSpecializationsToDegreeProgramDTO dto) { + return ResponseEntity.ok(degreeProgramService.addSpecializationsToDegreeProgram(degreeProgramId, dto.getDegreeProgramSpecializationIds())); + } + + @DeleteMapping("/{degreeProgramId}/degree-program-specializations/{degreeProgramSpecializationId}") + public ResponseEntity removeSpecializationFromDegreeProgram( + @PathVariable Long degreeProgramId, + @PathVariable Long degreeProgramSpecializationId) { + return ResponseEntity.ok(degreeProgramService.removeSpecializationFromDegreeProgram(degreeProgramId, degreeProgramSpecializationId)); + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/AddSpecializationsToDegreeProgramDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/AddSpecializationsToDegreeProgramDTO.java new file mode 100644 index 00000000..c17719c2 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/AddSpecializationsToDegreeProgramDTO.java @@ -0,0 +1,19 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public class AddSpecializationsToDegreeProgramDTO { + + @NotNull(message = "degreeProgramSpecializationIds must not be null") + private List degreeProgramSpecializationIds; + + public List getDegreeProgramSpecializationIds() { + return degreeProgramSpecializationIds; + } + + public void setDegreeProgramSpecializationIds(List degreeProgramSpecializationIds) { + this.degreeProgramSpecializationIds = degreeProgramSpecializationIds; + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramDTO.java new file mode 100644 index 00000000..ca6197e7 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramDTO.java @@ -0,0 +1,15 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.UUID; + +@Data +public class CreateDegreeProgramDTO { + @NotBlank + private String name; + @NotNull + private UUID responsibleUserId; +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramSpecializationDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramSpecializationDTO.java new file mode 100644 index 00000000..428acd8a --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/CreateDegreeProgramSpecializationDTO.java @@ -0,0 +1,15 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.UUID; + +@Data +public class CreateDegreeProgramSpecializationDTO { + @NotBlank + private String name; + @NotNull + private UUID responsibleUserId; +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java new file mode 100644 index 00000000..d05a4005 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java @@ -0,0 +1,34 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import modulemanagement.ls1.models.DegreeProgram; + +import java.util.List; +import java.util.stream.Collectors; + +@Data +public class DegreeProgramDTO { + @NotNull + private Long degreeProgramId; + @NotNull + private String name; + @NotNull + private ResponsibleUserDTO responsibleUser; + + private List degreeProgramSpecializations; + + public static DegreeProgramDTO fromDegreeProgram(DegreeProgram program) { + DegreeProgramDTO dto = new DegreeProgramDTO(); + dto.setDegreeProgramId(program.getDegreeProgramId()); + dto.setName(program.getName()); + if (program.getResponsibleUser() != null) { + dto.setResponsibleUser(ResponsibleUserDTO.fromUser(program.getResponsibleUser())); + } + if (program.getDegreeProgramSpecializations() != null) { + dto.setDegreeProgramSpecializations(program.getDegreeProgramSpecializations().stream() + .map(DegreeProgramSpecializationDTO::fromEntity).collect(Collectors.toList())); + } + return dto; + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramSpecializationDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramSpecializationDTO.java new file mode 100644 index 00000000..11cf9617 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramSpecializationDTO.java @@ -0,0 +1,25 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import modulemanagement.ls1.models.DegreeProgramSpecialization; + +@Data +public class DegreeProgramSpecializationDTO { + @NotNull + private Long degreeProgramSpecializationId; + @NotNull + private String name; + @NotNull + private ResponsibleUserDTO responsibleUser; + + public static DegreeProgramSpecializationDTO fromEntity(DegreeProgramSpecialization entity) { + DegreeProgramSpecializationDTO dto = new DegreeProgramSpecializationDTO(); + dto.setDegreeProgramSpecializationId(entity.getDegreeProgramSpecializationId()); + dto.setName(entity.getName()); + if (entity.getResponsibleUser() != null) { + dto.setResponsibleUser(ResponsibleUserDTO.fromUser(entity.getResponsibleUser())); + } + return dto; + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ResponsibleUserDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ResponsibleUserDTO.java new file mode 100644 index 00000000..9899d23e --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ResponsibleUserDTO.java @@ -0,0 +1,23 @@ +package modulemanagement.ls1.dtos; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class ResponsibleUserDTO { + private UUID userId; + private String firstName; + private String lastName; + private String email; + + public static ResponsibleUserDTO fromUser(modulemanagement.ls1.models.User user) { + if (user == null) return null; + ResponsibleUserDTO dto = new ResponsibleUserDTO(); + dto.setUserId(user.getUserId()); + dto.setFirstName(user.getFirstName()); + dto.setLastName(user.getLastName()); + dto.setEmail(user.getEmail()); + return dto; + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java new file mode 100644 index 00000000..dfd14230 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java @@ -0,0 +1,11 @@ +package modulemanagement.ls1.dtos; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class UpdateDegreeProgramDTO { + private String name; + private UUID responsibleUserId; +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramSpecializationDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramSpecializationDTO.java new file mode 100644 index 00000000..aa83b9ae --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramSpecializationDTO.java @@ -0,0 +1,11 @@ +package modulemanagement.ls1.dtos; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class UpdateDegreeProgramSpecializationDTO { + private String name; + private UUID responsibleUserId; +} diff --git a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java new file mode 100644 index 00000000..6f83a7e6 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java @@ -0,0 +1,32 @@ +package modulemanagement.ls1.models; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "degree_program") +public class DegreeProgram { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "degree_program_id") + private Long degreeProgramId; + + @Column(name = "name", nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "responsible_user_id", nullable = false) + private User responsibleUser; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "degree_program_specialization_assignment", joinColumns = @JoinColumn(name = "degree_program_id"), inverseJoinColumns = @JoinColumn(name = "degree_program_specialization_id")) + private List degreeProgramSpecializations = new ArrayList<>(); + +} diff --git a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java new file mode 100644 index 00000000..646e9aed --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java @@ -0,0 +1,30 @@ +package modulemanagement.ls1.models; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "degree_program_specialization") +public class DegreeProgramSpecialization { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "degree_program_specialization_id") + private Long degreeProgramSpecializationId; + + @Column(name = "name", nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "responsible_user_id", nullable = false) + private User responsibleUser; + + @ManyToMany(mappedBy = "degreeProgramSpecializations", fetch = FetchType.LAZY) + private List degreePrograms = new ArrayList<>(); +} diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java new file mode 100644 index 00000000..1c54da04 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java @@ -0,0 +1,22 @@ +package modulemanagement.ls1.repositories; + +import modulemanagement.ls1.models.DegreeProgram; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface DegreeProgramRepository extends JpaRepository { + + @EntityGraph(attributePaths = { "responsibleUser" }) + @Query("SELECT p FROM DegreeProgram p") + List findAllWithResponsibleUser(); + + @EntityGraph(attributePaths = { "responsibleUser", "degreeProgramSpecializations", + "degreeProgramSpecializations.responsibleUser" }) + Optional findWithSpecializationsByDegreeProgramId(Long degreeProgramId); +} diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java new file mode 100644 index 00000000..92d3170d --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java @@ -0,0 +1,23 @@ +package modulemanagement.ls1.repositories; + +import modulemanagement.ls1.models.DegreeProgramSpecialization; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface DegreeProgramSpecializationRepository extends JpaRepository { + + @EntityGraph(attributePaths = { "responsibleUser" }) + @Query("SELECT s FROM DegreeProgramSpecialization s") + List findAllWithResponsibleUser(); + + @EntityGraph(attributePaths = { "responsibleUser" }) + @Query("SELECT s FROM DegreeProgramSpecialization s WHERE s.degreeProgramSpecializationId = :id") + Optional findByIdWithResponsibleUser(@Param("id") Long id); +} diff --git a/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java b/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java index eec90e77..1388666f 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java @@ -10,7 +10,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import java.util.ArrayList; import java.util.UUID; @Service diff --git a/Server/src/main/java/modulemanagement/ls1/services/AuthenticationService.java b/Server/src/main/java/modulemanagement/ls1/services/AuthenticationService.java index 2b5850e0..2e435759 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/AuthenticationService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/AuthenticationService.java @@ -1,12 +1,10 @@ package modulemanagement.ls1.services; -import modulemanagement.ls1.enums.UserRole; import modulemanagement.ls1.models.User; import modulemanagement.ls1.repositories.UserRepository; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Service; -import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.UUID; diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java new file mode 100644 index 00000000..bfc6edaf --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java @@ -0,0 +1,106 @@ +package modulemanagement.ls1.services; + +import modulemanagement.ls1.dtos.*; +import modulemanagement.ls1.models.DegreeProgramSpecialization; +import modulemanagement.ls1.models.DegreeProgram; +import modulemanagement.ls1.repositories.DegreeProgramRepository; +import modulemanagement.ls1.repositories.DegreeProgramSpecializationRepository; +import modulemanagement.ls1.repositories.UserRepository; +import modulemanagement.ls1.shared.ResourceNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class DegreeProgramService { + + private final DegreeProgramRepository degreeProgramRepository; + private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; + private final UserRepository userRepository; + + public DegreeProgramService(DegreeProgramRepository degreeProgramRepository, + DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, + UserRepository userRepository) { + this.degreeProgramRepository = degreeProgramRepository; + this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; + this.userRepository = userRepository; + } + + public List getAllDegreePrograms() { + return degreeProgramRepository.findAllWithResponsibleUser().stream() + .map(DegreeProgramDTO::fromDegreeProgram) + .collect(Collectors.toList()); + } + + public DegreeProgramDTO getDegreeProgram(Long id) { + DegreeProgram program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id) + .orElseThrow(() -> new ResourceNotFoundException("Degree program not found: " + id)); + return DegreeProgramDTO.fromDegreeProgram(program); + } + + public DegreeProgramDTO createDegreeProgram(CreateDegreeProgramDTO dto) { + DegreeProgram program = new DegreeProgram(); + program.setName(dto.getName()); + program.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); + program = degreeProgramRepository.save(program); + program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(program.getDegreeProgramId()) + .orElse(program); + return DegreeProgramDTO.fromDegreeProgram(program); + } + + public DegreeProgramDTO updateDegreeProgram(Long id, UpdateDegreeProgramDTO dto) { + DegreeProgram program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id) + .orElseThrow(() -> new ResourceNotFoundException("Degree program not found: " + id)); + if (dto.getName() != null) + program.setName(dto.getName()); + if (dto.getResponsibleUserId() != null) + program.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); + program = degreeProgramRepository.save(program); + program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id).orElse(program); + return DegreeProgramDTO.fromDegreeProgram(program); + } + + public void deleteDegreeProgram(Long id) { + if (!degreeProgramRepository.existsById(id)) { + throw new ResourceNotFoundException("Degree program not found: " + id); + } + degreeProgramRepository.deleteById(id); + } + + public DegreeProgramDTO addSpecializationsToDegreeProgram(Long degreeProgramId, + List degreeProgramSpecializationIds) { + + DegreeProgram program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(degreeProgramId) + .orElseThrow(() -> new ResourceNotFoundException("Degree program not found: " + degreeProgramId)); + + if (degreeProgramSpecializationIds == null || degreeProgramSpecializationIds.isEmpty()) { + return DegreeProgramDTO.fromDegreeProgram(program); + } + + var existingIds = program.getDegreeProgramSpecializations().stream() + .map(DegreeProgramSpecialization::getDegreeProgramSpecializationId) + .collect(Collectors.toSet()); + + for (Long specId : degreeProgramSpecializationIds) { + if (existingIds.contains(specId)) + continue; + program.getDegreeProgramSpecializations() + .add(degreeProgramSpecializationRepository.getReferenceById(specId)); + existingIds.add(specId); + } + program = degreeProgramRepository.save(program); + return DegreeProgramDTO.fromDegreeProgram( + degreeProgramRepository.findWithSpecializationsByDegreeProgramId(degreeProgramId).orElse(program)); + } + + public DegreeProgramDTO removeSpecializationFromDegreeProgram(Long degreeProgramId, + Long degreeProgramSpecializationId) { + DegreeProgram program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(degreeProgramId) + .orElseThrow(() -> new ResourceNotFoundException("Degree program not found: " + degreeProgramId)); + program.getDegreeProgramSpecializations() + .removeIf(s -> s.getDegreeProgramSpecializationId().equals(degreeProgramSpecializationId)); + program = degreeProgramRepository.save(program); + return DegreeProgramDTO.fromDegreeProgram(program); + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java new file mode 100644 index 00000000..97a6a3a4 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java @@ -0,0 +1,60 @@ +package modulemanagement.ls1.services; + +import modulemanagement.ls1.dtos.*; +import modulemanagement.ls1.models.DegreeProgramSpecialization; +import modulemanagement.ls1.repositories.DegreeProgramSpecializationRepository; +import modulemanagement.ls1.repositories.UserRepository; +import modulemanagement.ls1.shared.ResourceNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class DegreeProgramSpecializationService { + + private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; + private final UserRepository userRepository; + + public DegreeProgramSpecializationService( + DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, + UserRepository userRepository) { + this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; + this.userRepository = userRepository; + } + + public List getAllDegreeProgramSpecializations() { + return degreeProgramSpecializationRepository.findAllWithResponsibleUser().stream() + .map(DegreeProgramSpecializationDTO::fromEntity) + .collect(Collectors.toList()); + } + + public DegreeProgramSpecializationDTO createDegreeProgramSpecialization(CreateDegreeProgramSpecializationDTO dto) { + DegreeProgramSpecialization entity = new DegreeProgramSpecialization(); + entity.setName(dto.getName()); + entity.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); + entity = degreeProgramSpecializationRepository.save(entity); + return DegreeProgramSpecializationDTO.fromEntity( + degreeProgramSpecializationRepository + .findByIdWithResponsibleUser(entity.getDegreeProgramSpecializationId()).orElse(entity)); + } + + public DegreeProgramSpecializationDTO updateDegreeProgramSpecialization(Long id, + UpdateDegreeProgramSpecializationDTO dto) { + DegreeProgramSpecialization entity = degreeProgramSpecializationRepository.findByIdWithResponsibleUser(id) + .orElseThrow(() -> new ResourceNotFoundException("Degree program specialization not found: " + id)); + if (dto.getName() != null) + entity.setName(dto.getName()); + if (dto.getResponsibleUserId() != null) + entity.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); + entity = degreeProgramSpecializationRepository.save(entity); + return DegreeProgramSpecializationDTO.fromEntity(entity); + } + + public void deleteDegreeProgramSpecialization(Long id) { + if (!degreeProgramSpecializationRepository.existsById(id)) { + throw new ResourceNotFoundException("Degree program specialization not found: " + id); + } + degreeProgramSpecializationRepository.deleteById(id); + } +} diff --git a/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml b/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml new file mode 100644 index 00000000..d37e43dc --- /dev/null +++ b/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml @@ -0,0 +1,83 @@ +databaseChangeLog: + - changeSet: + id: 0008-degree-program-and-area + author: system + changes: + - createTable: + tableName: degree_program + columns: + - column: + name: degree_program_id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: name + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: responsible_user_id + type: UUID + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: degree_program + baseColumnNames: responsible_user_id + referencedTableName: app_user + referencedColumnNames: user_id + constraintName: degree_program_responsible_user_fk + - createTable: + tableName: degree_program_specialization + columns: + - column: + name: degree_program_specialization_id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: name + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: responsible_user_id + type: UUID + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: degree_program_specialization + baseColumnNames: responsible_user_id + referencedTableName: app_user + referencedColumnNames: user_id + constraintName: degree_program_specialization_responsible_user_fk + - createTable: + tableName: degree_program_specialization_assignment + columns: + - column: + name: degree_program_id + type: BIGINT + constraints: + primaryKey: true + nullable: false + - column: + name: degree_program_specialization_id + type: BIGINT + constraints: + primaryKey: true + nullable: false + - addForeignKeyConstraint: + baseTableName: degree_program_specialization_assignment + baseColumnNames: degree_program_id + referencedTableName: degree_program + referencedColumnNames: degree_program_id + constraintName: deg_prog_spec_assign_program_fk + - addForeignKeyConstraint: + baseTableName: degree_program_specialization_assignment + baseColumnNames: degree_program_specialization_id + referencedTableName: degree_program_specialization + referencedColumnNames: degree_program_specialization_id + constraintName: deg_prog_spec_assign_spec_fk + onDelete: CASCADE diff --git a/Server/src/main/resources/db/changelog/master.yaml b/Server/src/main/resources/db/changelog/master.yaml index d770a6d3..ce435a17 100644 --- a/Server/src/main/resources/db/changelog/master.yaml +++ b/Server/src/main/resources/db/changelog/master.yaml @@ -20,3 +20,6 @@ databaseChangeLog: - include: relativeToChangelogFile: true file: changes/0007_user_roles_many.yaml + - include: + relativeToChangelogFile: true + file: changes/0008_degree_program_and_area_tables.yaml From ad4c35fc3fe1f3642989f00eb817257fa34190fc Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 1 Mar 2026 17:57:58 +0100 Subject: [PATCH 03/42] fix --- .../degree-program-details-page.component.ts | 8 ++------ .../app/pages/feedback-view/feedback-view.component.ts | 8 ++------ .../module-version-edit/module-version-edit.component.ts | 9 ++------- .../module-version-view/module-version-view.component.ts | 9 ++------- .../app/pages/proposal-view/proposal-view.component.ts | 8 ++------ 5 files changed, 10 insertions(+), 32 deletions(-) diff --git a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts index a5340925..afe422a9 100644 --- a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts +++ b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal, computed, OnDestroy } from '@angular/core'; +import { Component, inject, signal, computed } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { TableModule } from 'primeng/table'; @@ -26,7 +26,7 @@ import { BreadcrumbLabelsService } from '../../../components/breadcrumb/breadcru imports: [RouterLink, FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, MultiSelectModule, TooltipModule, ToastModule, UsersSelectComponent], templateUrl: './degree-program-details-page.component.html' }) -export class DegreeProgramDetailsPageComponent implements OnDestroy { +export class DegreeProgramDetailsPageComponent { private readonly route = inject(ActivatedRoute); private readonly degreeProgramsService = inject(DegreeProgramsControllerService); private readonly specializationsService = inject(DegreeProgramSpecializationsControllerService); @@ -62,10 +62,6 @@ export class DegreeProgramDetailsPageComponent implements OnDestroy { }); } - ngOnDestroy(): void { - this.breadcrumbLabels.degreeProgramName.set(null); - } - async loadProgram(id: number) { this.loading.set(true); try { diff --git a/Client/src/app/pages/feedback-view/feedback-view.component.ts b/Client/src/app/pages/feedback-view/feedback-view.component.ts index ad5dc590..1b355839 100644 --- a/Client/src/app/pages/feedback-view/feedback-view.component.ts +++ b/Client/src/app/pages/feedback-view/feedback-view.component.ts @@ -1,6 +1,6 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { Component, inject, signal, OnDestroy } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; import { FeedbackControllerService, ModuleVersionUpdateRequestDTO, FeedbackDTO, GiveFeedbackDTO, ModuleVersionControllerService } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { FormBuilder, FormGroup, FormsModule } from '@angular/forms'; @@ -20,7 +20,7 @@ import { MessageModule } from 'primeng/message'; imports: [FormsModule, RouterModule, ButtonModule, TextareaModule, DialogModule, ToastModule, ProgressSpinnerModule, TooltipModule, MessageModule], templateUrl: './feedback-view.component.html' }) -export class FeedbackViewComponent implements OnDestroy { +export class FeedbackViewComponent { router = inject(Router); feedbackService = inject(FeedbackControllerService); messageService = inject(MessageService); @@ -229,10 +229,6 @@ export class FeedbackViewComponent implements OnDestroy { } } - ngOnDestroy(): void { - this.breadcrumbLabels.feedbackLabel.set(null); - } - giveFeedback() { if (this.feedbackId) { const feedbackDTO: FeedbackDTO = { diff --git a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts index 89434af8..b8076bdd 100644 --- a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts +++ b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnDestroy } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, RouterModule } from '@angular/router'; @@ -31,7 +31,7 @@ import { MessageModule } from 'primeng/message'; ], templateUrl: '../../components/create-edit-base/create-edit-base.component.html' }) -export class ModuleVersionEditComponent extends ProposalBaseComponent implements OnDestroy { +export class ModuleVersionEditComponent extends ProposalBaseComponent { override moduleVersionId: number; moduleLoading = false; feedbackLoading = false; @@ -44,11 +44,6 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent implements this.fetchPreviousModuleVersionFeedback(this.moduleVersionId); } - ngOnDestroy(): void { - this.breadcrumbLabels.proposalTitle.set(null); - this.breadcrumbLabels.versionLabel.set(null); - } - fetchModuleVersion(moduleVersionId: number) { this.moduleLoading = true; this.moduleVersionService.getModuleVersionUpdateDtoFromId(moduleVersionId).subscribe({ diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.ts b/Client/src/app/pages/module-version-view/module-version-view.component.ts index 0fa5aca6..d25d11f6 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.ts +++ b/Client/src/app/pages/module-version-view/module-version-view.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; -import { Component, inject, signal, OnDestroy } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; import { RouterModule, ActivatedRoute } from '@angular/router'; import { ModuleVersionControllerService, ModuleVersionViewDTO, ModuleVersion, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; @@ -41,7 +41,7 @@ export interface ModuleField { ], templateUrl: './module-version-view.component.html' }) -export class ModuleVersionViewComponent implements OnDestroy { +export class ModuleVersionViewComponent { route = inject(ActivatedRoute); moduleVersionService = inject(ModuleVersionControllerService); private breadcrumbLabels = inject(BreadcrumbLabelsService); @@ -111,11 +111,6 @@ export class ModuleVersionViewComponent implements OnDestroy { }); } - ngOnDestroy(): void { - this.breadcrumbLabels.proposalTitle.set(null); - this.breadcrumbLabels.versionLabel.set(null); - } - pdfExport() { const mvid = this.moduleVersionId; if (!mvid) { diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.ts b/Client/src/app/pages/proposal-view/proposal-view.component.ts index 47a1baf8..add3a87a 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.ts +++ b/Client/src/app/pages/proposal-view/proposal-view.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal, OnDestroy } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; import { AddModuleVersionDTO, ModuleVersion, Proposal, ProposalControllerService, ProposalViewDTO } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; @@ -40,7 +40,7 @@ import { ProgressSpinnerModule } from 'primeng/progressspinner'; }, templateUrl: './proposal-view.component.html' }) -export class ProposalViewComponent implements OnDestroy { +export class ProposalViewComponent { router = inject(Router); route = inject(ActivatedRoute); proposalService = inject(ProposalControllerService); @@ -109,8 +109,4 @@ export class ProposalViewComponent implements OnDestroy { }); } } - - ngOnDestroy(): void { - this.breadcrumbLabels.proposalTitle.set(null); - } } From 7d79b14eed6ae278a391a159d5e56549bb92b474 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 8 Mar 2026 22:00:58 +0100 Subject: [PATCH 04/42] module creation steps --- .../create-edit-base-layout.css | 22 + .../create-edit-base.component.html | 1090 +++++++++-------- .../create-edit-base.component.ts | 129 +- .../module-edit-stepper.component.css | 81 ++ .../module-edit-stepper.component.html | 14 + .../module-edit-stepper.component.ts | 27 + .../module-edit-steps.config.ts | 56 + .../proposal-list-table.component.ts | 4 +- .../modules/openapi/.openapi-generator/FILES | 4 +- .../api/degree-programs-controller.service.ts | 59 + ...ee-programs-controller.serviceInterface.ts | 6 + .../api/feedback-controller.service.ts | 10 +- .../feedback-controller.serviceInterface.ts | 4 +- .../api/module-version-controller.service.ts | 18 +- ...ule-version-controller.serviceInterface.ts | 5 +- .../api/proposal-controller.service.ts | 10 +- .../proposal-controller.serviceInterface.ts | 3 +- .../core/modules/openapi/model/feedback.ts | 2 +- .../app/core/modules/openapi/model/models.ts | 4 +- .../module-degree-program-assignment-dto.ts | 16 + .../module-version-update-request-dto.ts | 9 + .../module-version-update-response-dto.ts | 62 - .../openapi/model/module-version-view-dto.ts | 9 + .../modules/openapi/model/module-version.ts | 64 - .../openapi/model/proposal-request-dto.ts | 9 + .../core/modules/openapi/model/proposal.ts | 34 - .../openapi/model/similar-module-dto.ts | 2 +- .../feedback-view/feedback-view.component.ts | 9 +- .../module-version-edit.component.ts | 68 +- .../module-version-view.component.ts | 4 +- .../proposal-create.component.ts | 74 +- .../proposal-view/proposal-view.component.ts | 6 +- Client/src/app/pipes/proposalStatus.pipe.ts | 6 +- .../controllers/DegreeProgramsController.java | 6 + .../ls1/controllers/FeedbackController.java | 6 +- .../controllers/ModuleVersionController.java | 9 +- .../ls1/controllers/ProposalController.java | 7 +- .../ModuleDegreeProgramAssignmentDTO.java | 12 + .../dtos/ModuleVersionUpdateRequestDTO.java | 53 +- .../dtos/ModuleVersionUpdateResponseDTO.java | 21 - .../ls1/dtos/ModuleVersionViewDTO.java | 27 + .../ls1/dtos/ProposalRequestDTO.java | 18 +- .../ls1/models/ModuleVersion.java | 28 +- .../ModuleVersionDegreeProgramAssignment.java | 29 + .../modulemanagement/ls1/models/Proposal.java | 20 +- .../repositories/DegreeProgramRepository.java | 4 + ...sionDegreeProgramAssignmentRepository.java | 17 + .../ls1/services/DegreeProgramService.java | 6 + .../ls1/services/FeedbackService.java | 6 +- .../ls1/services/ModuleVersionService.java | 73 +- .../ls1/services/ProposalService.java | 92 +- ...odule_version_initial_and_assignments.yaml | 76 ++ .../initialize/0010_seed_degree_programs.yaml | 61 + ...1_seed_degree_program_specializations.yaml | 84 ++ .../main/resources/db/changelog/master.yaml | 9 + 55 files changed, 1720 insertions(+), 864 deletions(-) create mode 100644 Client/src/app/components/create-edit-base/create-edit-base-layout.css create mode 100644 Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css create mode 100644 Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html create mode 100644 Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts create mode 100644 Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts create mode 100644 Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts delete mode 100644 Client/src/app/core/modules/openapi/model/module-version-update-response-dto.ts delete mode 100644 Client/src/app/core/modules/openapi/model/module-version.ts delete mode 100644 Client/src/app/core/modules/openapi/model/proposal.ts create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java delete mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateResponseDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/models/ModuleVersionDegreeProgramAssignment.java create mode 100644 Server/src/main/java/modulemanagement/ls1/repositories/ModuleVersionDegreeProgramAssignmentRepository.java create mode 100644 Server/src/main/resources/db/changelog/changes/0009_module_version_initial_and_assignments.yaml create mode 100644 Server/src/main/resources/db/changelog/initialize/0010_seed_degree_programs.yaml create mode 100644 Server/src/main/resources/db/changelog/initialize/0011_seed_degree_program_specializations.yaml diff --git a/Client/src/app/components/create-edit-base/create-edit-base-layout.css b/Client/src/app/components/create-edit-base/create-edit-base-layout.css new file mode 100644 index 00000000..fbbb0231 --- /dev/null +++ b/Client/src/app/components/create-edit-base/create-edit-base-layout.css @@ -0,0 +1,22 @@ +.module-edit-layout { + display: grid; + grid-template-columns: 280px 1fr; + gap: 2rem; + margin: 0 auto; +} + +@media (max-width: 768px) { + .module-edit-layout { + grid-template-columns: 1fr; + } +} + +.module-edit-stepper-column { + position: sticky; + top: 1rem; + align-self: start; +} + +.module-edit-form-column { + min-width: 0; +} diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index d89c2ef4..8aa31cbf 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -1,523 +1,639 @@ -
-

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' + moduleVersionDto()?.titleEng : 'Create New Module Proposal' }}

- -
- -
-

Information

-

- Please fill out all information fields required for a new module. -
- After giving the module a title you can save your progress at any time and come back at another time. -

+
+ +
+

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' + moduleVersionDto()?.titleEng : 'Create New Module Proposal' }}

+ +
+ +
+

Information

+

Complete the steps on the left. You can save progress at any time and submit for feedback when ready.

+
-
- + -
-
- - @if (moduleVersionId && hasFeedback('titleFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.titleFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.titleFeedback }} -

+ + @if (currentStepIndex() === 0) { +
+
+ + @if (moduleVersionId && hasFeedback('titleFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.titleFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.titleFeedback }} +

+ } + } +
} - } - - } -
- - @if (proposalForm.get('titleEng')?.invalid && proposalForm.get('titleEng')?.touched) { - A preliminary title is required - } -
-
- -
- -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('levelFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.levelFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.levelFeedback }} -

+
+ + @if (proposalForm.get('titleEng')?.invalid && proposalForm.get('titleEng')?.touched) { + A preliminary title is required + } +
+
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ -
- - @if (moduleVersionId && hasFeedback('languageFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.languageFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.languageFeedback }} -

+ +
+ +

Assign this module to degree programs and specializations.

+ @if (loadingPrograms()) { + + } @else { +
+ @for (row of assignments(); track $index; let i = $index) { +
+ + + +
+ } + +
} - } - +
+
} -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('repetitionFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.repetitionFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.repetitionFeedback }} -

+ @if (currentStepIndex() === 1) { +
+
+ + @if (moduleVersionId && hasFeedback('durationFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.durationFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.durationFeedback }} +

+ } + } +
} - } - - } -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('frequencyFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.frequencyFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.frequencyFeedback }} -

+
+ +
+
+
+ + @if (moduleVersionId && hasFeedback('repetitionFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.repetitionFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.repetitionFeedback }} +

+ } + } +
} - } - - } -
- -
-
- -
-
- - @if (moduleVersionId && hasFeedback('creditsFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.creditsFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.creditsFeedback }} -

+
+ +
+
+
+
+ + @if (moduleVersionId && hasFeedback('creditsFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.creditsFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.creditsFeedback }} +

+ } + } +
} - } - - } -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('hoursTotalFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.hoursTotalFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.hoursTotalFeedback }} -

+
+ +
+
+
+ + @if (moduleVersionId && hasFeedback('hoursTotalFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.hoursTotalFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.hoursTotalFeedback }} +

+ } + } +
} - } - - } -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('hoursSelfStudyFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.hoursSelfStudyFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.hoursSelfStudyFeedback }} -

+
+ +
+
+
+ + @if (moduleVersionId && hasFeedback('hoursSelfStudyFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.hoursSelfStudyFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.hoursSelfStudyFeedback }} +

+ } + } +
} - } - - } -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('hoursPresenceFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.hoursPresenceFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.hoursPresenceFeedback }} -

+
+ +
+
+
+ + @if (moduleVersionId && hasFeedback('hoursPresenceFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.hoursPresenceFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.hoursPresenceFeedback }} +

+ } + } +
} - } - - } -
- +
+ +
+
+
-
-
- -
- - @if (moduleVersionId && hasFeedback('examinationAchievementsFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.examinationAchievementsFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.examinationAchievementsFeedback }} -

- } - } -
} -
- -
-
- - {{ showPrompt['examination'] ? 'Hide Prompt' : 'Customize Prompt' }} - - - Generate Examination Achievements - -
- @if (showPrompt['examination']) { -
- -
- } -
- -
- - @if (moduleVersionId && hasFeedback('recommendedPrerequisitesFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.recommendedPrerequisitesFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.recommendedPrerequisitesFeedback }} -

+ @if (currentStepIndex() === 2) { +
+
+ + @if (moduleVersionId && hasFeedback('examinationAchievementsFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.examinationAchievementsFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.examinationAchievementsFeedback }} +

+ } + } +
} - } - - } -
- -
-
- -
- - @if (moduleVersionId && hasFeedback('contentFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.contentFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.contentFeedback }} -

+
+ +
+
+ + {{ showPrompt['examination'] ? 'Hide Prompt' : 'Customize Prompt' }} + + + Generate Examination Achievements + +
+ @if (showPrompt['examination']) { +
+ +
} - } -
- } -
- -
-
- - {{ showPrompt['content'] ? 'Hide Prompt' : 'Customize Prompt' }} - - Generate Content -
- @if (showPrompt['content']) { -
- -
- } -
- -
- - @if (moduleVersionId && hasFeedback('learningOutcomesFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.learningOutcomesFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.learningOutcomesFeedback }} -

+
+
+ + @if (moduleVersionId && hasFeedback('recommendedPrerequisitesFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.recommendedPrerequisitesFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.recommendedPrerequisitesFeedback }} +

+ } + } +
} - } - - } -
- -
-
- - {{ showPrompt['learning'] ? 'Hide Prompt' : 'Customize Prompt' }} - - Generate Learning Outcomes -
- @if (showPrompt['learning']) { -
- +
+ +
+
} -
- -
- - @if (moduleVersionId && hasFeedback('teachingMethodsFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.teachingMethodsFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.teachingMethodsFeedback }} -

+ @if (currentStepIndex() === 3) { +
+
+ + @if (moduleVersionId && hasFeedback('contentFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.contentFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.contentFeedback }} +

+ } + } +
} - } - - } -
- -
-
- - {{ showPrompt['teaching'] ? 'Hide Prompt' : 'Customize Prompt' }} - - Generate Teaching Methods -
- @if (showPrompt['teaching']) { -
- +
+ +
+
+ + {{ showPrompt['content'] ? 'Hide Prompt' : 'Customize Prompt' }} + + Generate Content +
+ @if (showPrompt['content']) { +
+ +
+ } +
+
+ + @if (moduleVersionId && hasFeedback('learningOutcomesFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.learningOutcomesFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.learningOutcomesFeedback }} +

+ } + } +
+ } +
+ +
+
+ + {{ showPrompt['learning'] ? 'Hide Prompt' : 'Customize Prompt' }} + + Generate Learning Outcomes +
+ @if (showPrompt['learning']) { +
+ +
+ } +
+
+ + @if (moduleVersionId && hasFeedback('teachingMethodsFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.teachingMethodsFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.teachingMethodsFeedback }} +

+ } + } +
+ } +
+ +
+
+ + {{ showPrompt['teaching'] ? 'Hide Prompt' : 'Customize Prompt' }} + + Generate Teaching Methods +
+ @if (showPrompt['teaching']) { +
+ +
+ } +
} -
+ @if (currentStepIndex() === 4) { +
+
+ + @if (moduleVersionId && hasFeedback('mediaFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.mediaFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.mediaFeedback }} +

+ } + } +
+ } +
+ +
+
-
- - @if (moduleVersionId && hasFeedback('mediaFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.mediaFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.mediaFeedback }} -

+
+ + @if (moduleVersionId && hasFeedback('literatureFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.literatureFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.literatureFeedback }} +

+ } + } +
} - } - - } -
- -
-
+
+ +
+
-
- - @if (moduleVersionId && hasFeedback('literatureFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.literatureFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.literatureFeedback }} -

+
+ + @if (moduleVersionId && hasFeedback('responsiblesFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.responsiblesFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.responsiblesFeedback }} +

+ } + } +
} - } - - } -
- -
-
+
+ +
+
-
- - @if (moduleVersionId && hasFeedback('responsiblesFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.responsiblesFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.responsiblesFeedback }} -

+
+ + @if (moduleVersionId && hasFeedback('lvSwsLecturerFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackRole) { + @if (feedback.lvSwsLecturerFeedback) { +

+ {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.lvSwsLecturerFeedback }} +

+ } + } +
} - } - +
+ +
+
+
} -
- -
-
-
- - @if (moduleVersionId && hasFeedback('lvSwsLecturerFeedback')) { - - @for (feedback of feedbacks(); track feedback.feedbackRole) { - @if (feedback.lvSwsLecturerFeedback) { -

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: - {{ feedback.lvSwsLecturerFeedback }} -

- } +
+ Cancel +
+ @if (currentStepIndex() > 0) { + Previous } - - } -
- + @if (isCreateMode()) { + + } + @if (!isCreateMode() && currentStepIndex() < MODULE_EDIT_STEPS.length - 1) { + Next + } + @if (!isCreateMode() && currentStepIndex() === MODULE_EDIT_STEPS.length - 1 && canSubmitForFeedback()) { + Submit for feedback + } + @if (!isCreateMode()) { + + {{ loading() ? 'Saving...' : 'Update proposal' }} + + } +
-
-
- Cancel - - {{ loading() ? 'Saving...' : moduleVersionDto() ? 'Update Proposal' : 'Create Proposal' }} - -
- - @if (error()) { - {{ error() }} - } - + @if (error()) { + {{ error() }} + } + +
diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.ts b/Client/src/app/components/create-edit-base/create-edit-base.component.ts index 212bd8d2..4bcfefb7 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.ts +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.ts @@ -1,16 +1,19 @@ -import { Component, inject, signal } from '@angular/core'; +import { Component, computed, effect, inject, signal } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CompletionServiceRequestDTO, + DegreeProgramDTO, ModuleVersionControllerService, ProposalControllerService, - ModuleVersionUpdateRequestDTO, + ModuleVersionViewDTO, CompletionServiceResponseDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; +import { DegreeProgramsControllerService } from '../../core/modules/openapi/api/degree-programs-controller.service'; import { Location } from '@angular/common'; import { Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; +import { MODULE_EDIT_STEPS } from '../module-edit-stepper/module-edit-steps.config'; @Component({ template: '' @@ -21,14 +24,62 @@ export abstract class ProposalBaseComponent { protected location = inject(Location); protected moduleVersionService = inject(ModuleVersionControllerService); protected proposalService = inject(ProposalControllerService); + protected degreeProgramsService = inject(DegreeProgramsControllerService); + + readonly MODULE_EDIT_STEPS = MODULE_EDIT_STEPS; proposalForm: FormGroup; loading = signal(false); + loadingPrograms = signal(true); error = signal(null); - moduleVersionDto = signal(null); + moduleVersionDto = signal(null); moduleVersionId: number | null = null; feedbacks = signal([]); + degreePrograms = signal([]); + assignments = signal<{ degreeProgramId: number | null; degreeProgramSpecializationId: number | null }[]>([]); + + isCreateMode = computed(() => this.moduleVersionId == null); + currentStepIndex = signal(0); + + stepCompleted = computed(() => { + const form = this.proposalForm; + return MODULE_EDIT_STEPS.map((step) => { + const required = step.requiredControlNames ?? []; + if (required.length === 0) return false; + return required.every((name) => { + const c = form.get(name); + return c && c.valid && c.value != null && c.value !== ''; + }); + }); + }); + + moduleVersionStatus = computed(() => { + const dto = this.moduleVersionDto(); + return dto && 'status' in dto ? (dto as ModuleVersionViewDTO).status : undefined; + }); + + currentVersionFeedbacks = computed(() => { + const dto = this.moduleVersionDto(); + return dto && 'feedbacks' in dto ? (dto as ModuleVersionViewDTO).feedbacks ?? [] : []; + }); + + canSubmitForFeedback = computed(() => { + const dto = this.moduleVersionDto(); + const status = dto && 'status' in dto ? (dto as ModuleVersionViewDTO).status : null; + return status === 'PENDING_SUBMISSION' && this.proposalForm.valid; + }); + + private _syncAssignmentsFromDto = effect(() => { + const dto = this.moduleVersionDto(); + if (dto && 'degreeProgramAssignments' in dto && Array.isArray((dto as ModuleVersionViewDTO).degreeProgramAssignments)) { + const list = (dto as ModuleVersionViewDTO).degreeProgramAssignments ?? []; + this.assignments.set( + list.map((a) => ({ degreeProgramId: a.degreeProgramId ?? null, degreeProgramSpecializationId: a.degreeProgramSpecializationId ?? null })) + ); + } + }); + showPrompt: { [key: string]: boolean } = { examination: false, content: false, @@ -55,11 +106,18 @@ export abstract class ProposalBaseComponent { this.proposalForm = this.formBuilder.group({ bulletPoints: [''], titleEng: ['', Validators.required], + titleDe: [''], levelEng: [''], languageEng: ['English'], repetitionEng: [''], frequencyEng: [''], credits: [null], + hoursLecture: [null], + hoursExercise: [null], + hoursPractical: [null], + hoursSeminar: [null], + firstSemesterAvailable: [''], + successorModuleName: [''], duration: [''], hoursTotal: [null], hoursSelfStudy: [null], @@ -80,6 +138,71 @@ export abstract class ProposalBaseComponent { }); } + goToStep(index: number) { + this.currentStepIndex.set(index); + } + + loadDegreePrograms() { + this.loadingPrograms.set(true); + this.degreeProgramsService.getDegreeProgramsWithSpecializations().subscribe({ + next: (list) => this.degreePrograms.set(list ?? []), + error: () => this.degreePrograms.set([]), + complete: () => this.loadingPrograms.set(false) + }); + } + + addAssignment() { + this.assignments.update((a) => [...a, { degreeProgramId: null, degreeProgramSpecializationId: null }]); + } + + removeAssignment(index: number) { + this.assignments.update((a) => a.filter((_, i) => i !== index)); + } + + setAssignmentProgram(index: number, degreeProgramId: number | null) { + this.assignments.update((a) => { + const next = [...a]; + next[index] = { ...next[index], degreeProgramId, degreeProgramSpecializationId: null }; + return next; + }); + } + + setAssignmentSpecialization(index: number, degreeProgramSpecializationId: number | null) { + this.assignments.update((a) => { + const next = [...a]; + next[index] = { ...next[index], degreeProgramSpecializationId }; + return next; + }); + } + + degreeProgramsAvailableForRow(rowIndex: number): DegreeProgramDTO[] { + const selected = this.assignments().map((a) => a.degreeProgramId).filter(Boolean) as number[]; + return this.degreePrograms().filter((p) => { + const current = this.assignments()[rowIndex]?.degreeProgramId; + return !selected.includes(p.degreeProgramId) || p.degreeProgramId === current; + }); + } + + specializationsForProgram(degreeProgramId: number | null) { + if (!degreeProgramId) return []; + const program = this.degreePrograms().find((p) => p.degreeProgramId === degreeProgramId); + return program?.degreeProgramSpecializations ?? []; + } + + onProgramChange(rowIndex: number) { + this.setAssignmentSpecialization(rowIndex, null); + } + + submitForFeedback(): void { + const dto = this.moduleVersionDto(); + const proposalId = dto && 'proposalId' in dto ? (dto as ModuleVersionViewDTO).proposalId : null; + if (proposalId == null) return; + this.proposalService.submitProposal(proposalId).subscribe({ + next: () => this.router.navigate(['/proposals/view', proposalId]), + error: (err: HttpErrorResponse) => this.error.set(err.error?.message ?? err.error ?? 'Failed to submit') + }); + } + private getCompletionServiceRequestDTO(promptFieldName: string): CompletionServiceRequestDTO { const data: CompletionServiceRequestDTO = { bulletPoints: this.proposalForm.get('bulletPoints')?.value || 'New Module', diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css new file mode 100644 index 00000000..36cb4894 --- /dev/null +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css @@ -0,0 +1,81 @@ +.stepper-nav { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.step-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + border: 1px solid var(--p-content-border-color, #dee2e6); + border-radius: var(--p-border-radius, 6px); + background: var(--p-content-background, #fff); + cursor: pointer; + text-align: left; + width: 100%; + transition: background 0.15s, border-color 0.15s; +} + +.step-item:hover { + background: var(--p-content-hover-background, #f8f9fa); +} + +.step-item--active { + border-color: var(--p-primary-color, #60a5fa); + background: var(--p-content-hover-background, #f8f9fa); +} + +.step-item--completed .step-number { + background: var(--p-success-color, #22c55e); + color: white; +} + +.step-number { + flex-shrink: 0; + width: 1.75rem; + height: 1.75rem; + border-radius: 50%; + background: var(--p-surface-200, #e9ecef); + color: var(--p-text-muted-color, #6c757d); + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + font-weight: 600; +} + +.step-title { + flex: 1; + font-size: 1rem; +} + +.step-feedbacks { + display: flex; + gap: 0.25rem; +} + +.feedback-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--p-surface-400, #adb5bd); +} + +.feedback-dot--PENDING_SUBMISSION { + background: #94a3b8; +} + +.feedback-dot--PENDING_FEEDBACK { + background: #eab308; +} + +.feedback-dot--APPROVED, +.feedback-dot--FEEDBACK_GIVEN { + background: #22c55e; +} + +.feedback-dot--REJECTED { + background: #ef4444; +} diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html new file mode 100644 index 00000000..d2c15a78 --- /dev/null +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html @@ -0,0 +1,14 @@ + diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts new file mode 100644 index 00000000..82f4f120 --- /dev/null +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts @@ -0,0 +1,27 @@ +import { Component, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ButtonModule } from 'primeng/button'; +import type { ModuleEditStepConfig } from './module-edit-steps.config'; +import type { ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; +import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; + +@Component({ + selector: 'app-module-edit-stepper', + standalone: true, + imports: [CommonModule, ButtonModule, FeedbackStatusPipe], + templateUrl: './module-edit-stepper.component.html', + styleUrl: './module-edit-stepper.component.css' +}) +export class ModuleEditStepperComponent { + steps = input.required(); + currentIndex = input.required(); + stepCompleted = input.required(); + feedbacks = input([]); + status = input(); + + stepChange = output(); + + goToStep(index: number) { + this.stepChange.emit(index); + } +} diff --git a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts new file mode 100644 index 00000000..060032a6 --- /dev/null +++ b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts @@ -0,0 +1,56 @@ +export interface ModuleEditStepConfig { + id: string; + title: string; + controlNames: string[]; + requiredControlNames?: string[]; +} + +export const MODULE_EDIT_STEPS: ModuleEditStepConfig[] = [ + { + id: 'basic', + title: 'Basic information & assignment', + controlNames: [ + 'titleEng', + 'titleDe', + 'bulletPoints', + 'credits', + 'frequencyEng', + 'hoursLecture', + 'hoursExercise', + 'hoursPractical', + 'hoursSeminar', + 'firstSemesterAvailable', + 'successorModuleName', + 'levelEng', + 'languageEng' + ], + requiredControlNames: ['titleEng'] + }, + { + id: 'schedule-workload', + title: 'Schedule & workload', + controlNames: ['duration', 'repetitionEng', 'hoursTotal', 'hoursSelfStudy', 'hoursPresence', 'credits'] + }, + { + id: 'examination-prereqs', + title: 'Examination & prerequisites', + controlNames: ['examinationAchievementsEng', 'examinationAchievementsPromptEng', 'recommendedPrerequisitesEng'] + }, + { + id: 'content-learning-teaching', + title: 'Content, learning & teaching', + controlNames: [ + 'contentEng', + 'contentPromptEng', + 'learningOutcomesEng', + 'learningOutcomesPromptEng', + 'teachingMethodsEng', + 'teachingMethodsPromptEng' + ] + }, + { + id: 'media-literature', + title: 'Media, literature & responsibles', + controlNames: ['mediaEng', 'literatureEng', 'responsiblesEng', 'lvSwsLecturerEng'] + } +]; diff --git a/Client/src/app/components/proposal-list-table/proposal-list-table.component.ts b/Client/src/app/components/proposal-list-table/proposal-list-table.component.ts index e23e1480..985100fb 100644 --- a/Client/src/app/components/proposal-list-table/proposal-list-table.component.ts +++ b/Client/src/app/components/proposal-list-table/proposal-list-table.component.ts @@ -2,7 +2,7 @@ import { Component, inject, signal } from '@angular/core'; import { TableModule } from 'primeng/table'; import { ButtonModule } from 'primeng/button'; import { TagModule } from 'primeng/tag'; -import { Proposal, ProposalControllerService, ProposalsCompactDTO } from '../../core/modules/openapi'; +import { ProposalControllerService, ProposalsCompactDTO } from '../../core/modules/openapi'; import { HttpErrorResponse } from '@angular/common/http'; import { Router, RouterModule } from '@angular/router'; @@ -25,7 +25,7 @@ export class ProposalListTableComponent { loading = signal(true); error = signal(null); proposals = signal([]); - proposalEnum = Proposal.StatusEnum; + proposalEnum = ProposalsCompactDTO.StatusEnum; user = this.securityStore.user; constructor() { diff --git a/Client/src/app/core/modules/openapi/.openapi-generator/FILES b/Client/src/app/core/modules/openapi/.openapi-generator/FILES index da40739c..4a76023e 100644 --- a/Client/src/app/core/modules/openapi/.openapi-generator/FILES +++ b/Client/src/app/core/modules/openapi/.openapi-generator/FILES @@ -34,16 +34,14 @@ model/feedback-list-item-dto.ts model/feedback.ts model/give-feedback-dto.ts model/models.ts +model/module-degree-program-assignment-dto.ts model/module-version-compact-dto.ts model/module-version-update-request-dto.ts -model/module-version-update-response-dto.ts model/module-version-view-dto.ts model/module-version-view-feedback-dto.ts -model/module-version.ts model/page-response-dto-user-dto.ts model/proposal-request-dto.ts model/proposal-view-dto.ts -model/proposal.ts model/proposals-compact-dto.ts model/responsible-user-dto.ts model/similar-module-dto.ts diff --git a/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts b/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts index 01150472..e319f745 100644 --- a/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts +++ b/Client/src/app/core/modules/openapi/api/degree-programs-controller.service.ts @@ -433,6 +433,65 @@ export class DegreeProgramsControllerService implements DegreeProgramsController ); } + /** + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getDegreeProgramsWithSpecializations(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getDegreeProgramsWithSpecializations(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getDegreeProgramsWithSpecializations(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getDegreeProgramsWithSpecializations(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/degree-programs/with-specializations`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + /** * @param degreeProgramId * @param degreeProgramSpecializationId diff --git a/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts index 70fe8197..4418d811 100644 --- a/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts +++ b/Client/src/app/core/modules/openapi/api/degree-programs-controller.serviceInterface.ts @@ -60,6 +60,12 @@ export interface DegreeProgramsControllerServiceInterface { */ getDegreeProgram(id: number, extraHttpRequestParams?: any): Observable; + /** + * + * + */ + getDegreeProgramsWithSpecializations(extraHttpRequestParams?: any): Observable>; + /** * * diff --git a/Client/src/app/core/modules/openapi/api/feedback-controller.service.ts b/Client/src/app/core/modules/openapi/api/feedback-controller.service.ts index 528ee86b..b60bad14 100644 --- a/Client/src/app/core/modules/openapi/api/feedback-controller.service.ts +++ b/Client/src/app/core/modules/openapi/api/feedback-controller.service.ts @@ -25,7 +25,7 @@ import { FeedbackListItemDto } from '../model/feedback-list-item-dto'; // @ts-ignore import { GiveFeedbackDTO } from '../model/give-feedback-dto'; // @ts-ignore -import { ModuleVersionUpdateRequestDTO } from '../model/module-version-update-request-dto'; +import { ModuleVersionViewDTO } from '../model/module-version-view-dto'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -280,9 +280,9 @@ export class FeedbackControllerService implements FeedbackControllerServiceInter * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getModuleVersionOfFeedback(feedbackId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public getModuleVersionOfFeedback(feedbackId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getModuleVersionOfFeedback(feedbackId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getModuleVersionOfFeedback(feedbackId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getModuleVersionOfFeedback(feedbackId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getModuleVersionOfFeedback(feedbackId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public getModuleVersionOfFeedback(feedbackId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (feedbackId === null || feedbackId === undefined) { throw new Error('Required parameter feedbackId was null or undefined when calling getModuleVersionOfFeedback.'); @@ -325,7 +325,7 @@ export class FeedbackControllerService implements FeedbackControllerServiceInter } let localVarPath = `/api/feedbacks/module-version-of-feedback/${this.configuration.encodeParam({name: "feedbackId", value: feedbackId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, diff --git a/Client/src/app/core/modules/openapi/api/feedback-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/feedback-controller.serviceInterface.ts index bffb04fa..e764692b 100644 --- a/Client/src/app/core/modules/openapi/api/feedback-controller.serviceInterface.ts +++ b/Client/src/app/core/modules/openapi/api/feedback-controller.serviceInterface.ts @@ -15,7 +15,7 @@ import { Feedback } from '../model/models'; import { FeedbackDTO } from '../model/models'; import { FeedbackListItemDto } from '../model/models'; import { GiveFeedbackDTO } from '../model/models'; -import { ModuleVersionUpdateRequestDTO } from '../model/models'; +import { ModuleVersionViewDTO } from '../model/models'; import { Configuration } from '../configuration'; @@ -51,7 +51,7 @@ export interface FeedbackControllerServiceInterface { * * @param feedbackId */ - getModuleVersionOfFeedback(feedbackId: number, extraHttpRequestParams?: any): Observable; + getModuleVersionOfFeedback(feedbackId: number, extraHttpRequestParams?: any): Observable; /** * diff --git a/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts b/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts index 03f4f3b0..8eec0648 100644 --- a/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts +++ b/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts @@ -23,8 +23,6 @@ import { CompletionServiceResponseDTO } from '../model/completion-service-respon // @ts-ignore import { ModuleVersionUpdateRequestDTO } from '../model/module-version-update-request-dto'; // @ts-ignore -import { ModuleVersionUpdateResponseDTO } from '../model/module-version-update-response-dto'; -// @ts-ignore import { ModuleVersionViewDTO } from '../model/module-version-view-dto'; // @ts-ignore import { ModuleVersionViewFeedbackDTO } from '../model/module-version-view-feedback-dto'; @@ -517,9 +515,9 @@ export class ModuleVersionControllerService implements ModuleVersionControllerSe * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public getModuleVersionUpdateDtoFromId(moduleVersionId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (moduleVersionId === null || moduleVersionId === undefined) { throw new Error('Required parameter moduleVersionId was null or undefined when calling getModuleVersionUpdateDtoFromId.'); @@ -562,7 +560,7 @@ export class ModuleVersionControllerService implements ModuleVersionControllerSe } let localVarPath = `/api/module-versions/${this.configuration.encodeParam({name: "moduleVersionId", value: moduleVersionId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -707,9 +705,9 @@ export class ModuleVersionControllerService implements ModuleVersionControllerSe * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (moduleVersionId === null || moduleVersionId === undefined) { throw new Error('Required parameter moduleVersionId was null or undefined when calling updateModuleVersion.'); @@ -764,7 +762,7 @@ export class ModuleVersionControllerService implements ModuleVersionControllerSe } let localVarPath = `/api/module-versions/${this.configuration.encodeParam({name: "moduleVersionId", value: moduleVersionId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; - return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, body: moduleVersionUpdateRequestDTO, diff --git a/Client/src/app/core/modules/openapi/api/module-version-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/module-version-controller.serviceInterface.ts index 879faf38..c1ae9d93 100644 --- a/Client/src/app/core/modules/openapi/api/module-version-controller.serviceInterface.ts +++ b/Client/src/app/core/modules/openapi/api/module-version-controller.serviceInterface.ts @@ -14,7 +14,6 @@ import { Observable } from 'rxjs'; import { CompletionServiceRequestDTO } from '../model/models'; import { CompletionServiceResponseDTO } from '../model/models'; import { ModuleVersionUpdateRequestDTO } from '../model/models'; -import { ModuleVersionUpdateResponseDTO } from '../model/models'; import { ModuleVersionViewDTO } from '../model/models'; import { ModuleVersionViewFeedbackDTO } from '../model/models'; import { SimilarModuleDTO } from '../model/models'; @@ -75,7 +74,7 @@ export interface ModuleVersionControllerServiceInterface { * * @param moduleVersionId */ - getModuleVersionUpdateDtoFromId(moduleVersionId: number, extraHttpRequestParams?: any): Observable; + getModuleVersionUpdateDtoFromId(moduleVersionId: number, extraHttpRequestParams?: any): Observable; /** * @@ -97,6 +96,6 @@ export interface ModuleVersionControllerServiceInterface { * @param moduleVersionId * @param moduleVersionUpdateRequestDTO */ - updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, extraHttpRequestParams?: any): Observable; + updateModuleVersion(moduleVersionId: number, moduleVersionUpdateRequestDTO: ModuleVersionUpdateRequestDTO, extraHttpRequestParams?: any): Observable; } diff --git a/Client/src/app/core/modules/openapi/api/proposal-controller.service.ts b/Client/src/app/core/modules/openapi/api/proposal-controller.service.ts index baa121bd..306e7933 100644 --- a/Client/src/app/core/modules/openapi/api/proposal-controller.service.ts +++ b/Client/src/app/core/modules/openapi/api/proposal-controller.service.ts @@ -19,8 +19,6 @@ import { Observable } from 'rxjs'; // @ts-ignore import { AddModuleVersionDTO } from '../model/add-module-version-dto'; // @ts-ignore -import { Proposal } from '../model/proposal'; -// @ts-ignore import { ProposalRequestDTO } from '../model/proposal-request-dto'; // @ts-ignore import { ProposalViewDTO } from '../model/proposal-view-dto'; @@ -242,9 +240,9 @@ export class ProposalControllerService implements ProposalControllerServiceInter * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public createProposal(proposalRequestDTO: ProposalRequestDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public createProposal(proposalRequestDTO: ProposalRequestDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public createProposal(proposalRequestDTO: ProposalRequestDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createProposal(proposalRequestDTO: ProposalRequestDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createProposal(proposalRequestDTO: ProposalRequestDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createProposal(proposalRequestDTO: ProposalRequestDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public createProposal(proposalRequestDTO: ProposalRequestDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (proposalRequestDTO === null || proposalRequestDTO === undefined) { throw new Error('Required parameter proposalRequestDTO was null or undefined when calling createProposal.'); @@ -296,7 +294,7 @@ export class ProposalControllerService implements ProposalControllerServiceInter } let localVarPath = `/api/proposals`; - return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, body: proposalRequestDTO, diff --git a/Client/src/app/core/modules/openapi/api/proposal-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/proposal-controller.serviceInterface.ts index ff92ddb5..112f9a3d 100644 --- a/Client/src/app/core/modules/openapi/api/proposal-controller.serviceInterface.ts +++ b/Client/src/app/core/modules/openapi/api/proposal-controller.serviceInterface.ts @@ -12,7 +12,6 @@ import { HttpHeaders } from '@angular/comm import { Observable } from 'rxjs'; import { AddModuleVersionDTO } from '../model/models'; -import { Proposal } from '../model/models'; import { ProposalRequestDTO } from '../model/models'; import { ProposalViewDTO } from '../model/models'; import { ProposalsCompactDTO } from '../model/models'; @@ -45,7 +44,7 @@ export interface ProposalControllerServiceInterface { * * @param proposalRequestDTO */ - createProposal(proposalRequestDTO: ProposalRequestDTO, extraHttpRequestParams?: any): Observable; + createProposal(proposalRequestDTO: ProposalRequestDTO, extraHttpRequestParams?: any): Observable; /** * diff --git a/Client/src/app/core/modules/openapi/model/feedback.ts b/Client/src/app/core/modules/openapi/model/feedback.ts index 89c91b77..1415112b 100644 --- a/Client/src/app/core/modules/openapi/model/feedback.ts +++ b/Client/src/app/core/modules/openapi/model/feedback.ts @@ -54,8 +54,8 @@ export interface Feedback { responsiblesAccepted?: boolean; lvSwsLecturerFeedback?: string; lvSwsLecturerAccepted?: boolean; - allFeedbackPositive?: boolean; feedbackGiven?: boolean; + allFeedbackPositive?: boolean; comment?: string; } export namespace Feedback { diff --git a/Client/src/app/core/modules/openapi/model/models.ts b/Client/src/app/core/modules/openapi/model/models.ts index 0ca448a1..701186b8 100644 --- a/Client/src/app/core/modules/openapi/model/models.ts +++ b/Client/src/app/core/modules/openapi/model/models.ts @@ -10,14 +10,12 @@ export * from './feedback'; export * from './feedback-dto'; export * from './feedback-list-item-dto'; export * from './give-feedback-dto'; -export * from './module-version'; +export * from './module-degree-program-assignment-dto'; export * from './module-version-compact-dto'; export * from './module-version-update-request-dto'; -export * from './module-version-update-response-dto'; export * from './module-version-view-dto'; export * from './module-version-view-feedback-dto'; export * from './page-response-dto-user-dto'; -export * from './proposal'; export * from './proposal-request-dto'; export * from './proposal-view-dto'; export * from './proposals-compact-dto'; diff --git a/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts b/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts new file mode 100644 index 00000000..7ce6a604 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ModuleDegreeProgramAssignmentDTO { + degreeProgramId: number; + degreeProgramSpecializationId: number; +} + diff --git a/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts index 30be8097..72bc204e 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts @@ -7,6 +7,7 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { ModuleDegreeProgramAssignmentDTO } from './module-degree-program-assignment-dto'; export interface ModuleVersionUpdateRequestDTO { @@ -17,10 +18,18 @@ export interface ModuleVersionUpdateRequestDTO { isComplete?: boolean; bulletPoints?: string; titleEng?: string; + titleDe?: string; levelEng?: string; languageEng?: ModuleVersionUpdateRequestDTO.LanguageEngEnum; frequencyEng?: string; credits?: number; + hoursLecture?: number; + hoursExercise?: number; + hoursPractical?: number; + hoursSeminar?: number; + firstSemesterAvailable?: string; + successorModuleName?: string; + degreeProgramAssignments?: Array; duration?: string; hoursTotal?: number; hoursSelfStudy?: number; diff --git a/Client/src/app/core/modules/openapi/model/module-version-update-response-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-update-response-dto.ts deleted file mode 100644 index e801e25d..00000000 --- a/Client/src/app/core/modules/openapi/model/module-version-update-response-dto.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * OpenAPI definition - * - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -export interface ModuleVersionUpdateResponseDTO { - moduleVersionId?: number; - version?: number; - moduleId?: string; - status?: ModuleVersionUpdateResponseDTO.StatusEnum; - isComplete?: boolean; - bulletPoints?: string; - titleEng?: string; - levelEng?: string; - languageEng?: ModuleVersionUpdateResponseDTO.LanguageEngEnum; - frequencyEng?: string; - credits?: number; - duration?: string; - hoursTotal?: number; - hoursSelfStudy?: number; - hoursPresence?: number; - examinationAchievementsEng?: string; - examinationAchievementsPromptEng?: string; - repetitionEng?: string; - recommendedPrerequisitesEng?: string; - contentEng?: string; - contentPromptEng?: string; - learningOutcomesEng?: string; - learningOutcomesPromptEng?: string; - teachingMethodsEng?: string; - teachingMethodsPromptEng?: string; - mediaEng?: string; - literatureEng?: string; - responsiblesEng?: string; - lvSwsLecturerEng?: string; - proposalId: number; -} -export namespace ModuleVersionUpdateResponseDTO { - export type StatusEnum = 'PENDING_SUBMISSION' | 'PENDING_FEEDBACK' | 'ACCEPTED' | 'FEEDBACK_GIVEN' | 'REJECTED' | 'OBSOLETE' | 'CANCELLED'; - export const StatusEnum = { - PendingSubmission: 'PENDING_SUBMISSION' as StatusEnum, - PendingFeedback: 'PENDING_FEEDBACK' as StatusEnum, - Accepted: 'ACCEPTED' as StatusEnum, - FeedbackGiven: 'FEEDBACK_GIVEN' as StatusEnum, - Rejected: 'REJECTED' as StatusEnum, - Obsolete: 'OBSOLETE' as StatusEnum, - Cancelled: 'CANCELLED' as StatusEnum - }; - export type LanguageEngEnum = 'English' | 'German'; - export const LanguageEngEnum = { - English: 'English' as LanguageEngEnum, - German: 'German' as LanguageEngEnum - }; -} - - diff --git a/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts index 1c0a1955..1c49dcd0 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts @@ -8,6 +8,7 @@ * Do not edit the class manually. */ import { ModuleVersionViewFeedbackDTO } from './module-version-view-feedback-dto'; +import { ModuleDegreeProgramAssignmentDTO } from './module-degree-program-assignment-dto'; export interface ModuleVersionViewDTO { @@ -19,10 +20,18 @@ export interface ModuleVersionViewDTO { status?: ModuleVersionViewDTO.StatusEnum; bulletPoints?: string; titleEng?: string; + titleDe?: string; levelEng?: string; languageEng?: ModuleVersionViewDTO.LanguageEngEnum; frequencyEng?: string; credits?: number; + hoursLecture?: number; + hoursExercise?: number; + hoursPractical?: number; + hoursSeminar?: number; + firstSemesterAvailable?: string; + successorModuleName?: string; + degreeProgramAssignments?: Array; duration?: string; hoursTotal?: number; hoursSelfStudy?: number; diff --git a/Client/src/app/core/modules/openapi/model/module-version.ts b/Client/src/app/core/modules/openapi/model/module-version.ts deleted file mode 100644 index c2671141..00000000 --- a/Client/src/app/core/modules/openapi/model/module-version.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * OpenAPI definition - * - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { Feedback } from './feedback'; - - -export interface ModuleVersion { - moduleVersionId?: number; - version?: number; - moduleId?: string; - creationDate?: string; - status: ModuleVersion.StatusEnum; - bulletPoints?: string; - titleEng?: string; - levelEng?: string; - languageEng?: ModuleVersion.LanguageEngEnum; - frequencyEng?: string; - credits?: number; - duration?: string; - hoursTotal?: number; - hoursSelfStudy?: number; - hoursPresence?: number; - examinationAchievementsEng?: string; - examinationAchievementsPromptEng?: string; - repetitionEng?: string; - recommendedPrerequisitesEng?: string; - contentEng?: string; - contentPromptEng?: string; - learningOutcomesEng?: string; - learningOutcomesPromptEng?: string; - teachingMethodsEng?: string; - teachingMethodsPromptEng?: string; - mediaEng?: string; - literatureEng?: string; - responsiblesEng?: string; - lvSwsLecturerEng?: string; - requiredFeedbacks?: Array; - feedbackGiven?: boolean; -} -export namespace ModuleVersion { - export type StatusEnum = 'PENDING_SUBMISSION' | 'PENDING_FEEDBACK' | 'ACCEPTED' | 'FEEDBACK_GIVEN' | 'REJECTED' | 'OBSOLETE' | 'CANCELLED'; - export const StatusEnum = { - PendingSubmission: 'PENDING_SUBMISSION' as StatusEnum, - PendingFeedback: 'PENDING_FEEDBACK' as StatusEnum, - Accepted: 'ACCEPTED' as StatusEnum, - FeedbackGiven: 'FEEDBACK_GIVEN' as StatusEnum, - Rejected: 'REJECTED' as StatusEnum, - Obsolete: 'OBSOLETE' as StatusEnum, - Cancelled: 'CANCELLED' as StatusEnum - }; - export type LanguageEngEnum = 'English' | 'German'; - export const LanguageEngEnum = { - English: 'English' as LanguageEngEnum, - German: 'German' as LanguageEngEnum - }; -} - - diff --git a/Client/src/app/core/modules/openapi/model/proposal-request-dto.ts b/Client/src/app/core/modules/openapi/model/proposal-request-dto.ts index 788a15e9..14704f7b 100644 --- a/Client/src/app/core/modules/openapi/model/proposal-request-dto.ts +++ b/Client/src/app/core/modules/openapi/model/proposal-request-dto.ts @@ -7,15 +7,24 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { ModuleDegreeProgramAssignmentDTO } from './module-degree-program-assignment-dto'; export interface ProposalRequestDTO { bulletPoints?: string; titleEng: string; + titleDe?: string; levelEng?: string; languageEng?: ProposalRequestDTO.LanguageEngEnum; frequencyEng?: string; credits?: number; + hoursLecture?: number; + hoursExercise?: number; + hoursPractical?: number; + hoursSeminar?: number; + firstSemesterAvailable?: string; + successorModuleName?: string; + degreeProgramAssignments?: Array; duration?: string; hoursTotal?: number; hoursSelfStudy?: number; diff --git a/Client/src/app/core/modules/openapi/model/proposal.ts b/Client/src/app/core/modules/openapi/model/proposal.ts deleted file mode 100644 index b0c8dd93..00000000 --- a/Client/src/app/core/modules/openapi/model/proposal.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * OpenAPI definition - * - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { ModuleVersionViewFeedbackDTO } from './module-version-view-feedback-dto'; -import { User } from './user'; -import { ModuleVersion } from './module-version'; - - -export interface Proposal { - proposalId?: number; - createdBy?: User; - creationDate?: string; - status: Proposal.StatusEnum; - moduleVersions?: Array; - previousModuleVersionFeedback?: Array; -} -export namespace Proposal { - export type StatusEnum = 'PENDING_SUBMISSION' | 'PENDING_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED'; - export const StatusEnum = { - PendingSubmission: 'PENDING_SUBMISSION' as StatusEnum, - PendingFeedback: 'PENDING_FEEDBACK' as StatusEnum, - Accepted: 'ACCEPTED' as StatusEnum, - RequiresReview: 'REQUIRES_REVIEW' as StatusEnum, - Rejected: 'REJECTED' as StatusEnum - }; -} - - diff --git a/Client/src/app/core/modules/openapi/model/similar-module-dto.ts b/Client/src/app/core/modules/openapi/model/similar-module-dto.ts index ad23d307..5d41a7b1 100644 --- a/Client/src/app/core/modules/openapi/model/similar-module-dto.ts +++ b/Client/src/app/core/modules/openapi/model/similar-module-dto.ts @@ -14,7 +14,7 @@ export interface SimilarModuleDTO { titleEng?: string; levelEng?: string; languageEng?: string; - frequencyEng?: string; + semesterAvailability?: string; credits?: number; duration?: string; hoursTotal?: number; diff --git a/Client/src/app/pages/feedback-view/feedback-view.component.ts b/Client/src/app/pages/feedback-view/feedback-view.component.ts index 1b355839..9b18779f 100644 --- a/Client/src/app/pages/feedback-view/feedback-view.component.ts +++ b/Client/src/app/pages/feedback-view/feedback-view.component.ts @@ -1,7 +1,6 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; - import { Component, inject, signal } from '@angular/core'; -import { FeedbackControllerService, ModuleVersionUpdateRequestDTO, FeedbackDTO, GiveFeedbackDTO, ModuleVersionControllerService } from '../../core/modules/openapi'; +import { FeedbackControllerService, ModuleVersionViewDTO, FeedbackDTO, GiveFeedbackDTO, ModuleVersionControllerService } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { FormBuilder, FormGroup, FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; @@ -28,7 +27,7 @@ export class FeedbackViewComponent { private breadcrumbLabels = inject(BreadcrumbLabelsService); feedbackForm: FormGroup; feedbackId: number | null = null; - moduleVersion = signal(null); + moduleVersion = signal(null); loading = signal(true); error = signal(null); rejectionReason: string = ''; @@ -39,7 +38,7 @@ export class FeedbackViewComponent { fieldStates: Record = {}; fieldFeedback: Record = {}; - getModuleVersionProperty(key: keyof ModuleVersionUpdateRequestDTO): string | undefined { + getModuleVersionProperty(key: keyof ModuleVersionViewDTO): string | undefined { const mv = this.moduleVersion(); return mv ? mv[key]?.toString() : undefined; } @@ -123,7 +122,7 @@ export class FeedbackViewComponent { this.loading.set(true); if (feedbackId) { this.feedbackService.getModuleVersionOfFeedback(feedbackId).subscribe({ - next: (response: ModuleVersionUpdateRequestDTO) => { + next: (response: ModuleVersionViewDTO) => { this.moduleVersion.set(response); this.breadcrumbLabels.feedbackLabel.set(response?.titleEng ?? null); }, diff --git a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts index b8076bdd..939b7820 100644 --- a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts +++ b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts @@ -1,11 +1,17 @@ import { Component, inject } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { ProposalBaseComponent } from '../../components/create-edit-base/create-edit-base.component'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; -import { ModuleVersionUpdateRequestDTO, ModuleVersionUpdateResponseDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; +import { ModuleEditStepperComponent } from '../../components/module-edit-stepper/module-edit-stepper.component'; +import { + ModuleDegreeProgramAssignmentDTO, + ModuleVersionUpdateRequestDTO, + ModuleVersionViewDTO, + ModuleVersionViewFeedbackDTO +} from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { ToggleButtonGroupComponent } from '../../components/toggle-button-group/toggle-button-group.component'; import { ButtonModule } from 'primeng/button'; @@ -13,23 +19,30 @@ import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { InputNumberModule } from 'primeng/inputnumber'; import { MessageModule } from 'primeng/message'; +import { SelectModule } from 'primeng/select'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; @Component({ selector: 'app-module-version-edit', standalone: true, imports: [ ReactiveFormsModule, + FormsModule, CommonModule, RouterModule, FeedbackDepartmentPipe, ToggleButtonGroupComponent, + ModuleEditStepperComponent, ButtonModule, InputTextModule, TextareaModule, InputNumberModule, - MessageModule + MessageModule, + SelectModule, + ProgressSpinnerModule ], - templateUrl: '../../components/create-edit-base/create-edit-base.component.html' + templateUrl: '../../components/create-edit-base/create-edit-base.component.html', + styleUrl: '../../components/create-edit-base/create-edit-base-layout.css' }) export class ModuleVersionEditComponent extends ProposalBaseComponent { override moduleVersionId: number; @@ -40,6 +53,7 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent { constructor(route: ActivatedRoute) { super(); this.moduleVersionId = Number(route.snapshot.paramMap.get('versionId')); + this.loadDegreePrograms(); this.fetchModuleVersion(this.moduleVersionId); this.fetchPreviousModuleVersionFeedback(this.moduleVersionId); } @@ -47,7 +61,7 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent { fetchModuleVersion(moduleVersionId: number) { this.moduleLoading = true; this.moduleVersionService.getModuleVersionUpdateDtoFromId(moduleVersionId).subscribe({ - next: (response: ModuleVersionUpdateRequestDTO) => { + next: (response: ModuleVersionViewDTO) => { this.proposalForm.patchValue(response); this.moduleVersionDto.set(response); const version = response?.version; @@ -74,30 +88,24 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent { }); } - override async onSubmit() { - if (this.proposalForm.valid && this.moduleVersionId) { - this.loading.set(true); - this.error.set(null); - - const proposalData: ModuleVersionUpdateRequestDTO = { - ...this.proposalForm.value, - moduleVersionId: this.moduleVersionId - }; - - this.moduleVersionService.updateModuleVersion(this.moduleVersionId, proposalData).subscribe({ - next: (response: ModuleVersionUpdateResponseDTO) => { - this.proposalForm.reset(); - this.router.navigate(['/proposals/view', response.proposalId], { - queryParams: { created: true } - }); - }, - error: (err: HttpErrorResponse) => { - console.error(err); - this.error.set(err.error); - this.loading.set(false); - }, - complete: () => this.loading.set(false) - }); - } + override onSubmit(): void { + if (this.moduleVersionId == null) return; + this.loading.set(true); + this.error.set(null); + const rawAssignments = this.assignments().filter((a) => a.degreeProgramId != null && a.degreeProgramSpecializationId != null); + const degreeProgramAssignments: ModuleDegreeProgramAssignmentDTO[] = rawAssignments.map((a) => ({ + degreeProgramId: a.degreeProgramId!, + degreeProgramSpecializationId: a.degreeProgramSpecializationId! + })); + const payload: ModuleVersionUpdateRequestDTO = { + ...this.proposalForm.value, + moduleVersionId: this.moduleVersionId, + degreeProgramAssignments: degreeProgramAssignments.length > 0 ? degreeProgramAssignments : undefined + }; + this.moduleVersionService.updateModuleVersion(this.moduleVersionId, payload).subscribe({ + next: (response: ModuleVersionViewDTO) => this.moduleVersionDto.set(response), + error: (err: HttpErrorResponse) => this.error.set(err.error), + complete: () => this.loading.set(false) + }); } } diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.ts b/Client/src/app/pages/module-version-view/module-version-view.component.ts index d25d11f6..cc12cf97 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.ts +++ b/Client/src/app/pages/module-version-view/module-version-view.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { Component, inject, signal } from '@angular/core'; import { RouterModule, ActivatedRoute } from '@angular/router'; -import { ModuleVersionControllerService, ModuleVersionViewDTO, ModuleVersion, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; +import { ModuleVersionControllerService, ModuleVersionViewDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; @@ -49,7 +49,7 @@ export class ModuleVersionViewComponent { moduleVersionId: number | null = null; loading = signal(true); moduleVersionDto = signal(null); - moduleVersionStatus = ModuleVersion.StatusEnum; + moduleVersionStatus = ModuleVersionViewDTO.StatusEnum; error = signal(null); moduleFields: ModuleField[] = [ diff --git a/Client/src/app/pages/proposal-create/proposal-create.component.ts b/Client/src/app/pages/proposal-create/proposal-create.component.ts index 64f9c502..20743332 100644 --- a/Client/src/app/pages/proposal-create/proposal-create.component.ts +++ b/Client/src/app/pages/proposal-create/proposal-create.component.ts @@ -1,53 +1,81 @@ import { Component } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; - +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { ProposalBaseComponent } from '../../components/create-edit-base/create-edit-base.component'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; import { ToggleButtonGroupComponent } from '../../components/toggle-button-group/toggle-button-group.component'; +import { ModuleEditStepperComponent } from '../../components/module-edit-stepper/module-edit-stepper.component'; +import { ModuleDegreeProgramAssignmentDTO, ProposalRequestDTO } from '../../core/modules/openapi'; import { ButtonModule } from 'primeng/button'; import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { InputNumberModule } from 'primeng/inputnumber'; import { MessageModule } from 'primeng/message'; +import { SelectModule } from 'primeng/select'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; @Component({ selector: 'app-proposal-create', standalone: true, imports: [ ReactiveFormsModule, + FormsModule, RouterModule, FeedbackDepartmentPipe, ToggleButtonGroupComponent, + ModuleEditStepperComponent, ButtonModule, InputTextModule, TextareaModule, InputNumberModule, - MessageModule -], - templateUrl: '../../components/create-edit-base/create-edit-base.component.html' + MessageModule, + SelectModule, + ProgressSpinnerModule + ], + templateUrl: '../../components/create-edit-base/create-edit-base.component.html', + styleUrl: '../../components/create-edit-base/create-edit-base-layout.css' }) export class ProposalCreateComponent extends ProposalBaseComponent { - override async onSubmit() { - if (this.proposalForm.valid) { - this.loading.set(true); - this.error.set(null); + constructor() { + super(); + this.loadDegreePrograms(); + } - const proposalData = this.proposalForm.value; - this.proposalService.createProposal(proposalData).subscribe({ - next: (response) => { - this.proposalForm.reset(); - this.router.navigate(['/proposals/view', response.proposalId], { - queryParams: { created: true } - }); - }, - error: (err: HttpErrorResponse) => { - this.error.set(err.error); - this.loading.set(false); - }, - complete: () => this.loading.set(false) - }); + override async onSubmit(): Promise { + if (!this.proposalForm.valid) return; + + const rawAssignments = this.assignments().filter((a) => a.degreeProgramId != null && a.degreeProgramSpecializationId != null); + const programIds = rawAssignments.map((a) => a.degreeProgramId!); + if (new Set(programIds).size !== programIds.length) { + this.error.set('Each degree program can only be assigned once.'); + return; + } + const degreeProgramAssignments: ModuleDegreeProgramAssignmentDTO[] = rawAssignments.map((a) => ({ + degreeProgramId: a.degreeProgramId!, + degreeProgramSpecializationId: a.degreeProgramSpecializationId! + })); + const body: ProposalRequestDTO = { + ...this.proposalForm.value, + titleEng: this.proposalForm.value.titleEng?.trim() ?? '', + degreeProgramAssignments: degreeProgramAssignments.length > 0 ? degreeProgramAssignments : undefined + }; + this.loading.set(true); + this.error.set(null); + try { + const res = await firstValueFrom(this.proposalService.createProposal(body)); + const proposalId = res.proposalId; + const moduleVersionId = res.latestModuleVersion?.moduleVersionId; + if (proposalId != null && moduleVersionId != null) { + await this.router.navigate(['/proposals/view', proposalId, 'version', moduleVersionId, 'edit'], { queryParams: { created: true } }); + } else { + this.error.set('Unexpected response from server.'); + } + } catch (err: unknown) { + this.error.set(err instanceof HttpErrorResponse ? (err.error ?? err.message) : 'Failed to create proposal.'); + } finally { + this.loading.set(false); } } } diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.ts b/Client/src/app/pages/proposal-view/proposal-view.component.ts index add3a87a..164335f4 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.ts +++ b/Client/src/app/pages/proposal-view/proposal-view.component.ts @@ -1,5 +1,5 @@ import { Component, inject, signal } from '@angular/core'; -import { AddModuleVersionDTO, ModuleVersion, Proposal, ProposalControllerService, ProposalViewDTO } from '../../core/modules/openapi'; +import { AddModuleVersionDTO, ModuleVersionCompactDTO, ProposalControllerService, ProposalViewDTO } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; @@ -48,8 +48,8 @@ export class ProposalViewComponent { loading = signal(true); error = signal(null); proposal = signal(null); - proposalStatusEnum = Proposal.StatusEnum; - moduleStatusEnum = ModuleVersion.StatusEnum; + proposalStatusEnum = ProposalViewDTO.StatusEnum; + moduleStatusEnum = ModuleVersionCompactDTO.StatusEnum; constructor() { this.fetchProposal(); diff --git a/Client/src/app/pipes/proposalStatus.pipe.ts b/Client/src/app/pipes/proposalStatus.pipe.ts index 71b9de43..3bbef4cb 100644 --- a/Client/src/app/pipes/proposalStatus.pipe.ts +++ b/Client/src/app/pipes/proposalStatus.pipe.ts @@ -1,9 +1,9 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { Proposal } from '../core/modules/openapi'; +import { ProposalViewDTO } from '../core/modules/openapi'; import { Tag } from 'primeng/tag'; @Pipe({ name: 'statusDisplay', standalone: true }) export class StatusDisplayPipe implements PipeTransform { - transform(status: Proposal.StatusEnum): { text: string; severity: Tag['severity'] } { + transform(status: ProposalViewDTO.StatusEnum): { text: string; severity: Tag['severity'] } { switch (status) { case 'PENDING_SUBMISSION': return { text: 'Pending Submission', severity: 'secondary' }; @@ -23,7 +23,7 @@ export class StatusDisplayPipe implements PipeTransform { @Pipe({ name: 'statusInfo', standalone: true }) export class StatusInfoPipeline implements PipeTransform { - transform(status: Proposal.StatusEnum): string { + transform(status: ProposalViewDTO.StatusEnum): string { switch (status) { case 'PENDING_SUBMISSION': return 'This module proposal is pending submission. Please fill all module information fields and submit the module proposal. After submission the necessary staff will be notified to review your proposal.'; diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java b/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java index 44d8bcbf..7e84e4bd 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/DegreeProgramsController.java @@ -25,6 +25,12 @@ public ResponseEntity> getAllDegreePrograms() { return ResponseEntity.ok(degreeProgramService.getAllDegreePrograms()); } + @GetMapping("/with-specializations") + @PreAuthorize("hasAnyRole('ADMIN', 'PROFESSOR')") + public ResponseEntity> getDegreeProgramsWithSpecializations() { + return ResponseEntity.ok(degreeProgramService.getAllDegreeProgramsWithSpecializations()); + } + @GetMapping("/{id}") public ResponseEntity getDegreeProgram(@PathVariable Long id) { return ResponseEntity.ok(degreeProgramService.getDegreeProgram(id)); diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java b/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java index deb9d4f9..489c0511 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java @@ -2,7 +2,7 @@ import modulemanagement.ls1.dtos.FeedbackDTO; import modulemanagement.ls1.dtos.FeedbackListItemDto; -import modulemanagement.ls1.dtos.ModuleVersionUpdateRequestDTO; +import modulemanagement.ls1.dtos.ModuleVersionViewDTO; import modulemanagement.ls1.dtos.GiveFeedbackDTO; import modulemanagement.ls1.models.Feedback; import modulemanagement.ls1.models.User; @@ -39,8 +39,8 @@ public ResponseEntity> getFeedbacksForAuthenticatedUse @GetMapping("/module-version-of-feedback/{feedbackId}") @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") - public ResponseEntity getModuleVersionOfFeedback(@PathVariable Long feedbackId) { - ModuleVersionUpdateRequestDTO dto = feedbackService.getModuleVersionOfFeedback(feedbackId); + public ResponseEntity getModuleVersionOfFeedback(@PathVariable Long feedbackId) { + ModuleVersionViewDTO dto = feedbackService.getModuleVersionOfFeedback(feedbackId); return ResponseEntity.ok(dto); } diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java index 58acec71..875c51fd 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java @@ -1,7 +1,6 @@ package modulemanagement.ls1.controllers; import modulemanagement.ls1.dtos.ModuleVersionUpdateRequestDTO; -import modulemanagement.ls1.dtos.ModuleVersionUpdateResponseDTO; import modulemanagement.ls1.dtos.ModuleVersionViewDTO; import modulemanagement.ls1.dtos.CompletionServiceResponseDTO; import modulemanagement.ls1.dtos.CompletionServiceRequestDTO; @@ -42,18 +41,18 @@ public ModuleVersionController(ModuleVersionService moduleVersionService, @GetMapping("/{moduleVersionId}") @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity getModuleVersionUpdateDtoFromId( + public ResponseEntity getModuleVersionUpdateDtoFromId( @CurrentUser User user, @PathVariable Long moduleVersionId) { - ModuleVersionUpdateResponseDTO dto = moduleVersionService.getModuleVersionUpdateDtoFromId(moduleVersionId, + ModuleVersionViewDTO dto = moduleVersionService.getModuleVersionUpdateDtoFromId(moduleVersionId, user.getUserId()); return ResponseEntity.ok(dto); } @PutMapping("/{moduleVersionId}") @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity updateModuleVersion(@CurrentUser User user, + public ResponseEntity updateModuleVersion(@CurrentUser User user, @PathVariable Long moduleVersionId, @Valid @RequestBody ModuleVersionUpdateRequestDTO moduleVersion) { - ModuleVersionUpdateResponseDTO updatedModuleVersion = moduleVersionService + ModuleVersionViewDTO updatedModuleVersion = moduleVersionService .updateModuleVersionFromRequest(user.getUserId(), moduleVersionId, moduleVersion); return ResponseEntity.ok(updatedModuleVersion); } diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java index 0e86423d..cb085f4a 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java @@ -1,7 +1,6 @@ package modulemanagement.ls1.controllers; import modulemanagement.ls1.dtos.*; -import modulemanagement.ls1.models.Proposal; import modulemanagement.ls1.models.User; import modulemanagement.ls1.services.ProposalService; import modulemanagement.ls1.shared.CurrentUser; @@ -30,11 +29,11 @@ public ProposalController(ProposalService proposalService) { @PostMapping @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity createProposal(@CurrentUser User user, + public ResponseEntity createProposal(@CurrentUser User user, @Valid @RequestBody ProposalRequestDTO request) { log.info("createProposal invoked"); - Proposal proposal = proposalService.createProposalFromRequest(user, request); - return ResponseEntity.ok(proposal); + ProposalViewDTO proposalView = proposalService.createProposalFromRequest(user, request); + return ResponseEntity.ok(proposalView); } @PostMapping(value = "/submit/{proposalId}") diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java new file mode 100644 index 00000000..36c0682e --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java @@ -0,0 +1,12 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ModuleDegreeProgramAssignmentDTO { + @NotNull + private Long degreeProgramId; + @NotNull + private Long degreeProgramSpecializationId; +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateRequestDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateRequestDTO.java index ebc12ed1..0321f5a1 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateRequestDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateRequestDTO.java @@ -1,10 +1,11 @@ package modulemanagement.ls1.dtos; -import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; import modulemanagement.ls1.enums.Language; import modulemanagement.ls1.enums.ModuleVersionStatus; import lombok.Data; -import modulemanagement.ls1.models.ModuleVersion; +import java.util.ArrayList; +import java.util.List; @Data public class ModuleVersionUpdateRequestDTO { @@ -15,10 +16,19 @@ public class ModuleVersionUpdateRequestDTO { private Boolean isComplete; private String bulletPoints; private String titleEng; + private String titleDe; private String levelEng; private Language languageEng; private String frequencyEng; private Integer credits; + private Integer hoursLecture; + private Integer hoursExercise; + private Integer hoursPractical; + private Integer hoursSeminar; + private String firstSemesterAvailable; + private String successorModuleName; + @Valid + private List degreeProgramAssignments = new ArrayList<>(); private String duration; private Integer hoursTotal; private Integer hoursSelfStudy; @@ -38,43 +48,4 @@ public class ModuleVersionUpdateRequestDTO { private String responsiblesEng; private String lvSwsLecturerEng; - - @JsonIgnore - public static ModuleVersionUpdateRequestDTO fromModuleVersion(ModuleVersion mv) { - ModuleVersionUpdateRequestDTO mdto = new ModuleVersionUpdateRequestDTO(); - ModuleVersionToRequestDTO(mv, mdto); - return mdto; - } - - static void ModuleVersionToRequestDTO(ModuleVersion mv, ModuleVersionUpdateRequestDTO mdto) { - mdto.setModuleVersionId(mv.getModuleVersionId()); - mdto.setVersion(mv.getVersion()); - mdto.setModuleId(mv.getModuleId()); - mdto.setStatus(mv.getStatus()); - mdto.setIsComplete(mv.isCompleted()); - mdto.setBulletPoints(mv.getBulletPoints()); - mdto.setTitleEng(mv.getTitleEng()); - mdto.setLevelEng(mv.getLevelEng()); - mdto.setLanguageEng(mv.getLanguageEng()); - mdto.setFrequencyEng(mv.getFrequencyEng()); - mdto.setCredits(mv.getCredits()); - mdto.setDuration(mv.getDuration()); - mdto.setHoursTotal(mv.getHoursTotal()); - mdto.setHoursSelfStudy(mv.getHoursSelfStudy()); - mdto.setHoursPresence(mv.getHoursPresence()); - mdto.setExaminationAchievementsEng(mv.getExaminationAchievementsEng()); - mdto.setExaminationAchievementsPromptEng(mv.getExaminationAchievementsPromptEng()); - mdto.setRepetitionEng(mv.getRepetitionEng()); - mdto.setRecommendedPrerequisitesEng(mv.getRecommendedPrerequisitesEng()); - mdto.setContentEng(mv.getContentEng()); - mdto.setContentPromptEng(mv.getContentPromptEng()); - mdto.setLearningOutcomesEng(mv.getLearningOutcomesEng()); - mdto.setLearningOutcomesPromptEng(mv.getLearningOutcomesPromptEng()); - mdto.setTeachingMethodsEng(mv.getTeachingMethodsEng()); - mdto.setTeachingMethodsPromptEng(mv.getTeachingMethodsPromptEng()); - mdto.setMediaEng(mv.getMediaEng()); - mdto.setLiteratureEng(mv.getLiteratureEng()); - mdto.setResponsiblesEng(mv.getResponsiblesEng()); - mdto.setLvSwsLecturerEng(mv.getLvSwsLecturerEng()); - } } \ No newline at end of file diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateResponseDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateResponseDTO.java deleted file mode 100644 index 4cc25827..00000000 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionUpdateResponseDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package modulemanagement.ls1.dtos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; -import modulemanagement.ls1.models.ModuleVersion; - -@Setter -@Getter -public class ModuleVersionUpdateResponseDTO extends ModuleVersionUpdateRequestDTO { - @NotNull private Long proposalId; - - @JsonIgnore - public static ModuleVersionUpdateResponseDTO fromModuleVersion(ModuleVersion mv) { - ModuleVersionUpdateResponseDTO mdto = new ModuleVersionUpdateResponseDTO(); - ModuleVersionUpdateRequestDTO.ModuleVersionToRequestDTO(mv, mdto); - mdto.setProposalId(mv.getProposal().getProposalId()); - return mdto; - } -} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java index c8c6f264..2a97beca 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java @@ -9,6 +9,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Data public class ModuleVersionViewDTO { @@ -21,10 +22,18 @@ public class ModuleVersionViewDTO { private String bulletPoints; private String titleEng; + private String titleDe; private String levelEng; private Language languageEng; private String frequencyEng; private Integer credits; + private Integer hoursLecture; + private Integer hoursExercise; + private Integer hoursPractical; + private Integer hoursSeminar; + private String firstSemesterAvailable; + private String successorModuleName; + private List degreeProgramAssignments = new ArrayList<>(); private String duration; private Integer hoursTotal; private Integer hoursSelfStudy; @@ -55,10 +64,28 @@ public static ModuleVersionViewDTO from(ModuleVersion moduleVersion) { dto.status = moduleVersion.getStatus(); dto.bulletPoints = moduleVersion.getBulletPoints(); dto.titleEng = moduleVersion.getTitleEng(); + dto.titleDe = moduleVersion.getTitleDe(); dto.levelEng = moduleVersion.getLevelEng(); dto.languageEng = moduleVersion.getLanguageEng(); dto.frequencyEng = moduleVersion.getFrequencyEng(); dto.credits = moduleVersion.getCredits(); + dto.hoursLecture = moduleVersion.getHoursLecture(); + dto.hoursExercise = moduleVersion.getHoursExercise(); + dto.hoursPractical = moduleVersion.getHoursPractical(); + dto.hoursSeminar = moduleVersion.getHoursSeminar(); + dto.firstSemesterAvailable = moduleVersion.getFirstSemesterAvailable(); + dto.successorModuleName = moduleVersion.getSuccessorModuleName(); + if (moduleVersion.getDegreeProgramAssignments() != null) { + dto.degreeProgramAssignments = moduleVersion.getDegreeProgramAssignments().stream() + .map(a -> { + ModuleDegreeProgramAssignmentDTO item = new ModuleDegreeProgramAssignmentDTO(); + item.setDegreeProgramId(a.getDegreeProgram().getDegreeProgramId()); + item.setDegreeProgramSpecializationId( + a.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); + return item; + }) + .collect(Collectors.toList()); + } dto.duration = moduleVersion.getDuration(); dto.hoursTotal = moduleVersion.getHoursTotal(); dto.hoursSelfStudy = moduleVersion.getHoursSelfStudy(); diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ProposalRequestDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ProposalRequestDTO.java index 3ad82552..ebb18315 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ProposalRequestDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ProposalRequestDTO.java @@ -1,17 +1,31 @@ package modulemanagement.ls1.dtos; +import jakarta.validation.Valid; import modulemanagement.ls1.enums.Language; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import lombok.Data; +import java.util.ArrayList; +import java.util.List; + @Data public class ProposalRequestDTO { private String bulletPoints; - @NotNull private String titleEng; + @NotBlank + private String titleEng; + private String titleDe; private String levelEng; private Language languageEng; private String frequencyEng; private Integer credits; + private Integer hoursLecture; + private Integer hoursExercise; + private Integer hoursPractical; + private Integer hoursSeminar; + private String firstSemesterAvailable; + private String successorModuleName; + @Valid + private List degreeProgramAssignments = new ArrayList<>(); private String duration; private Integer hoursTotal; private Integer hoursSelfStudy; diff --git a/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java b/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java index 834e51ca..9e1a7f3b 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java +++ b/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java @@ -10,6 +10,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Data @@ -31,7 +32,8 @@ public class ModuleVersion { private LocalDateTime creationDate; @Column(name = "status") - @NotNull private ModuleVersionStatus status; + @NotNull + private ModuleVersionStatus status; @Column(name = "bullet_points", columnDefinition = "CLOB") private String bulletPoints; @@ -40,6 +42,27 @@ public class ModuleVersion { @Column(name = "title_eng") private String titleEng; + @Column(name = "title_de") + private String titleDe; + + @Column(name = "hours_lecture") + private Integer hoursLecture; + + @Column(name = "hours_exercise") + private Integer hoursExercise; + + @Column(name = "hours_practical") + private Integer hoursPractical; + + @Column(name = "hours_seminar") + private Integer hoursSeminar; + + @Column(name = "first_semester_available") + private String firstSemesterAvailable; + + @Column(name = "successor_module_name") + private String successorModuleName; + @Column(name = "level_eng") private String levelEng; @@ -117,6 +140,9 @@ public class ModuleVersion { @OneToMany(mappedBy = "moduleVersion", cascade = CascadeType.ALL) private List requiredFeedbacks; + @OneToMany(mappedBy = "moduleVersion", cascade = CascadeType.ALL, orphanRemoval = true) + private List degreeProgramAssignments = new ArrayList<>(); + @JsonIgnore public boolean isCompleted() { return !StringUtils.isEmpty(titleEng) diff --git a/Server/src/main/java/modulemanagement/ls1/models/ModuleVersionDegreeProgramAssignment.java b/Server/src/main/java/modulemanagement/ls1/models/ModuleVersionDegreeProgramAssignment.java new file mode 100644 index 00000000..01ef533e --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/models/ModuleVersionDegreeProgramAssignment.java @@ -0,0 +1,29 @@ +package modulemanagement.ls1.models; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "module_version_degree_program_assignment", + uniqueConstraints = @UniqueConstraint(columnNames = { "module_version_id", "degree_program_id" })) +public class ModuleVersionDegreeProgramAssignment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "module_version_id", nullable = false) + private ModuleVersion moduleVersion; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "degree_program_id", nullable = false) + private DegreeProgram degreeProgram; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "degree_program_specialization_id", nullable = false) + private DegreeProgramSpecialization degreeProgramSpecialization; +} diff --git a/Server/src/main/java/modulemanagement/ls1/models/Proposal.java b/Server/src/main/java/modulemanagement/ls1/models/Proposal.java index 9d8bbc55..a45f3156 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/Proposal.java +++ b/Server/src/main/java/modulemanagement/ls1/models/Proposal.java @@ -34,7 +34,8 @@ public class Proposal { private LocalDateTime creationDate; @Column(name = "status") - @NotNull private ProposalStatus status; + @NotNull + private ProposalStatus status; @OneToMany(mappedBy = "proposal", cascade = CascadeType.ALL, orphanRemoval = true) private List moduleVersions = new ArrayList<>(); @@ -54,7 +55,6 @@ public Integer getLatestModuleVersion() { return getLatestModuleVersionWithContent().getVersion(); } - @JsonIgnore public void addNewModuleVersion() { ModuleVersion latestMv = getLatestModuleVersionWithContent(); @@ -65,6 +65,13 @@ public void addNewModuleVersion() { newMv.setStatus(ModuleVersionStatus.PENDING_SUBMISSION); newMv.setBulletPoints(latestMv.getBulletPoints()); newMv.setTitleEng(latestMv.getTitleEng()); + newMv.setTitleDe(latestMv.getTitleDe()); + newMv.setHoursLecture(latestMv.getHoursLecture()); + newMv.setHoursExercise(latestMv.getHoursExercise()); + newMv.setHoursPractical(latestMv.getHoursPractical()); + newMv.setHoursSeminar(latestMv.getHoursSeminar()); + newMv.setFirstSemesterAvailable(latestMv.getFirstSemesterAvailable()); + newMv.setSuccessorModuleName(latestMv.getSuccessorModuleName()); newMv.setLevelEng(latestMv.getLevelEng()); newMv.setLanguageEng(latestMv.getLanguageEng()); newMv.setFrequencyEng(latestMv.getFrequencyEng()); @@ -84,6 +91,15 @@ public void addNewModuleVersion() { newMv.setResponsiblesEng(latestMv.getResponsiblesEng()); newMv.setLvSwsLecturerEng(latestMv.getLvSwsLecturerEng()); + if (latestMv.getDegreeProgramAssignments() != null) { + for (var a : latestMv.getDegreeProgramAssignments()) { + ModuleVersionDegreeProgramAssignment newAssig = new ModuleVersionDegreeProgramAssignment(); + newAssig.setModuleVersion(newMv); + newAssig.setDegreeProgram(a.getDegreeProgram()); + newAssig.setDegreeProgramSpecialization(a.getDegreeProgramSpecialization()); + newMv.getDegreeProgramAssignments().add(newAssig); + } + } List requiredFeedbacks = new ArrayList<>(); ProposalService.createNewFeedbacks(newMv, requiredFeedbacks); newMv.setRequiredFeedbacks(requiredFeedbacks); diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java index 1c54da04..6f16a27d 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java @@ -19,4 +19,8 @@ public interface DegreeProgramRepository extends JpaRepository findWithSpecializationsByDegreeProgramId(Long degreeProgramId); + + @EntityGraph(attributePaths = { "degreeProgramSpecializations" }) + @Query("SELECT p FROM DegreeProgram p ORDER BY p.name") + List findAllWithSpecializations(); } diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/ModuleVersionDegreeProgramAssignmentRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/ModuleVersionDegreeProgramAssignmentRepository.java new file mode 100644 index 00000000..4bae49d5 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/repositories/ModuleVersionDegreeProgramAssignmentRepository.java @@ -0,0 +1,17 @@ +package modulemanagement.ls1.repositories; + +import modulemanagement.ls1.models.ModuleVersionDegreeProgramAssignment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ModuleVersionDegreeProgramAssignmentRepository + extends JpaRepository { + + @Modifying + @Query("DELETE FROM ModuleVersionDegreeProgramAssignment a WHERE a.moduleVersion.moduleVersionId = :moduleVersionId") + void deleteByModuleVersion_ModuleVersionId(@Param("moduleVersionId") Long moduleVersionId); +} diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java index bfc6edaf..ad5098b3 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java @@ -33,6 +33,12 @@ public List getAllDegreePrograms() { .collect(Collectors.toList()); } + public List getAllDegreeProgramsWithSpecializations() { + return degreeProgramRepository.findAllWithSpecializations().stream() + .map(DegreeProgramDTO::fromDegreeProgram) + .collect(Collectors.toList()); + } + public DegreeProgramDTO getDegreeProgram(Long id) { DegreeProgram program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id) .orElseThrow(() -> new ResourceNotFoundException("Degree program not found: " + id)); diff --git a/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java b/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java index fd320a24..4bc2fa95 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java @@ -3,7 +3,7 @@ import jakarta.validation.constraints.NotBlank; import modulemanagement.ls1.dtos.FeedbackDTO; import modulemanagement.ls1.dtos.FeedbackListItemDto; -import modulemanagement.ls1.dtos.ModuleVersionUpdateRequestDTO; +import modulemanagement.ls1.dtos.ModuleVersionViewDTO; import modulemanagement.ls1.enums.FeedbackStatus; import modulemanagement.ls1.models.Feedback; import modulemanagement.ls1.models.User; @@ -81,10 +81,10 @@ private Feedback getPendingFeedback(Long feedbackId) { return feedback; } - public ModuleVersionUpdateRequestDTO getModuleVersionOfFeedback(Long feedbackId) { + public ModuleVersionViewDTO getModuleVersionOfFeedback(Long feedbackId) { Feedback feedback = feedbackRepository.findById(feedbackId) .orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); - return ModuleVersionUpdateRequestDTO.fromModuleVersion(feedback.getModuleVersion()); + return ModuleVersionViewDTO.from(feedback.getModuleVersion()); } } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index 6276d540..020688ab 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -1,7 +1,7 @@ package modulemanagement.ls1.services; +import modulemanagement.ls1.dtos.ModuleDegreeProgramAssignmentDTO; import modulemanagement.ls1.dtos.ModuleVersionUpdateRequestDTO; -import modulemanagement.ls1.dtos.ModuleVersionUpdateResponseDTO; import modulemanagement.ls1.dtos.ModuleVersionViewDTO; import modulemanagement.ls1.dtos.ModuleVersionViewFeedbackDTO; import modulemanagement.ls1.dtos.SimilarModuleDTO; @@ -9,10 +9,15 @@ import modulemanagement.ls1.enums.ModuleVersionStatus; import modulemanagement.ls1.enums.ProposalStatus; import modulemanagement.ls1.enums.UserRole; +import modulemanagement.ls1.models.DegreeProgram; +import modulemanagement.ls1.models.DegreeProgramSpecialization; import modulemanagement.ls1.models.Feedback; import modulemanagement.ls1.models.ModuleVersion; +import modulemanagement.ls1.models.ModuleVersionDegreeProgramAssignment; import modulemanagement.ls1.models.Proposal; import modulemanagement.ls1.models.User; +import modulemanagement.ls1.repositories.DegreeProgramRepository; +import modulemanagement.ls1.repositories.ModuleVersionDegreeProgramAssignmentRepository; import modulemanagement.ls1.repositories.ModuleVersionRepository; import modulemanagement.ls1.repositories.ProposalRepository; import modulemanagement.ls1.shared.PdfCreator; @@ -21,24 +26,36 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; +import jakarta.persistence.EntityManager; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @Service public class ModuleVersionService { private final ModuleVersionRepository moduleVersionRepository; + private final ModuleVersionDegreeProgramAssignmentRepository assignmentRepository; + private final DegreeProgramRepository degreeProgramRepository; private final ProposalRepository proposalRepository; private final OverlapDetectionService overlapDetectionService; private final PdfCreator pdfCreator; + private final EntityManager entityManager; - public ModuleVersionService(ModuleVersionRepository moduleVersionRepository, ProposalRepository proposalRepository, - OverlapDetectionService overlapDetectionService, PdfCreator pdfCreator) { + public ModuleVersionService(ModuleVersionRepository moduleVersionRepository, + ModuleVersionDegreeProgramAssignmentRepository assignmentRepository, + DegreeProgramRepository degreeProgramRepository, ProposalRepository proposalRepository, + OverlapDetectionService overlapDetectionService, PdfCreator pdfCreator, + EntityManager entityManager) { this.moduleVersionRepository = moduleVersionRepository; + this.assignmentRepository = assignmentRepository; + this.degreeProgramRepository = degreeProgramRepository; this.proposalRepository = proposalRepository; this.overlapDetectionService = overlapDetectionService; this.pdfCreator = pdfCreator; + this.entityManager = entityManager; } private boolean hasAccessPermission(Proposal proposal, User user) { @@ -46,13 +63,13 @@ private boolean hasAccessPermission(Proposal proposal, User user) { return true; } - return user.getRoles() != null && ( - user.getRoles().contains(UserRole.QUALITY_MANAGEMENT) || + return user.getRoles() != null && (user.getRoles().contains(UserRole.QUALITY_MANAGEMENT) || user.getRoles().contains(UserRole.EXAMINATION_BOARD) || user.getRoles().contains(UserRole.ACADEMIC_PROGRAM_ADVISOR)); } - public ModuleVersionUpdateResponseDTO updateModuleVersionFromRequest(UUID userId, Long moduleVersionId, + @Transactional + public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long moduleVersionId, ModuleVersionUpdateRequestDTO request) { ModuleVersion mv = moduleVersionRepository.findById(moduleVersionId) .orElseThrow(() -> new ResourceNotFoundException("ModuleVersion not found")); @@ -69,10 +86,17 @@ public ModuleVersionUpdateResponseDTO updateModuleVersionFromRequest(UUID userId mv.setBulletPoints(request.getBulletPoints()); mv.setTitleEng(request.getTitleEng()); + mv.setTitleDe(request.getTitleDe()); mv.setLevelEng(request.getLevelEng()); mv.setLanguageEng(request.getLanguageEng()); mv.setFrequencyEng(request.getFrequencyEng()); mv.setCredits(request.getCredits()); + mv.setHoursLecture(request.getHoursLecture()); + mv.setHoursExercise(request.getHoursExercise()); + mv.setHoursPractical(request.getHoursPractical()); + mv.setHoursSeminar(request.getHoursSeminar()); + mv.setFirstSemesterAvailable(request.getFirstSemesterAvailable()); + mv.setSuccessorModuleName(request.getSuccessorModuleName()); mv.setDuration(request.getDuration()); mv.setHoursTotal(request.getHoursTotal()); mv.setHoursSelfStudy(request.getHoursSelfStudy()); @@ -92,8 +116,39 @@ public ModuleVersionUpdateResponseDTO updateModuleVersionFromRequest(UUID userId mv.setResponsiblesEng(request.getResponsiblesEng()); mv.setLvSwsLecturerEng(request.getLvSwsLecturerEng()); + if (request.getDegreeProgramAssignments() != null && !request.getDegreeProgramAssignments().isEmpty()) { + List programIds = request.getDegreeProgramAssignments().stream() + .map(ModuleDegreeProgramAssignmentDTO::getDegreeProgramId) + .collect(Collectors.toList()); + if (programIds.size() != programIds.stream().distinct().count()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "A module cannot be assigned to the same degree program more than once."); + } + assignmentRepository.deleteByModuleVersion_ModuleVersionId(mv.getModuleVersionId()); + entityManager.flush(); + mv.getDegreeProgramAssignments().clear(); + for (ModuleDegreeProgramAssignmentDTO item : request.getDegreeProgramAssignments()) { + DegreeProgram program = degreeProgramRepository + .findWithSpecializationsByDegreeProgramId(item.getDegreeProgramId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Degree program not found: " + item.getDegreeProgramId())); + DegreeProgramSpecialization spec = program.getDegreeProgramSpecializations().stream() + .filter(s -> s.getDegreeProgramSpecializationId() + .equals(item.getDegreeProgramSpecializationId())) + .findFirst() + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Specialization " + item.getDegreeProgramSpecializationId() + + " does not belong to degree program " + item.getDegreeProgramId())); + ModuleVersionDegreeProgramAssignment assignment = new ModuleVersionDegreeProgramAssignment(); + assignment.setModuleVersion(mv); + assignment.setDegreeProgram(program); + assignment.setDegreeProgramSpecialization(spec); + mv.getDegreeProgramAssignments().add(assignment); + } + } + mv = moduleVersionRepository.save(mv); - return ModuleVersionUpdateResponseDTO.fromModuleVersion(mv); + return ModuleVersionViewDTO.from(mv); } public void updateStatus(Long moduleVersionId) { @@ -139,7 +194,7 @@ public void updateStatus(Long moduleVersionId) { moduleVersionRepository.save(mv); } - public ModuleVersionUpdateResponseDTO getModuleVersionUpdateDtoFromId(Long moduleVersionId, UUID userId) { + public ModuleVersionViewDTO getModuleVersionUpdateDtoFromId(Long moduleVersionId, UUID userId) { var mv = moduleVersionRepository.findById(moduleVersionId) .orElseThrow(() -> new ResourceNotFoundException("Module Version not found")); Proposal p = mv.getProposal(); @@ -151,7 +206,7 @@ public ModuleVersionUpdateResponseDTO getModuleVersionUpdateDtoFromId(Long modul throw new IllegalStateException("Proposal must have at least one ModuleVersion."); } - return ModuleVersionUpdateResponseDTO.fromModuleVersion(mv); + return ModuleVersionViewDTO.from(mv); } public ModuleVersionViewDTO getModuleVersionViewDto(Long moduleVersionId, UUID userId) { diff --git a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java index 8f876a25..810fc519 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java @@ -3,16 +3,21 @@ import jakarta.validation.Valid; import modulemanagement.ls1.dtos.*; import modulemanagement.ls1.enums.*; +import modulemanagement.ls1.models.DegreeProgram; +import modulemanagement.ls1.models.DegreeProgramSpecialization; import modulemanagement.ls1.models.Feedback; import modulemanagement.ls1.models.ModuleVersion; +import modulemanagement.ls1.models.ModuleVersionDegreeProgramAssignment; import modulemanagement.ls1.models.Proposal; import modulemanagement.ls1.models.User; +import modulemanagement.ls1.repositories.DegreeProgramRepository; import modulemanagement.ls1.repositories.FeedbackRepository; import modulemanagement.ls1.repositories.ModuleVersionRepository; import modulemanagement.ls1.repositories.ProposalRepository; import modulemanagement.ls1.shared.ResourceNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; @@ -28,14 +33,18 @@ public class ProposalService { private final ProposalRepository proposalRepository; private final ModuleVersionRepository moduleVersionRepository; private final FeedbackRepository feedbackRepository; + private final DegreeProgramRepository degreeProgramRepository; - public ProposalService(ProposalRepository proposalRepository, ModuleVersionRepository moduleVersionRepository, FeedbackRepository feedbackRepository) { + public ProposalService(ProposalRepository proposalRepository, ModuleVersionRepository moduleVersionRepository, + FeedbackRepository feedbackRepository, DegreeProgramRepository degreeProgramRepository) { this.proposalRepository = proposalRepository; this.moduleVersionRepository = moduleVersionRepository; this.feedbackRepository = feedbackRepository; + this.degreeProgramRepository = degreeProgramRepository; } - public Proposal createProposalFromRequest(User user, ProposalRequestDTO request) { + @Transactional + public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO request) { Proposal p = new Proposal(); p.setCreatedBy(user); p.setCreationDate(LocalDateTime.now()); @@ -50,10 +59,17 @@ public Proposal createProposalFromRequest(User user, ProposalRequestDTO request) mv.setStatus(ModuleVersionStatus.PENDING_SUBMISSION); mv.setBulletPoints(request.getBulletPoints()); mv.setTitleEng(request.getTitleEng()); + mv.setTitleDe(request.getTitleDe()); mv.setLevelEng(request.getLevelEng()); mv.setLanguageEng(request.getLanguageEng()); mv.setFrequencyEng(request.getFrequencyEng()); mv.setCredits(request.getCredits()); + mv.setHoursLecture(request.getHoursLecture()); + mv.setHoursExercise(request.getHoursExercise()); + mv.setHoursPractical(request.getHoursPractical()); + mv.setHoursSeminar(request.getHoursSeminar()); + mv.setFirstSemesterAvailable(request.getFirstSemesterAvailable()); + mv.setSuccessorModuleName(request.getSuccessorModuleName()); mv.setDuration(request.getDuration()); mv.setHoursTotal(request.getHoursTotal()); mv.setHoursSelfStudy(request.getHoursSelfStudy()); @@ -74,6 +90,35 @@ public Proposal createProposalFromRequest(User user, ProposalRequestDTO request) mv.setLvSwsLecturerEng(request.getLvSwsLecturerEng()); mv = moduleVersionRepository.save(mv); + if (request.getDegreeProgramAssignments() != null && !request.getDegreeProgramAssignments().isEmpty()) { + List programIds = request.getDegreeProgramAssignments().stream() + .map(ModuleDegreeProgramAssignmentDTO::getDegreeProgramId) + .collect(Collectors.toList()); + if (programIds.size() != programIds.stream().distinct().count()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "A module cannot be assigned to the same degree program more than once."); + } + for (ModuleDegreeProgramAssignmentDTO item : request.getDegreeProgramAssignments()) { + DegreeProgram program = degreeProgramRepository + .findWithSpecializationsByDegreeProgramId(item.getDegreeProgramId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Degree program not found: " + item.getDegreeProgramId())); + DegreeProgramSpecialization spec = program.getDegreeProgramSpecializations().stream() + .filter(s -> s.getDegreeProgramSpecializationId() + .equals(item.getDegreeProgramSpecializationId())) + .findFirst() + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Specialization " + item.getDegreeProgramSpecializationId() + + " does not belong to degree program " + item.getDegreeProgramId())); + ModuleVersionDegreeProgramAssignment assignment = new ModuleVersionDegreeProgramAssignment(); + assignment.setModuleVersion(mv); + assignment.setDegreeProgram(program); + assignment.setDegreeProgramSpecialization(spec); + mv.getDegreeProgramAssignments().add(assignment); + } + moduleVersionRepository.save(mv); + } + List feedbacks = new ArrayList<>(); createNewFeedbacks(mv, feedbacks); feedbacks = feedbackRepository.saveAll(feedbacks); @@ -81,7 +126,7 @@ public Proposal createProposalFromRequest(User user, ProposalRequestDTO request) moduleVersionRepository.save(mv); p.addModuleVersion(mv); proposalRepository.save(p); - return p; + return ProposalViewDTO.from(p); } public static void createNewFeedbacks(ModuleVersion mv, List feedbacks) { @@ -97,11 +142,14 @@ public static void createNewFeedbacks(ModuleVersion mv, List feedbacks } public ProposalViewDTO addModuleVersion(UUID userId, @Valid AddModuleVersionDTO request) { - Proposal p = proposalRepository.findById(request.getProposalId()).orElseThrow(() -> new ResourceNotFoundException("Proposal not found")); + Proposal p = proposalRepository.findById(request.getProposalId()) + .orElseThrow(() -> new ResourceNotFoundException("Proposal not found")); if (!userId.equals(p.getCreatedBy().getUserId())) - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "You cannot add a module version to a module you did not create."); + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, + "You cannot add a module version to a module you did not create."); if (!p.getStatus().equals(ProposalStatus.REQUIRES_REVIEW)) - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "You can only add a new module version, if the proposal requires a review."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "You can only add a new module version, if the proposal requires a review."); for (Feedback f : p.getLatestModuleVersionWithContent().getRequiredFeedbacks()) { if (f.getStatus().equals(FeedbackStatus.PENDING_FEEDBACK)) { @@ -122,9 +170,12 @@ public List getCompactProposalsOfUser(UUID userId) { p.getProposalId(), p.getCreatedBy().getFirstName(), p.getStatus(), - p.getLatestModuleVersionWithContent() != null ? p.getLatestModuleVersionWithContent().getModuleVersionId() : null, - p.getLatestModuleVersionWithContent() != null ? p.getLatestModuleVersionWithContent().getTitleEng() : null - )) + p.getLatestModuleVersionWithContent() != null + ? p.getLatestModuleVersionWithContent().getModuleVersionId() + : null, + p.getLatestModuleVersionWithContent() != null + ? p.getLatestModuleVersionWithContent().getTitleEng() + : null)) .sorted(Comparator.comparing(ProposalsCompactDTO::getProposalId)) .collect(Collectors.toList()); @@ -141,7 +192,7 @@ public ProposalViewDTO getProposalViewDtoById(UUID userId, long id) { public ProposalViewDTO submitProposal(Long proposalId, UUID userId) { Proposal proposal = proposalRepository.findById(proposalId) - .orElseThrow(() -> new IllegalArgumentException("No proposal with id " + proposalId +" found")); + .orElseThrow(() -> new IllegalArgumentException("No proposal with id " + proposalId + " found")); if (!proposal.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); } @@ -173,7 +224,7 @@ public ProposalViewDTO submitProposal(Long proposalId, UUID userId) { public ProposalViewDTO cancelSubmission(Long proposalId, UUID userId) { Proposal proposal = proposalRepository.findById(proposalId) - .orElseThrow(() -> new IllegalArgumentException("No proposal with id " + proposalId +" found")); + .orElseThrow(() -> new IllegalArgumentException("No proposal with id " + proposalId + " found")); if (!proposal.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); @@ -181,11 +232,12 @@ public ProposalViewDTO cancelSubmission(Long proposalId, UUID userId) { ModuleVersion mv = proposal.getLatestModuleVersionWithContent(); if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_FEEDBACK)) { - throw new IllegalStateException("Only submitted proposals can cancel their submission. This proposal is " + mv.getStatus() + "."); + throw new IllegalStateException( + "Only submitted proposals can cancel their submission. This proposal is " + mv.getStatus() + "."); } boolean oneFeedbackNotPending = false; - for (Feedback f: mv.getRequiredFeedbacks()) { + for (Feedback f : mv.getRequiredFeedbacks()) { if (!f.getStatus().equals(FeedbackStatus.PENDING_FEEDBACK)) { oneFeedbackNotPending = true; break; @@ -193,16 +245,15 @@ public ProposalViewDTO cancelSubmission(Long proposalId, UUID userId) { } if (oneFeedbackNotPending) { - for (Feedback f: mv.getRequiredFeedbacks()) { + for (Feedback f : mv.getRequiredFeedbacks()) { if (f.getStatus().equals(FeedbackStatus.PENDING_FEEDBACK)) { f.setStatus(FeedbackStatus.CANCELLED); } } mv.setStatus(ModuleVersionStatus.CANCELLED); proposal.setStatus(ProposalStatus.REQUIRES_REVIEW); - } - else { - for (Feedback f: mv.getRequiredFeedbacks()) { + } else { + for (Feedback f : mv.getRequiredFeedbacks()) { f.setStatus(FeedbackStatus.PENDING_SUBMISSION); } mv.setStatus(ModuleVersionStatus.PENDING_SUBMISSION); @@ -213,12 +264,15 @@ public ProposalViewDTO cancelSubmission(Long proposalId, UUID userId) { } public void deleteProposalById(long proposalId, UUID userId) { - Proposal p = proposalRepository.findById(proposalId).orElseThrow(() -> new ResourceNotFoundException("Proposal with id " + proposalId + " not found.")); + Proposal p = proposalRepository.findById(proposalId) + .orElseThrow(() -> new ResourceNotFoundException("Proposal with id " + proposalId + " not found.")); if (!p.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); } if (p.getStatus() != ProposalStatus.PENDING_SUBMISSION) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "You can only delete a proposal that is not already submit. This module proposal is " + p.getStatus() + "."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "You can only delete a proposal that is not already submit. This module proposal is " + + p.getStatus() + "."); } proposalRepository.delete(p); } diff --git a/Server/src/main/resources/db/changelog/changes/0009_module_version_initial_and_assignments.yaml b/Server/src/main/resources/db/changelog/changes/0009_module_version_initial_and_assignments.yaml new file mode 100644 index 00000000..56ef8012 --- /dev/null +++ b/Server/src/main/resources/db/changelog/changes/0009_module_version_initial_and_assignments.yaml @@ -0,0 +1,76 @@ +databaseChangeLog: + - changeSet: + id: 0009-module-version-initial-and-assignments + author: system + changes: + - addColumn: + tableName: module_version + columns: + - column: + name: title_de + type: VARCHAR(500) + - column: + name: hours_lecture + type: INT + - column: + name: hours_exercise + type: INT + - column: + name: hours_practical + type: INT + - column: + name: hours_seminar + type: INT + - column: + name: first_semester_available + type: VARCHAR(100) + - column: + name: successor_module_name + type: VARCHAR(500) + - createTable: + tableName: module_version_degree_program_assignment + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: module_version_id + type: BIGINT + constraints: + nullable: false + - column: + name: degree_program_id + type: BIGINT + constraints: + nullable: false + - column: + name: degree_program_specialization_id + type: BIGINT + constraints: + nullable: false + - addUniqueConstraint: + tableName: module_version_degree_program_assignment + columnNames: module_version_id, degree_program_id + constraintName: mv_dp_assign_unique_mv_program + - addForeignKeyConstraint: + baseTableName: module_version_degree_program_assignment + baseColumnNames: module_version_id + referencedTableName: module_version + referencedColumnNames: module_version_id + constraintName: mv_dp_assign_mv_fk + onDelete: CASCADE + - addForeignKeyConstraint: + baseTableName: module_version_degree_program_assignment + baseColumnNames: degree_program_id + referencedTableName: degree_program + referencedColumnNames: degree_program_id + constraintName: mv_dp_assign_program_fk + - addForeignKeyConstraint: + baseTableName: module_version_degree_program_assignment + baseColumnNames: degree_program_specialization_id + referencedTableName: degree_program_specialization + referencedColumnNames: degree_program_specialization_id + constraintName: mv_dp_assign_spec_fk diff --git a/Server/src/main/resources/db/changelog/initialize/0010_seed_degree_programs.yaml b/Server/src/main/resources/db/changelog/initialize/0010_seed_degree_programs.yaml new file mode 100644 index 00000000..5d693af1 --- /dev/null +++ b/Server/src/main/resources/db/changelog/initialize/0010_seed_degree_programs.yaml @@ -0,0 +1,61 @@ +databaseChangeLog: + # Seed degree programs (responsible_user_id = admin user from 0006) + - changeSet: + id: 0010-seed-degree-programs + author: system + changes: + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "B.Sc. Informatics" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "B.Sc. Informatics: Games Engineering" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "B.Sc. Information Systems" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "M.Sc. Informatics" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "M.Sc. Informatics: Games Engineering" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "M.Sc. Information Systems" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "M.Sc. Data Engineering and Analytics" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "B.Sc. and M.Sc. Information Engineering (Campus Heilbronn)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "B.Sc. and M.Sc. Bioinformatics" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "M.Sc. Robotics, Cognition, Intelligence" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program + columns: + - column: { name: name, value: "M.Sc. Computational Science and Engineering" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } diff --git a/Server/src/main/resources/db/changelog/initialize/0011_seed_degree_program_specializations.yaml b/Server/src/main/resources/db/changelog/initialize/0011_seed_degree_program_specializations.yaml new file mode 100644 index 00000000..76cefa36 --- /dev/null +++ b/Server/src/main/resources/db/changelog/initialize/0011_seed_degree_program_specializations.yaml @@ -0,0 +1,84 @@ +databaseChangeLog: + # Seed areas of specialization (responsible_user_id = admin user from 0006) + - changeSet: + id: 0011-seed-specializations + author: system + changes: + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Engineering Software-intensive Systems (SE)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Databases and Information Systems (DBI)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Robotics (ROB)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Machine Learning and Analytics (MLA)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Computer Graphics and Vision (CGV)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Computer Architecture, Computer Networks and Distributed Systems (RRV)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Security and Privacy (SP)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Algorithms (ALG)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Scientific Computing and High Performance Computing (HPC)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Formal Methods and their Applications (FMA)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Human Centered Engineering (HCE)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Digital Biology and Digital Medicine (DBM)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + - insert: + tableName: degree_program_specialization + columns: + - column: { name: name, value: "Support Electives (UFG)" } + - column: { name: responsible_user_id, value: "f47ac10b-58cc-4372-a567-0e02b2c3d479" } + + # Assign all 13 specializations to all 11 degree programs (from 0010) + - changeSet: + id: 0011-assign-specializations-to-programs + author: system + changes: + - sql: + - INSERT INTO degree_program_specialization_assignment (degree_program_id, degree_program_specialization_id) + SELECT dp.degree_program_id, ds.degree_program_specialization_id + FROM degree_program dp + CROSS JOIN degree_program_specialization ds + WHERE dp.degree_program_id BETWEEN 1 AND 11 + AND ds.degree_program_specialization_id BETWEEN 1 AND 13 diff --git a/Server/src/main/resources/db/changelog/master.yaml b/Server/src/main/resources/db/changelog/master.yaml index ce435a17..d2170be5 100644 --- a/Server/src/main/resources/db/changelog/master.yaml +++ b/Server/src/main/resources/db/changelog/master.yaml @@ -23,3 +23,12 @@ databaseChangeLog: - include: relativeToChangelogFile: true file: changes/0008_degree_program_and_area_tables.yaml + - include: + relativeToChangelogFile: true + file: changes/0009_module_version_initial_and_assignments.yaml + - include: + relativeToChangelogFile: true + file: initialize/0010_seed_degree_programs.yaml + - include: + relativeToChangelogFile: true + file: initialize/0011_seed_degree_program_specializations.yaml From fc0d56a8c48ff3ece20dd3bd4d619bbac7622361 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 8 Mar 2026 22:29:40 +0100 Subject: [PATCH 05/42] fixes --- .../create-edit-base.component.ts | 46 +++++++++++++------ .../module-edit-stepper.component.ts | 3 +- .../module-edit-steps.config.ts | 15 ++---- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.ts b/Client/src/app/components/create-edit-base/create-edit-base.component.ts index 4bcfefb7..c43b1880 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.ts +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.ts @@ -1,4 +1,5 @@ -import { Component, computed, effect, inject, signal } from '@angular/core'; +import { Component, computed, effect, inject, signal, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CompletionServiceRequestDTO, @@ -42,18 +43,34 @@ export abstract class ProposalBaseComponent { isCreateMode = computed(() => this.moduleVersionId == null); currentStepIndex = signal(0); + /** Updated on form valueChanges so stepCompleted computed re-runs when user types. */ + private formValueVersion = signal(0); + stepCompleted = computed(() => { + this.formValueVersion(); const form = this.proposalForm; - return MODULE_EDIT_STEPS.map((step) => { - const required = step.requiredControlNames ?? []; - if (required.length === 0) return false; - return required.every((name) => { - const c = form.get(name); - return c && c.valid && c.value != null && c.value !== ''; - }); + const assignmentsList = this.assignments(); + return MODULE_EDIT_STEPS.map((step, index) => { + const allFieldsFilled = step.controlNames.every((name) => this.controlHasValue(form.get(name))); + if (step.id === 'basic') { + const hasCompleteAssignment = assignmentsList.some( + (a) => a.degreeProgramId != null && a.degreeProgramSpecializationId != null + ); + return allFieldsFilled && hasCompleteAssignment; + } + return allFieldsFilled; }); }); + private controlHasValue(c: ReturnType): boolean { + if (!c) return false; + const v = c.value; + if (v === undefined || v === null) return false; + if (typeof v === 'string') return v.trim() !== ''; + if (typeof v === 'number') return true; + return true; + } + moduleVersionStatus = computed(() => { const dto = this.moduleVersionDto(); return dto && 'status' in dto ? (dto as ModuleVersionViewDTO).status : undefined; @@ -61,7 +78,7 @@ export abstract class ProposalBaseComponent { currentVersionFeedbacks = computed(() => { const dto = this.moduleVersionDto(); - return dto && 'feedbacks' in dto ? (dto as ModuleVersionViewDTO).feedbacks ?? [] : []; + return dto && 'feedbacks' in dto ? ((dto as ModuleVersionViewDTO).feedbacks ?? []) : []; }); canSubmitForFeedback = computed(() => { @@ -74,9 +91,7 @@ export abstract class ProposalBaseComponent { const dto = this.moduleVersionDto(); if (dto && 'degreeProgramAssignments' in dto && Array.isArray((dto as ModuleVersionViewDTO).degreeProgramAssignments)) { const list = (dto as ModuleVersionViewDTO).degreeProgramAssignments ?? []; - this.assignments.set( - list.map((a) => ({ degreeProgramId: a.degreeProgramId ?? null, degreeProgramSpecializationId: a.degreeProgramSpecializationId ?? null })) - ); + this.assignments.set(list.map((a) => ({ degreeProgramId: a.degreeProgramId ?? null, degreeProgramSpecializationId: a.degreeProgramSpecializationId ?? null }))); } }); @@ -102,6 +117,8 @@ export abstract class ProposalBaseComponent { this.location.back(); } + private destroyRef = inject(DestroyRef); + constructor() { this.proposalForm = this.formBuilder.group({ bulletPoints: [''], @@ -136,6 +153,7 @@ export abstract class ProposalBaseComponent { responsiblesEng: [''], lvSwsLecturerEng: [''] }); + this.proposalForm.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.formValueVersion.update((n) => n + 1)); } goToStep(index: number) { @@ -176,7 +194,9 @@ export abstract class ProposalBaseComponent { } degreeProgramsAvailableForRow(rowIndex: number): DegreeProgramDTO[] { - const selected = this.assignments().map((a) => a.degreeProgramId).filter(Boolean) as number[]; + const selected = this.assignments() + .map((a) => a.degreeProgramId) + .filter(Boolean) as number[]; return this.degreePrograms().filter((p) => { const current = this.assignments()[rowIndex]?.degreeProgramId; return !selected.includes(p.degreeProgramId) || p.degreeProgramId === current; diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts index 82f4f120..aec20852 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.ts @@ -3,12 +3,11 @@ import { CommonModule } from '@angular/common'; import { ButtonModule } from 'primeng/button'; import type { ModuleEditStepConfig } from './module-edit-steps.config'; import type { ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; -import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; @Component({ selector: 'app-module-edit-stepper', standalone: true, - imports: [CommonModule, ButtonModule, FeedbackStatusPipe], + imports: [CommonModule, ButtonModule], templateUrl: './module-edit-stepper.component.html', styleUrl: './module-edit-stepper.component.css' }) diff --git a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts index 060032a6..94f646df 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts +++ b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts @@ -12,7 +12,7 @@ export const MODULE_EDIT_STEPS: ModuleEditStepConfig[] = [ controlNames: [ 'titleEng', 'titleDe', - 'bulletPoints', + // 'bulletPoints', 'credits', 'frequencyEng', 'hoursLecture', @@ -21,7 +21,7 @@ export const MODULE_EDIT_STEPS: ModuleEditStepConfig[] = [ 'hoursSeminar', 'firstSemesterAvailable', 'successorModuleName', - 'levelEng', + // 'levelEng', 'languageEng' ], requiredControlNames: ['titleEng'] @@ -34,19 +34,12 @@ export const MODULE_EDIT_STEPS: ModuleEditStepConfig[] = [ { id: 'examination-prereqs', title: 'Examination & prerequisites', - controlNames: ['examinationAchievementsEng', 'examinationAchievementsPromptEng', 'recommendedPrerequisitesEng'] + controlNames: ['examinationAchievementsEng', 'recommendedPrerequisitesEng'] }, { id: 'content-learning-teaching', title: 'Content, learning & teaching', - controlNames: [ - 'contentEng', - 'contentPromptEng', - 'learningOutcomesEng', - 'learningOutcomesPromptEng', - 'teachingMethodsEng', - 'teachingMethodsPromptEng' - ] + controlNames: ['contentEng', 'learningOutcomesEng', 'teachingMethodsEng'] }, { id: 'media-literature', From 90237af3137305d2c087c1effe775a08a2db17da Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 8 Mar 2026 22:33:12 +0100 Subject: [PATCH 06/42] fix --- .../module-version-edit.component.ts | 2 +- .../proposal-create.component.ts | 2 +- .../ls1/services/ModuleVersionService.java | 17 ++++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts index 939b7820..9484f8f9 100644 --- a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts +++ b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts @@ -100,7 +100,7 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent { const payload: ModuleVersionUpdateRequestDTO = { ...this.proposalForm.value, moduleVersionId: this.moduleVersionId, - degreeProgramAssignments: degreeProgramAssignments.length > 0 ? degreeProgramAssignments : undefined + degreeProgramAssignments }; this.moduleVersionService.updateModuleVersion(this.moduleVersionId, payload).subscribe({ next: (response: ModuleVersionViewDTO) => this.moduleVersionDto.set(response), diff --git a/Client/src/app/pages/proposal-create/proposal-create.component.ts b/Client/src/app/pages/proposal-create/proposal-create.component.ts index 20743332..7d8faaa5 100644 --- a/Client/src/app/pages/proposal-create/proposal-create.component.ts +++ b/Client/src/app/pages/proposal-create/proposal-create.component.ts @@ -59,7 +59,7 @@ export class ProposalCreateComponent extends ProposalBaseComponent { const body: ProposalRequestDTO = { ...this.proposalForm.value, titleEng: this.proposalForm.value.titleEng?.trim() ?? '', - degreeProgramAssignments: degreeProgramAssignments.length > 0 ? degreeProgramAssignments : undefined + degreeProgramAssignments }; this.loading.set(true); this.error.set(null); diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index 020688ab..b6ea2b4a 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -116,13 +116,16 @@ public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long mod mv.setResponsiblesEng(request.getResponsiblesEng()); mv.setLvSwsLecturerEng(request.getLvSwsLecturerEng()); - if (request.getDegreeProgramAssignments() != null && !request.getDegreeProgramAssignments().isEmpty()) { - List programIds = request.getDegreeProgramAssignments().stream() - .map(ModuleDegreeProgramAssignmentDTO::getDegreeProgramId) - .collect(Collectors.toList()); - if (programIds.size() != programIds.stream().distinct().count()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, - "A module cannot be assigned to the same degree program more than once."); + if (request.getDegreeProgramAssignments() != null) { + List assignments = request.getDegreeProgramAssignments(); + if (!assignments.isEmpty()) { + List programIds = assignments.stream() + .map(ModuleDegreeProgramAssignmentDTO::getDegreeProgramId) + .collect(Collectors.toList()); + if (programIds.size() != programIds.stream().distinct().count()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "A module cannot be assigned to the same degree program more than once."); + } } assignmentRepository.deleteByModuleVersion_ModuleVersionId(mv.getModuleVersionId()); entityManager.flush(); From 0541fbef5c8ca95d88893527c0510b0b4b7e4df1 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Tue, 17 Mar 2026 21:44:25 +0100 Subject: [PATCH 07/42] first iteration without showing feedbacks --- .../create-edit-base.component.html | 185 ++++++--- .../create-edit-base.component.ts | 120 ++++-- .../module-edit-stepper.component.css | 6 + .../module-edit-stepper.component.html | 11 +- .../module-edit-stepper.component.ts | 8 +- .../module-edit-steps.config.ts | 25 ++ .../modules/openapi/.openapi-generator/FILES | 1 - .../api/module-version-controller.service.ts | 73 +--- ...ule-version-controller.serviceInterface.ts | 9 +- .../api/proposal-controller.service.ts | 167 +++----- .../proposal-controller.serviceInterface.ts | 24 +- .../openapi/model/add-module-version-dto.ts | 15 - .../openapi/model/feedback-list-item-dto.ts | 7 +- .../core/modules/openapi/model/feedback.ts | 16 +- .../app/core/modules/openapi/model/models.ts | 1 - .../model/module-version-compact-dto.ts | 11 +- .../module-version-update-request-dto.ts | 11 +- .../openapi/model/module-version-view-dto.ts | 11 +- .../model/module-version-view-feedback-dto.ts | 15 +- .../openapi/model/proposal-view-dto.ts | 11 +- .../openapi/model/proposals-compact-dto.ts | 11 +- .../openapi/model/similar-module-dto.ts | 2 +- .../openapi/model/update-user-role-dto.ts | 6 +- .../core/modules/openapi/model/user-dto.ts | 6 +- .../app/core/modules/openapi/model/user.ts | 6 +- Client/src/app/core/shared/user-role.utils.ts | 10 +- .../module-version-edit.component.ts | 22 +- .../module-version-view.component.css | 23 ++ .../module-version-view.component.html | 370 ++++++++++++------ .../module-version-view.component.ts | 54 ++- .../proposal-create.component.ts | 6 +- .../proposal-view.component.html | 34 +- .../proposal-view/proposal-view.component.ts | 30 +- .../src/app/pipes/feedbackDepartment.pipe.ts | 11 +- Client/src/app/pipes/feedbackStatus.pipe.ts | 22 +- .../src/app/pipes/moduleVersionStatus.pipe.ts | 29 +- Client/src/app/pipes/proposalStatus.pipe.ts | 32 +- .../ls1/controllers/FeedbackController.java | 16 +- .../controllers/ModuleVersionController.java | 15 +- .../ls1/controllers/ProposalController.java | 33 +- .../dtos/ModuleVersionViewFeedbackDTO.java | 14 +- .../ls1/enums/FeedbackStatus.java | 9 +- .../ls1/enums/ModuleVersionStatus.java | 24 +- .../ls1/enums/ProposalStatus.java | 18 +- .../modulemanagement/ls1/enums/UserRole.java | 6 +- .../ls1/models/DegreeProgram.java | 2 +- .../models/DegreeProgramSpecialization.java | 2 +- .../modulemanagement/ls1/models/Feedback.java | 51 ++- .../ls1/models/ModuleVersion.java | 23 +- .../modulemanagement/ls1/models/Proposal.java | 31 +- .../repositories/DegreeProgramRepository.java | 5 + ...DegreeProgramSpecializationRepository.java | 5 + .../ls1/repositories/FeedbackRepository.java | 4 + .../ls1/repositories/ProposalRepository.java | 5 + .../ls1/services/AdminUserService.java | 16 +- .../ls1/services/DegreeProgramService.java | 24 +- .../DegreeProgramSpecializationService.java | 24 +- .../ls1/services/FeedbackService.java | 39 +- .../ls1/services/ModuleVersionService.java | 160 ++++++-- .../ls1/services/ProposalService.java | 165 ++++---- .../services/ResponsibleUserRoleService.java | 96 +++++ .../0008_degree_program_and_area_tables.yaml | 4 +- .../0012_feedback_requested_from_user.yaml | 26 ++ ...3_grant_program_area_responsible_role.yaml | 33 ++ .../main/resources/db/changelog/master.yaml | 6 + 65 files changed, 1401 insertions(+), 856 deletions(-) delete mode 100644 Client/src/app/core/modules/openapi/model/add-module-version-dto.ts create mode 100644 Client/src/app/pages/module-version-view/module-version-view.component.css create mode 100644 Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java create mode 100644 Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml create mode 100644 Server/src/main/resources/db/changelog/changes/0013_grant_program_area_responsible_role.yaml diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index 8aa31cbf..a30ab661 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -1,13 +1,6 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' + moduleVersionDto()?.titleEng : 'Create New Module Proposal' }}

@@ -31,10 +24,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('titleFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.titleFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.titleFeedback }}

} @@ -123,10 +116,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('levelFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.levelFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.levelFeedback }}

} @@ -150,10 +143,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('languageFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.languageFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.languageFeedback }}

} @@ -211,15 +204,63 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

} @if (currentStepIndex() === 1) { +
+ @if (isCreateMode()) { + + Complete the first step and create the proposal. After saving, you can use this step to submit for feedback from program coordinators and area responsibles. + + } @else { +

+ Submit to request feedback from program coordinators and area responsibles (first submission). One feedback is required per chosen degree program and area. Complete + step 1 first, then use the button below. You can cancel and resubmit later if needed. +

+ @if (!isFirstStepComplete()) { + + Complete step 1: fill basic information and add at least one degree program assignment. Then you can submit for coordinator feedback. + + } + +
+ @if (canRequestCoordinatorsFeedback()) { + + } +
+ } +
+ } + @if (currentStepIndex() === 2) {
@if (moduleVersionId && hasFeedback('durationFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.durationFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.durationFeedback }}

} @@ -243,10 +284,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('repetitionFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.repetitionFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.repetitionFeedback }}

} @@ -270,10 +311,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('creditsFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.creditsFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.creditsFeedback }}

} @@ -288,10 +329,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('hoursTotalFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.hoursTotalFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.hoursTotalFeedback }}

} @@ -306,10 +347,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('hoursSelfStudyFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.hoursSelfStudyFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.hoursSelfStudyFeedback }}

} @@ -324,10 +365,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('hoursPresenceFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.hoursPresenceFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.hoursPresenceFeedback }}

} @@ -341,16 +382,16 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

} - @if (currentStepIndex() === 2) { + @if (currentStepIndex() === 3) {
@if (moduleVersionId && hasFeedback('examinationAchievementsFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.examinationAchievementsFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.examinationAchievementsFeedback }}

} @@ -392,10 +433,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('recommendedPrerequisitesFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.recommendedPrerequisitesFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.recommendedPrerequisitesFeedback }}

} @@ -408,16 +449,16 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

} - @if (currentStepIndex() === 3) { + @if (currentStepIndex() === 4) {
@if (moduleVersionId && hasFeedback('contentFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.contentFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.contentFeedback }}

} @@ -450,10 +491,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('learningOutcomesFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.learningOutcomesFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.learningOutcomesFeedback }}

} @@ -486,10 +527,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('teachingMethodsFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.teachingMethodsFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.teachingMethodsFeedback }}

} @@ -520,16 +561,16 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

} - @if (currentStepIndex() === 4) { + @if (currentStepIndex() === 5) {
@if (moduleVersionId && hasFeedback('mediaFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.mediaFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.mediaFeedback }}

} @@ -545,10 +586,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('literatureFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.literatureFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.literatureFeedback }}

} @@ -564,10 +605,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('responsiblesFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.responsiblesFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.responsiblesFeedback }}

} @@ -583,10 +624,10 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (moduleVersionId && hasFeedback('lvSwsLecturerFeedback')) { - @for (feedback of feedbacks(); track feedback.feedbackRole) { + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.lvSwsLecturerFeedback) {

- {{ (feedback.feedbackRole! | feedbackDepartment).text }}: + {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: {{ feedback.lvSwsLecturerFeedback }}

} @@ -600,6 +641,53 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

} + @if (currentStepIndex() === 6) { +
+ @if (isCreateMode()) { + + Complete all steps and get coordinator feedback accepted first. Then you can submit for full feedback (quality management, program advisor, examination board). + + } @else { +

+ Submit for full feedback from quality management, academic program advisor, and examination board. This requires all steps to be completed and all program/area + coordinator feedback to be accepted. +

+ + @if (canRequestFullFeedback()) { + + } + } +
+ } +
Cancel
@@ -620,9 +708,6 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' @if (!isCreateMode() && currentStepIndex() < MODULE_EDIT_STEPS.length - 1) { Next } - @if (!isCreateMode() && currentStepIndex() === MODULE_EDIT_STEPS.length - 1 && canSubmitForFeedback()) { - Submit for feedback - } @if (!isCreateMode()) { {{ loading() ? 'Saving...' : 'Update proposal' }} diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.ts b/Client/src/app/components/create-edit-base/create-edit-base.component.ts index c43b1880..4f1e6bf1 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.ts +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.ts @@ -8,13 +8,15 @@ import { ProposalControllerService, ModuleVersionViewDTO, CompletionServiceResponseDTO, - ModuleVersionViewFeedbackDTO + ModuleVersionViewFeedbackDTO, + ProposalViewDTO } from '../../core/modules/openapi'; import { DegreeProgramsControllerService } from '../../core/modules/openapi/api/degree-programs-controller.service'; import { Location } from '@angular/common'; import { Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; -import { MODULE_EDIT_STEPS } from '../module-edit-stepper/module-edit-steps.config'; +import { MODULE_EDIT_STEPS, StepperStatus } from '../module-edit-stepper/module-edit-steps.config'; +import { BreadcrumbLabelsService } from '../breadcrumb/breadcrumb-labels.service'; @Component({ template: '' @@ -26,6 +28,7 @@ export abstract class ProposalBaseComponent { protected moduleVersionService = inject(ModuleVersionControllerService); protected proposalService = inject(ProposalControllerService); protected degreeProgramsService = inject(DegreeProgramsControllerService); + protected breadcrumbLabels = inject(BreadcrumbLabelsService); readonly MODULE_EDIT_STEPS = MODULE_EDIT_STEPS; @@ -46,19 +49,27 @@ export abstract class ProposalBaseComponent { /** Updated on form valueChanges so stepCompleted computed re-runs when user types. */ private formValueVersion = signal(0); - stepCompleted = computed(() => { + stepsStatuses = computed(() => { this.formValueVersion(); const form = this.proposalForm; const assignmentsList = this.assignments(); - return MODULE_EDIT_STEPS.map((step, index) => { - const allFieldsFilled = step.controlNames.every((name) => this.controlHasValue(form.get(name))); + return MODULE_EDIT_STEPS.map((step) => { if (step.id === 'basic') { - const hasCompleteAssignment = assignmentsList.some( - (a) => a.degreeProgramId != null && a.degreeProgramSpecializationId != null - ); - return allFieldsFilled && hasCompleteAssignment; + const allFieldsFilled = step.controlNames.every((name) => this.controlHasValue(form.get(name))); + const hasCompleteAssignment = assignmentsList.some((a) => a.degreeProgramId != null && a.degreeProgramSpecializationId != null); + if (allFieldsFilled && hasCompleteAssignment) { + return StepperStatus.Completed; + } else { + return StepperStatus.Default; + } } - return allFieldsFilled; + if (step.id === 'submit-coordinator-feedback') { + return StepperStatus.Default; + } + if (step.id === 'submit-full-feedback') { + return StepperStatus.Default; + } + return step.controlNames.every((name) => this.controlHasValue(form.get(name))) ? StepperStatus.Completed : StepperStatus.Default; }); }); @@ -76,23 +87,31 @@ export abstract class ProposalBaseComponent { return dto && 'status' in dto ? (dto as ModuleVersionViewDTO).status : undefined; }); - currentVersionFeedbacks = computed(() => { - const dto = this.moduleVersionDto(); - return dto && 'feedbacks' in dto ? ((dto as ModuleVersionViewDTO).feedbacks ?? []) : []; + /** First step (basic + assignments) is complete. */ + isFirstStepComplete = computed(() => this.stepsStatuses()[0] === StepperStatus.Completed); + + /** Can submit for feedback (never submitted yet). */ + canRequestCoordinatorsFeedback = computed(() => { + const status = this.moduleVersionStatus(); + return status === 'PENDING_FIRST_SUBMISSION' && this.isFirstStepComplete(); }); - canSubmitForFeedback = computed(() => { - const dto = this.moduleVersionDto(); - const status = dto && 'status' in dto ? (dto as ModuleVersionViewDTO).status : null; - return status === 'PENDING_SUBMISSION' && this.proposalForm.valid; + /** All form steps (basic, schedule, exam, content, media) are complete – not the two submission steps. */ + allFormStepsComplete = computed(() => { + const completed = this.stepsStatuses(); + return ( + completed[0] === StepperStatus.Completed && + completed[2] === StepperStatus.Completed && + completed[3] === StepperStatus.Completed && + completed[4] === StepperStatus.Completed && + completed[5] === StepperStatus.Completed + ); }); - private _syncAssignmentsFromDto = effect(() => { - const dto = this.moduleVersionDto(); - if (dto && 'degreeProgramAssignments' in dto && Array.isArray((dto as ModuleVersionViewDTO).degreeProgramAssignments)) { - const list = (dto as ModuleVersionViewDTO).degreeProgramAssignments ?? []; - this.assignments.set(list.map((a) => ({ degreeProgramId: a.degreeProgramId ?? null, degreeProgramSpecializationId: a.degreeProgramSpecializationId ?? null }))); - } + /** Can submit for full feedback (second submission): PENDING_FULL_SUBMISSION, all steps done, all coordinator feedback accepted. */ + canRequestFullFeedback = computed(() => { + const status = this.moduleVersionStatus(); + return status === 'PENDING_FULL_SUBMISSION' && this.allFormStepsComplete(); }); showPrompt: { [key: string]: boolean } = { @@ -102,13 +121,6 @@ export abstract class ProposalBaseComponent { teaching: false }; - fieldMapping: { [key: string]: string } = { - content: 'contentEng', - examination: 'examinationAchievementsEng', - learning: 'learningOutcomesEng', - teaching: 'teachingMethodsEng' - }; - togglePromptField(field: string) { this.showPrompt[field] = !this.showPrompt[field]; } @@ -213,13 +225,53 @@ export abstract class ProposalBaseComponent { this.setAssignmentSpecialization(rowIndex, null); } - submitForFeedback(): void { + requestCoordinatorsFeedback(): void { + const dto = this.moduleVersionDto(); + const proposalId = dto && 'proposalId' in dto ? (dto as ModuleVersionViewDTO).proposalId : null; + if (proposalId == null) return; + this.loading.set(true); + this.error.set(null); + this.proposalService.requestCoordinatorsFeedback(proposalId).subscribe({ + next: (response: ProposalViewDTO) => { + this.moduleVersionDto.set(response); + // When backend created a new version (immutable versioning), switch to editing the new version + const newId = response?.latestModuleVersion?.moduleVersionId; + if (newId != null && newId !== this.moduleVersionId) { + this.moduleVersionId = newId; + this.breadcrumbLabels.versionLabel.set(response?.latestVersion != null ? `Version ${response.latestVersion}` : null); + this.router.navigate(['/proposals/view', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); + } + }, + error: (err: HttpErrorResponse) => { + this.error.set(err.error?.message ?? err.error ?? 'Failed to submit'); + this.loading.set(false); + }, + complete: () => this.loading.set(false) + }); + } + + requestFullFeedback(): void { const dto = this.moduleVersionDto(); const proposalId = dto && 'proposalId' in dto ? (dto as ModuleVersionViewDTO).proposalId : null; if (proposalId == null) return; - this.proposalService.submitProposal(proposalId).subscribe({ - next: () => this.router.navigate(['/proposals/view', proposalId]), - error: (err: HttpErrorResponse) => this.error.set(err.error?.message ?? err.error ?? 'Failed to submit') + this.loading.set(true); + this.error.set(null); + this.proposalService.requestFullFeedback(proposalId).subscribe({ + next: (response: ProposalViewDTO) => { + this.moduleVersionDto.set(response); + // When backend created a new version (immutable versioning), switch to editing the new version + const newId = response?.latestModuleVersion?.moduleVersionId; + if (newId != null && newId !== this.moduleVersionId) { + this.moduleVersionId = newId; + this.breadcrumbLabels.versionLabel.set(response?.latestVersion != null ? `Version ${response.latestVersion}` : null); + this.router.navigate(['/proposals/view', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); + } + }, + error: (err: HttpErrorResponse) => { + this.error.set(err.error?.message ?? err.error ?? 'Failed to submit for full feedback'); + this.loading.set(false); + }, + complete: () => this.loading.set(false) }); } diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css index 36cb4894..59540382 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css @@ -32,6 +32,12 @@ color: white; } +/* Feedback step(s): yellow/orange when at least one feedback is pending */ +.step-item--pending .step-number { + background: var(--p-warn-color, #eab308); + color: var(--p-primary-contrast-color, #1a1a1a); +} + .step-number { flex-shrink: 0; width: 1.75rem; diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html index d2c15a78..3b49daf3 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html @@ -1,8 +1,15 @@

} @else if (moduleVersionDto(); as moduleVersionDto) { -
-

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ moduleVersionDto.version }}

-
- -
- @if (proposalId) { - - } - @if (isLatestVersion() && moduleVersionDto.status == moduleVersionStatus.PendingSubmission && proposalId) { - - } +
+ +
+

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ moduleVersionDto.version }}

+
+ +
+ @if (proposalId) { + + } + @if (isLatestVersion() && proposalId) { + + } +
-
-
-
-

Module Version ID

-

{{ moduleVersionDto.moduleVersionId }}

-
-
-

Version

-

{{ moduleVersionDto.version }} / {{ moduleVersionDto.latestVersion }}

-
-
-

Status

- -
-
-

Created On

-

{{ moduleVersionDto.creationDate | date: 'yyyy-MM-dd' }}

+
+
+

Module Version ID

+

{{ moduleVersionDto.moduleVersionId }}

+
+
+

Version

+

{{ moduleVersionDto.version }} / {{ moduleVersionDto.latestVersion }}

+
+
+

Status

+ +
+
+

Created On

+

{{ moduleVersionDto.creationDate | date: 'yyyy-MM-dd' }}

+
-
- - @if (moduleVersionDto.feedbacks?.length) { -
- @for (feedback of moduleVersionDto.feedbacks; track feedback.feedbackId) { - - -
-

{{ feedback.feedbackFromFirstName }} {{ feedback.feedbackFromLastName }}

-

- {{ feedback.submissionDate | date: 'yyyy-MM-dd' }} -

-
- - @if (feedback.rejectionComment) { - -

Rejection Comment:

-

{{ feedback.rejectionComment }}

-
+ @if (currentStepIndex() === 0) { +
+ @for (field of getFieldsForViewStep(0); track field.key) { + +

+ @if (getModuleVersionProperty(field.key)) { + {{ getModuleVersionProperty(field.key) }} + } @else { + Not specified + } +

+ @if (field.feedbackKey && getFieldFeedbacks(field.key).length > 0) { +
+ @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { + +

{{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}

+

{{ getFeedbackContent(feedback, field.key) }}

+
+ } +
} +
+ } +
+ @if (moduleVersionDto.degreeProgramAssignments?.length) { + +
    + @for (a of moduleVersionDto.degreeProgramAssignments; track a.degreeProgramId + '-' + a.degreeProgramSpecializationId) { +
  • Program {{ a.degreeProgramId }} – Specialization {{ a.degreeProgramSpecializationId }}
  • + } +
+
+ } + } + + @if (currentStepIndex() === 1) { + + } -
- @for (field of getFeedbackFields(feedback); track field.key) { - -

{{ field.label }}

-

{{ field.value }}

-
+ @if (currentStepIndex() === 2) { +
+ @for (field of getFieldsForViewStep(2); track field.key) { + +

+ @if (getModuleVersionProperty(field.key)) { + {{ getModuleVersionProperty(field.key) }} + } @else { + Not specified } -

- +

+ @if (field.feedbackKey && getFieldFeedbacks(field.key).length > 0) { +
+ @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { + +

{{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}

+

{{ getFeedbackContent(feedback, field.key) }}

+
+ } +
+ } + }
- } @else { -

No feedback provided yet

} - -
-

Module Version Data

-
- @for (field of getFieldsBySection('basic'); track field.key) { - -

+ @if (currentStepIndex() === 3) { +

+ @for (field of getFieldsForViewStep(3); track field.key) { + @if (getModuleVersionProperty(field.key)) { - {{ getModuleVersionProperty(field.key) }} +

{{ getModuleVersionProperty(field.key) }}

+ @if (field.hasPrompt && getModuleVersionProperty(field.hasPrompt)) { + +

{{ getModuleVersionProperty(field.hasPrompt) }}

+
+ } + @if (field.feedbackKey && getFieldFeedbacks(field.key).length > 0) { +
+ @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { + +

{{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}

+

{{ getFeedbackContent(feedback, field.key) }}

+
+ } +
+ } } @else { - Not specified +

Not specified

} -

- @if (getFieldFeedbacks(field.key).length > 0) { -
- @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { - -

{{ (feedback.feedbackRole! | feedbackDepartment).text }}

-

{{ getFeedbackContent(feedback, field.key) }}

-
- } -
- } -
- } -
-
+ + } +
+ } -
-
- @for (field of getFieldsBySection('hours'); track field.key) { - -

+ @if (currentStepIndex() === 4) { +

+ @for (field of getFieldsForViewStep(4); track field.key) { + @if (getModuleVersionProperty(field.key)) { - {{ getModuleVersionProperty(field.key) }} +

{{ getModuleVersionProperty(field.key) }}

+ @if (field.hasPrompt && getModuleVersionProperty(field.hasPrompt)) { + +

{{ getModuleVersionProperty(field.hasPrompt) }}

+
+ } + @if (field.feedbackKey && getFieldFeedbacks(field.key).length > 0) { +
+ @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { + +

{{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}

+

{{ getFeedbackContent(feedback, field.key) }}

+
+ } +
+ } } @else { - Not specified +

Not specified

} -

- @if (getFieldFeedbacks(field.key).length > 0) { -
- @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { - -

{{ (feedback.feedbackRole! | feedbackDepartment).text }}

-

{{ getFeedbackContent(feedback, field.key) }}

-
+ + } +
+ } + + @if (currentStepIndex() === 5) { +
+ @for (field of getFieldsForViewStep(5); track field.key) { + + @if (getModuleVersionProperty(field.key)) { +

{{ getModuleVersionProperty(field.key) }}

+ @if (field.feedbackKey && getFieldFeedbacks(field.key).length > 0) { +
+ @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { + +

{{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}

+

{{ getFeedbackContent(feedback, field.key) }}

+
+ } +
} -
- } -
- } -
-
+ } @else { +

Not specified

+ } +
+ } +
+ } - @for (field of getFieldsBySection('content'); track field.key) { - - @if (getModuleVersionProperty(field.key)) { -

{{ getModuleVersionProperty(field.key) }}

- @if (field.hasPrompt && getModuleVersionProperty(field.hasPrompt)) { - -

{{ getModuleVersionProperty(field.hasPrompt) }}

-
+ @if (currentStepIndex() === 6) { + +

+ Second submission: after all steps are complete and coordinator feedback is accepted, you can submit for feedback from quality management, academic program advisor, and + examination board. +

+ @if (moduleVersionDto.feedbacks?.length) { +
+

Feedback status

+
    + @for (fb of moduleVersionDto.feedbacks; track fb.feedbackId) { +
  • + {{ fb.requestedFromUserName ?? (fb.feedbackRole | feedbackDepartment).text }}: + +
  • + } +
+
} - @if (getFieldFeedbacks(field.key).length > 0) { -
- @for (feedback of getFieldFeedbacks(field.key); track feedback.feedbackId) { - -

{{ (feedback.feedbackRole! | feedbackDepartment).text }}

-

{{ getFeedbackContent(feedback, field.key) }}

-
+ + } + + @if (currentStepIndex() === 7) { + + @if (moduleVersionDto.feedbacks?.length) { +
+ @for (feedback of moduleVersionDto.feedbacks; track feedback.feedbackId) { + + +
+

{{ feedback.feedbackFromFirstName }} {{ feedback.feedbackFromLastName }}

+

+ {{ feedback.submissionDate | date: 'yyyy-MM-dd' }} +

+
+ + @if (feedback.rejectionComment) { + +

Rejection Comment:

+

{{ feedback.rejectionComment }}

+
+ } + +
+ @for (field of getFeedbackFields(feedback); track field.key) { + +

{{ field.label }}

+

{{ field.value }}

+
+ } +
+
}
+ } @else { +

No feedback provided yet

} - } @else { -

No content provided

- } -
- } + + } +
} @else {
No module version found
diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.ts b/Client/src/app/pages/module-version-view/module-version-view.component.ts index cc12cf97..2c6925e7 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.ts +++ b/Client/src/app/pages/module-version-view/module-version-view.component.ts @@ -1,9 +1,11 @@ import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; -import { Component, inject, signal } from '@angular/core'; +import { Component, computed, inject, signal } from '@angular/core'; import { RouterModule, ActivatedRoute } from '@angular/router'; import { ModuleVersionControllerService, ModuleVersionViewDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; +import { ModuleEditStepperComponent } from '../../components/module-edit-stepper/module-edit-stepper.component'; +import { MODULE_VIEW_STEPS, StepperStatus } from '../../components/module-edit-stepper/module-edit-steps.config'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; import { ModuleVersionStatusPipe } from '../../pipes/moduleVersionStatus.pipe'; @@ -29,6 +31,7 @@ export interface ModuleField { imports: [ CommonModule, RouterModule, + ModuleEditStepperComponent, ModuleVersionStatusPipe, FeedbackStatusPipe, FeedbackDepartmentPipe, @@ -39,7 +42,8 @@ export interface ModuleField { CardModule, PanelModule ], - templateUrl: './module-version-view.component.html' + templateUrl: './module-version-view.component.html', + styleUrl: './module-version-view.component.css' }) export class ModuleVersionViewComponent { route = inject(ActivatedRoute); @@ -52,14 +56,24 @@ export class ModuleVersionViewComponent { moduleVersionStatus = ModuleVersionViewDTO.StatusEnum; error = signal(null); + readonly MODULE_VIEW_STEPS = MODULE_VIEW_STEPS; + currentStepIndex = signal(0); + moduleFields: ModuleField[] = [ { key: 'titleEng', label: 'Title', section: 'basic', feedbackKey: 'titleFeedback' }, + { key: 'titleDe', label: 'Title (German)', section: 'basic' }, { key: 'levelEng', label: 'Level', section: 'basic', feedbackKey: 'levelFeedback' }, { key: 'languageEng', label: 'Language', section: 'basic', feedbackKey: 'languageFeedback' }, { key: 'frequencyEng', label: 'Frequency', section: 'basic', feedbackKey: 'frequencyFeedback' }, + { key: 'credits', label: 'Credits', section: 'hours', feedbackKey: 'creditsFeedback' }, + { key: 'hoursLecture', label: 'Hours (Lecture)', section: 'basic' }, + { key: 'hoursExercise', label: 'Hours (Exercise)', section: 'basic' }, + { key: 'hoursPractical', label: 'Hours (Practical)', section: 'basic' }, + { key: 'hoursSeminar', label: 'Hours (Seminar)', section: 'basic' }, + { key: 'firstSemesterAvailable', label: 'First semester available', section: 'basic' }, + { key: 'successorModuleName', label: 'Successor module', section: 'basic' }, { key: 'duration', label: 'Duration', section: 'basic', feedbackKey: 'durationFeedback' }, { key: 'repetitionEng', label: 'Repetition', section: 'basic', feedbackKey: 'repetitionFeedback' }, - { key: 'credits', label: 'Credits', section: 'hours', feedbackKey: 'creditsFeedback' }, { key: 'hoursTotal', label: 'Total Hours', section: 'hours', feedbackKey: 'hoursTotalFeedback' }, { key: 'hoursSelfStudy', label: 'Self-Study Hours', section: 'hours', feedbackKey: 'hoursSelfStudyFeedback' }, { key: 'hoursPresence', label: 'Presence Hours', section: 'hours', feedbackKey: 'hoursPresenceFeedback' }, @@ -89,6 +103,28 @@ export class ModuleVersionViewComponent { { key: 'lvSwsLecturerEng', label: 'Lecturer', section: 'content', isLongText: true, feedbackKey: 'lvSwsLecturerFeedback' } ]; + stepStatuses = computed(() => { + const dto = this.moduleVersionDto(); + if (!dto) return MODULE_VIEW_STEPS.map(() => StepperStatus.Default); + + return MODULE_VIEW_STEPS.map((step, index) => { + if (step.id === 'feedbacks') return StepperStatus.Default; + if (step.id === 'submit-coordinator-feedback') return StepperStatus.Default; + if (step.id === 'submit-full-feedback') return StepperStatus.Default; + + const keys = MODULE_VIEW_STEPS[index].controlNames as (keyof ModuleVersionViewDTO)[]; + if (!keys?.length) return StepperStatus.Default; + const allFilled = keys.every((k) => { + const v = dto[k]; + if (v === undefined || v === null) return false; + if (typeof v === 'string') return v.trim() !== ''; + if (typeof v === 'number') return true; + return true; + }); + return allFilled ? StepperStatus.Completed : StepperStatus.Default; + }); + }); + constructor() { const params = this.route.snapshot.paramMap; this.proposalId = params.get('id'); @@ -97,9 +133,19 @@ export class ModuleVersionViewComponent { this.fetchModuleVersionViewDto(this.moduleVersionId); } + goToStep(index: number) { + this.currentStepIndex.set(index); + } + + getFieldsForViewStep(stepIndex: number): ModuleField[] { + const keys = MODULE_VIEW_STEPS[stepIndex].controlNames as (keyof ModuleVersionViewDTO)[]; + if (!keys?.length) return []; + return this.moduleFields.filter((f) => keys.includes(f.key)); + } + private fetchModuleVersionViewDto(moduleVersionId: number) { this.loading.set(true); - this.moduleVersionService.getModuleVersionViewDto(moduleVersionId).subscribe({ + this.moduleVersionService.getModuleVersion(moduleVersionId).subscribe({ next: (data: ModuleVersionViewDTO) => { this.moduleVersionDto.set(data); const version = data?.version; diff --git a/Client/src/app/pages/proposal-create/proposal-create.component.ts b/Client/src/app/pages/proposal-create/proposal-create.component.ts index 7d8faaa5..e9ee0d85 100644 --- a/Client/src/app/pages/proposal-create/proposal-create.component.ts +++ b/Client/src/app/pages/proposal-create/proposal-create.component.ts @@ -5,6 +5,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import { ProposalBaseComponent } from '../../components/create-edit-base/create-edit-base.component'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; +import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; import { ToggleButtonGroupComponent } from '../../components/toggle-button-group/toggle-button-group.component'; import { ModuleEditStepperComponent } from '../../components/module-edit-stepper/module-edit-stepper.component'; import { ModuleDegreeProgramAssignmentDTO, ProposalRequestDTO } from '../../core/modules/openapi'; @@ -15,6 +16,7 @@ import { InputNumberModule } from 'primeng/inputnumber'; import { MessageModule } from 'primeng/message'; import { SelectModule } from 'primeng/select'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { TagModule } from 'primeng/tag'; @Component({ selector: 'app-proposal-create', @@ -24,6 +26,7 @@ import { ProgressSpinnerModule } from 'primeng/progressspinner'; FormsModule, RouterModule, FeedbackDepartmentPipe, + FeedbackStatusPipe, ToggleButtonGroupComponent, ModuleEditStepperComponent, ButtonModule, @@ -32,7 +35,8 @@ import { ProgressSpinnerModule } from 'primeng/progressspinner'; InputNumberModule, MessageModule, SelectModule, - ProgressSpinnerModule + ProgressSpinnerModule, + TagModule ], templateUrl: '../../components/create-edit-base/create-edit-base.component.html', styleUrl: '../../components/create-edit-base/create-edit-base-layout.css' diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.html b/Client/src/app/pages/proposal-view/proposal-view.component.html index a3acfd93..1b1c0391 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.html +++ b/Client/src/app/pages/proposal-view/proposal-view.component.html @@ -83,33 +83,13 @@

Current Module Versions

- @if (proposal.latestModuleVersion!.isComplete && proposal.latestModuleVersion!.status == moduleStatusEnum.PendingSubmission) { - - } - @if (proposal.latestModuleVersion!.status === moduleStatusEnum.FeedbackGiven || proposal.latestModuleVersion!.status === moduleStatusEnum.Rejected) { - - } - @if (proposal.latestModuleVersion!.status === moduleStatusEnum.PendingSubmission) { - - } - @if (proposal.latestModuleVersion!.status === moduleStatusEnum.PendingFeedback) { - - } - @if (proposal.status === proposalStatusEnum.RequiresReview) { - - } + this.proposal.set(response), - error: (err: HttpErrorResponse) => this.error.set(err.error) - }); - } - } - - cancelProposal() { - if (this.proposal()) { - this.proposalService.cancelSubmission(this.proposal()!.proposalId!).subscribe({ - next: (response: ProposalViewDTO) => this.proposal.set(response), - error: (err: HttpErrorResponse) => this.error.set(err.error) - }); - } - } - deleteProposal() { if (this.proposal()) { this.proposalService.deleteProposal(this.proposal()!.proposalId!).subscribe({ @@ -99,14 +81,4 @@ export class ProposalViewComponent { }); } } - - addNewModuleVersion() { - if (this.proposal()) { - const addModuleVersionDto: AddModuleVersionDTO = { proposalId: this.proposal()!.proposalId! }; - this.proposalService.addModuleVersion(addModuleVersionDto).subscribe({ - next: (response: ProposalViewDTO) => this.proposal.set(response), - error: (err: HttpErrorResponse) => this.error.set(err.error) - }); - } - } } diff --git a/Client/src/app/pipes/feedbackDepartment.pipe.ts b/Client/src/app/pipes/feedbackDepartment.pipe.ts index a1ef5a94..21140f90 100644 --- a/Client/src/app/pipes/feedbackDepartment.pipe.ts +++ b/Client/src/app/pipes/feedbackDepartment.pipe.ts @@ -3,16 +3,21 @@ import { Feedback } from '../core/modules/openapi'; @Pipe({ name: 'feedbackDepartment', standalone: true }) export class FeedbackDepartmentPipe implements PipeTransform { - transform(status: Feedback.RequiredRoleEnum): { text: string } { - switch (status) { + transform(role: Feedback.RequiredRoleEnum | null | undefined): { text: string } { + if (role == null) return { text: 'Specialization area responsible' }; + switch (role) { case 'QUALITY_MANAGEMENT': return { text: 'Quality Management' }; case 'EXAMINATION_BOARD': return { text: 'Examination Board' }; case 'ACADEMIC_PROGRAM_ADVISOR': return { text: 'Academic Program Advisor' }; + case 'PROGRAM_COORDINATOR': + return { text: 'Program coordinator' }; + case 'SPECIALIZATION_AREA_RESPONSIBLE': + return { text: 'Specialization area responsible' }; default: - return { text: status }; + return { text: role }; } } } diff --git a/Client/src/app/pipes/feedbackStatus.pipe.ts b/Client/src/app/pipes/feedbackStatus.pipe.ts index 1f64907f..3b78d102 100644 --- a/Client/src/app/pipes/feedbackStatus.pipe.ts +++ b/Client/src/app/pipes/feedbackStatus.pipe.ts @@ -11,13 +11,6 @@ export class FeedbackStatusPipe implements PipeTransform { severity: Tag['severity']; } { switch (status) { - case 'PENDING_SUBMISSION': - return { - text: 'Pending Submission', - normalColor: 'bg-gray-500 text-white', - fadedColor: 'bg-gray-300 text-white', - severity: 'secondary' - }; case 'PENDING_FEEDBACK': return { text: 'Pending Feedback', @@ -46,20 +39,7 @@ export class FeedbackStatusPipe implements PipeTransform { fadedColor: 'bg-red-300 text-white', severity: 'danger' }; - case 'OBSOLETE': - return { - text: 'Obsolete', - normalColor: 'bg-gray-300 text-gray-600', - fadedColor: 'bg-gray-200 text-gray-600', - severity: 'secondary' - }; - case 'CANCELLED': - return { - text: 'Cancelled', - normalColor: 'bg-gray-400 text-white', - fadedColor: 'bg-gray-300 text-white', - severity: 'secondary' - }; + default: return { text: status, diff --git a/Client/src/app/pipes/moduleVersionStatus.pipe.ts b/Client/src/app/pipes/moduleVersionStatus.pipe.ts index f12e9aae..a6569985 100644 --- a/Client/src/app/pipes/moduleVersionStatus.pipe.ts +++ b/Client/src/app/pipes/moduleVersionStatus.pipe.ts @@ -14,16 +14,30 @@ export class ModuleVersionStatusPipe implements PipeTransform { severity: Tag['severity']; } { switch (status) { - case ModuleVersionCompactDTO.StatusEnum.PendingSubmission: + case ModuleVersionCompactDTO.StatusEnum.PendingFirstSubmission: return { - text: 'Pending Submission', + text: 'Pending first submission', normalColor: 'bg-gray-500', fadedColor: 'bg-gray-300', severity: 'secondary' }; - case ModuleVersionCompactDTO.StatusEnum.PendingFeedback: + case ModuleVersionCompactDTO.StatusEnum.PendingCoordinatorFeedback: return { - text: 'Pending Feedback', + text: 'Pending coordinator feedback', + normalColor: 'bg-yellow-500', + fadedColor: 'bg-yellow-300', + severity: 'warn' + }; + case ModuleVersionCompactDTO.StatusEnum.PendingFullSubmission: + return { + text: 'Pending full submission', + normalColor: 'bg-gray-500', + fadedColor: 'bg-gray-300', + severity: 'secondary' + }; + case ModuleVersionCompactDTO.StatusEnum.PendingFullFeedback: + return { + text: 'Pending full feedback', normalColor: 'bg-yellow-500', fadedColor: 'bg-yellow-300', severity: 'warn' @@ -35,13 +49,6 @@ export class ModuleVersionStatusPipe implements PipeTransform { fadedColor: 'bg-green-300', severity: 'success' }; - case ModuleVersionCompactDTO.StatusEnum.FeedbackGiven: - return { - text: 'Feedback given', - normalColor: 'bg-blue-500', - fadedColor: 'bg-blue-300', - severity: 'info' - }; case ModuleVersionCompactDTO.StatusEnum.Rejected: return { text: 'Rejected', diff --git a/Client/src/app/pipes/proposalStatus.pipe.ts b/Client/src/app/pipes/proposalStatus.pipe.ts index 3bbef4cb..09a379c2 100644 --- a/Client/src/app/pipes/proposalStatus.pipe.ts +++ b/Client/src/app/pipes/proposalStatus.pipe.ts @@ -5,10 +5,14 @@ import { Tag } from 'primeng/tag'; export class StatusDisplayPipe implements PipeTransform { transform(status: ProposalViewDTO.StatusEnum): { text: string; severity: Tag['severity'] } { switch (status) { - case 'PENDING_SUBMISSION': - return { text: 'Pending Submission', severity: 'secondary' }; - case 'PENDING_FEEDBACK': - return { text: 'Pending Feedback', severity: 'warn' }; + case 'PENDING_FIRST_SUBMISSION': + return { text: 'Pending first submission', severity: 'secondary' }; + case 'PENDING_COORDINATOR_FEEDBACK': + return { text: 'Pending coordinator feedback', severity: 'warn' }; + case 'PENDING_FULL_SUBMISSION': + return { text: 'Pending full submission', severity: 'secondary' }; + case 'PENDING_FULL_FEEDBACK': + return { text: 'Pending full feedback', severity: 'warn' }; case 'ACCEPTED': return { text: 'Accepted', severity: 'success' }; case 'REQUIRES_REVIEW': @@ -16,7 +20,7 @@ export class StatusDisplayPipe implements PipeTransform { case 'REJECTED': return { text: 'Rejected', severity: 'danger' }; default: - return { text: status, severity: 'secondary' }; + return { text: status ?? '', severity: 'secondary' }; } } } @@ -25,18 +29,22 @@ export class StatusDisplayPipe implements PipeTransform { export class StatusInfoPipeline implements PipeTransform { transform(status: ProposalViewDTO.StatusEnum): string { switch (status) { - case 'PENDING_SUBMISSION': - return 'This module proposal is pending submission. Please fill all module information fields and submit the module proposal. After submission the necessary staff will be notified to review your proposal.'; - case 'PENDING_FEEDBACK': - return 'This module proposal is submitted and waiting for review by the necessary staff. Please wait for their feedback. If you made a mistake you can cancel the submission. If a staff member already gave feedback, you will need to create a new module version for consistency. New module versions copy the content of the previously submitted module version.'; + case 'PENDING_FIRST_SUBMISSION': + return 'This module proposal is pending submission. Please complete step 1 (basic information and degree program assignments) and submit for coordinator feedback.'; + case 'PENDING_COORDINATOR_FEEDBACK': + return 'Submitted for coordinator feedback. Program and area responsibles are reviewing. You can cancel and resubmit if needed.'; + case 'PENDING_FULL_SUBMISSION': + return 'Coordinator feedback was accepted. Complete all steps and submit for full feedback (quality management, program advisor, examination board) when ready.'; + case 'PENDING_FULL_FEEDBACK': + return 'This module proposal is submitted and waiting for review. You can cancel the submission if needed.'; case 'ACCEPTED': return 'This module is approved.'; case 'REQUIRES_REVIEW': - return 'This module proposal requires your review. It was either rejected by a staff member, or you canceled it after a staff member already gave feedback. Please create a new module version and update your proposal by the rejection feedback.'; + return 'This module proposal requires your review. Create a new module version and update by the rejection feedback.'; case 'REJECTED': - return 'This proposal was rejected. You cannot modify this module proposal anymore.'; + return 'This proposal was rejected. Create a new module version to resubmit.'; default: - return status; + return status ?? ''; } } } diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java b/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java index 489c0511..543936c7 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/FeedbackController.java @@ -31,21 +31,21 @@ public FeedbackController(FeedbackService feedbackService, ModuleVersionService } @GetMapping("/for-authenticated-user") - @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") + @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD', 'PROGRAM_COORDINATOR', 'SPECIALIZATION_AREA_RESPONSIBLE')") public ResponseEntity> getFeedbacksForAuthenticatedUser(@CurrentUser User user) { List feedbacks = feedbackService.getAllFeedbacksForUser(user); return ResponseEntity.ok(feedbacks); } @GetMapping("/module-version-of-feedback/{feedbackId}") - @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") - public ResponseEntity getModuleVersionOfFeedback(@PathVariable Long feedbackId) { - ModuleVersionViewDTO dto = feedbackService.getModuleVersionOfFeedback(feedbackId); + @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD', 'PROGRAM_COORDINATOR', 'SPECIALIZATION_AREA_RESPONSIBLE')") + public ResponseEntity getModuleVersionOfFeedback(@CurrentUser User user, @PathVariable Long feedbackId) { + ModuleVersionViewDTO dto = feedbackService.getModuleVersionOfFeedback(feedbackId, user); return ResponseEntity.ok(dto); } @PutMapping("/{feedbackId}/accept") - @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") + @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD', 'PROGRAM_COORDINATOR', 'SPECIALIZATION_AREA_RESPONSIBLE')") public ResponseEntity approveFeedback(@CurrentUser User user, @PathVariable Long feedbackId) { Feedback updatedFeedback = feedbackService.Accept(feedbackId, user); moduleVersionService.updateStatus(updatedFeedback.getModuleVersion().getModuleVersionId()); @@ -53,7 +53,7 @@ public ResponseEntity approveFeedback(@CurrentUser User user, @PathVar } @PutMapping("/{feedbackId}/give-feedback") - @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") + @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD', 'PROGRAM_COORDINATOR', 'SPECIALIZATION_AREA_RESPONSIBLE')") public ResponseEntity giveFeedback(@CurrentUser User user, @PathVariable Long feedbackId, @Valid @RequestBody FeedbackDTO givenFeedback) { Feedback updatedFeedback = feedbackService.GiveFeedback(feedbackId, user, givenFeedback); @@ -62,7 +62,7 @@ public ResponseEntity giveFeedback(@CurrentUser User user, @PathVariab } @PutMapping("/{feedbackId}/reject") - @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") + @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD', 'PROGRAM_COORDINATOR', 'SPECIALIZATION_AREA_RESPONSIBLE')") public ResponseEntity rejectFeedback(@CurrentUser User user, @PathVariable Long feedbackId, @Valid @RequestBody GiveFeedbackDTO request) { Feedback updatedFeedback = feedbackService.RejectFeedback(feedbackId, user, request.getComment()); @@ -72,7 +72,7 @@ public ResponseEntity rejectFeedback(@CurrentUser User user, @PathVari } @GetMapping(value = "/{moduleVersionId}/export-pdf", produces = MediaType.APPLICATION_PDF_VALUE) - @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD')") + @PreAuthorize("hasAnyRole('QUALITY_MANAGEMENT', 'ACADEMIC_PROGRAM_ADVISOR', 'EXAMINATION_BOARD', 'PROGRAM_COORDINATOR', 'SPECIALIZATION_AREA_RESPONSIBLE')") public ResponseEntity exportModuleVersionPdf(@CurrentUser User user, @PathVariable Long moduleVersionId) { diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java index 875c51fd..6695ab3d 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java @@ -39,15 +39,6 @@ public ModuleVersionController(ModuleVersionService moduleVersionService, this.llmGenerationService = llmGenerationService; } - @GetMapping("/{moduleVersionId}") - @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity getModuleVersionUpdateDtoFromId( - @CurrentUser User user, @PathVariable Long moduleVersionId) { - ModuleVersionViewDTO dto = moduleVersionService.getModuleVersionUpdateDtoFromId(moduleVersionId, - user.getUserId()); - return ResponseEntity.ok(dto); - } - @PutMapping("/{moduleVersionId}") @PreAuthorize("hasAnyRole('PROFESSOR')") public ResponseEntity updateModuleVersion(@CurrentUser User user, @@ -57,11 +48,11 @@ public ResponseEntity updateModuleVersion(@CurrentUser Use return ResponseEntity.ok(updatedModuleVersion); } - @GetMapping("/view/{moduleVersionId}") + @GetMapping("/{moduleVersionId}") @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity getModuleVersionViewDto(@CurrentUser User user, + public ResponseEntity getModuleVersion(@CurrentUser User user, @PathVariable Long moduleVersionId) { - ModuleVersionViewDTO dto = moduleVersionService.getModuleVersionViewDto(moduleVersionId, user.getUserId()); + ModuleVersionViewDTO dto = moduleVersionService.getModuleVersion(moduleVersionId, user.getUserId()); return ResponseEntity.ok(dto); } diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java index cb085f4a..57c06a95 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ProposalController.java @@ -7,11 +7,9 @@ import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; import java.util.List; @@ -36,29 +34,22 @@ public ResponseEntity createProposal(@CurrentUser User user, return ResponseEntity.ok(proposalView); } - @PostMapping(value = "/submit/{proposalId}") + @PostMapping(value = "/request-coordinators-feedback/{proposalId}") @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity submitProposal(@CurrentUser User user, + public ResponseEntity requestCoordinatorsFeedback(@CurrentUser User user, @PathVariable Long proposalId) { - var proposalDto = proposalService.submitProposal(proposalId, user.getUserId()); + var proposalDto = proposalService.requestCoordinatorsFeedback(proposalId, user.getUserId()); return ResponseEntity.ok(proposalDto); } - @PostMapping(value = "/cancel-submission/{proposalId}") - public ResponseEntity cancelSubmission(@CurrentUser User user, + @PostMapping(value = "/request-full-feedback/{proposalId}") + @PreAuthorize("hasAnyRole('PROFESSOR')") + public ResponseEntity requestFullFeedback(@CurrentUser User user, @PathVariable Long proposalId) { - var proposalDto = proposalService.cancelSubmission(proposalId, user.getUserId()); + var proposalDto = proposalService.requestFullFeedback(proposalId, user.getUserId()); return ResponseEntity.ok(proposalDto); } - @PostMapping("/add-module-version") - @PreAuthorize("hasAnyRole('PROFESSOR')") - public ResponseEntity addModuleVersion(@CurrentUser User user, - @Valid @RequestBody AddModuleVersionDTO request) { - ProposalViewDTO proposal = proposalService.addModuleVersion(user.getUserId(), request); - return ResponseEntity.ok(proposal); - } - @GetMapping("/{id}/view") @PreAuthorize("hasAnyRole('PROFESSOR')") public ResponseEntity getProposalView(@CurrentUser User user, @PathVariable Long id) { @@ -73,14 +64,10 @@ public ResponseEntity> getCompactProposalsFromUser(@Cu return ResponseEntity.ok(proposals); } - @DeleteMapping(value = "/{proposalId}", produces = MediaType.TEXT_PLAIN_VALUE) + @DeleteMapping(value = "/{proposalId}") @PreAuthorize("hasAnyRole('PROFESSOR')") public ResponseEntity deleteProposal(@CurrentUser User user, @PathVariable Long proposalId) { - try { - proposalService.deleteProposalById(proposalId, user.getUserId()); - return ResponseEntity.ok("Proposal deleted successfully."); - } catch (ResponseStatusException e) { - return ResponseEntity.badRequest().body(e.getReason()); - } + proposalService.deleteProposalById(proposalId, user.getUserId()); + return ResponseEntity.ok("Proposal deleted successfully."); } } diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java index 9a9f081c..f25df9af 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java @@ -10,13 +10,16 @@ @Data public class ModuleVersionViewFeedbackDTO { - @NotNull private Long feedbackId; + @NotNull + private Long feedbackId; private String feedbackFromFirstName; private String feedbackFromLastName; private String rejectionComment; private UserRole feedbackRole; + private String requestedFromUserName; private FeedbackStatus feedbackStatus; private LocalDateTime submissionDate; + private Long degreeProgramSpecializationId; private String titleFeedback; private String levelFeedback; @@ -47,8 +50,17 @@ public static ModuleVersionViewFeedbackDTO from(Feedback f) { } dto.setRejectionComment(f.getComment()); dto.setFeedbackRole(f.getRequiredRole()); + if (f.getDegreeProgramSpecialization() != null + && f.getDegreeProgramSpecialization().getResponsibleUser() != null) { + var resp = f.getDegreeProgramSpecialization().getResponsibleUser(); + dto.setRequestedFromUserName(resp.getFirstName() + " " + resp.getLastName() + " (" + + f.getDegreeProgramSpecialization().getName() + ")"); + } dto.setFeedbackStatus(f.getStatus()); dto.setSubmissionDate(f.getSubmissionDate()); + if (f.getDegreeProgramSpecialization() != null) { + dto.setDegreeProgramSpecializationId(f.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); + } dto.setTitleFeedback(f.getTitleFeedback()); dto.setLevelFeedback(f.getLevelFeedback()); dto.setLanguageFeedback(f.getLanguageFeedback()); diff --git a/Server/src/main/java/modulemanagement/ls1/enums/FeedbackStatus.java b/Server/src/main/java/modulemanagement/ls1/enums/FeedbackStatus.java index 866d67e8..4f36710c 100644 --- a/Server/src/main/java/modulemanagement/ls1/enums/FeedbackStatus.java +++ b/Server/src/main/java/modulemanagement/ls1/enums/FeedbackStatus.java @@ -1,11 +1,8 @@ package modulemanagement.ls1.enums; public enum FeedbackStatus { - PENDING_SUBMISSION, // Corresponding ModuleVersion only created, but nothing to review yet. - PENDING_FEEDBACK, // Corresponding ModuleVersion is submitted, waiting for this feedback. - APPROVED, // Reviewer accepts this ModuleVersion. + PENDING_FEEDBACK, // Corresponding ModuleVersion is submitted, waiting for this feedback. + APPROVED, // Reviewer accepts this ModuleVersion. FEEDBACK_GIVEN, - REJECTED, // Reviewer rejects this ModuleVersion. - OBSOLETE, // A new ModuleVersion for this proposal was submitted. - CANCELLED // Review of corresponding ModuleVersion was cancelled. + REJECTED, // Reviewer rejects this ModuleVersion. } diff --git a/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java b/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java index e58013b2..b9504087 100644 --- a/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java +++ b/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java @@ -1,11 +1,21 @@ package modulemanagement.ls1.enums; public enum ModuleVersionStatus { - PENDING_SUBMISSION, // Not submitted yet. - PENDING_FEEDBACK, // Submitted, and review is requested. - ACCEPTED, // All feedbacks accept. - FEEDBACK_GIVEN, - REJECTED, // One feedback rejects. - OBSOLETE, // New ModuleVersion was proposed. - CANCELLED, // Proposal of this version was cancelled. + /** Not yet submitted for coordinator feedback (first submission). */ + PENDING_FIRST_SUBMISSION, + /** + * Submitted for coordinator feedback; waiting for program/area coordinators. + */ + PENDING_COORDINATOR_FEEDBACK, + /** + * Coordinator feedback accepted; professor has not yet submitted for full + * feedback. + */ + PENDING_FULL_SUBMISSION, + /** Submitted for full feedback; waiting for QM, advisor, examination board. */ + PENDING_FULL_FEEDBACK, + ACCEPTED, + REQUIRES_REVIEW, + REJECTED, + CANCELLED, } diff --git a/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java b/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java index 7eb93e88..fb5ac52a 100644 --- a/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java +++ b/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java @@ -1,9 +1,23 @@ package modulemanagement.ls1.enums; public enum ProposalStatus { - PENDING_SUBMISSION, - PENDING_FEEDBACK, + /** + * Professor has not yet submitted for coordinator feedback (first submission). + */ + PENDING_FIRST_SUBMISSION, + /** + * Submitted for coordinator feedback; waiting for program/area coordinators. + */ + PENDING_COORDINATOR_FEEDBACK, + /** + * Coordinator feedback accepted; professor has not yet submitted for full + * feedback. + */ + PENDING_FULL_SUBMISSION, + /** Submitted for full feedback; waiting for QM, advisor, examination board. */ + PENDING_FULL_FEEDBACK, ACCEPTED, REQUIRES_REVIEW, REJECTED, + CANCELLED, } diff --git a/Server/src/main/java/modulemanagement/ls1/enums/UserRole.java b/Server/src/main/java/modulemanagement/ls1/enums/UserRole.java index 81dca201..7b878e7b 100644 --- a/Server/src/main/java/modulemanagement/ls1/enums/UserRole.java +++ b/Server/src/main/java/modulemanagement/ls1/enums/UserRole.java @@ -5,5 +5,9 @@ public enum UserRole { QUALITY_MANAGEMENT, ACADEMIC_PROGRAM_ADVISOR, EXAMINATION_BOARD, - PROFESSOR + PROFESSOR, + /** User is responsible for at least one degree program; can receive and respond to feedback requests for those. */ + PROGRAM_COORDINATOR, + /** User is responsible for at least one area of specialization; can receive and respond to feedback requests for those. */ + SPECIALIZATION_AREA_RESPONSIBLE } diff --git a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java index 6f83a7e6..41adbdad 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java +++ b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java @@ -22,7 +22,7 @@ public class DegreeProgram { private String name; @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "responsible_user_id", nullable = false) + @JoinColumn(name = "responsible_user_id") private User responsibleUser; @ManyToMany(fetch = FetchType.LAZY) diff --git a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java index 646e9aed..6b5a6d0b 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java +++ b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgramSpecialization.java @@ -22,7 +22,7 @@ public class DegreeProgramSpecialization { private String name; @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "responsible_user_id", nullable = false) + @JoinColumn(name = "responsible_user_id") private User responsibleUser; @ManyToMany(mappedBy = "degreeProgramSpecializations", fetch = FetchType.LAZY) diff --git a/Server/src/main/java/modulemanagement/ls1/models/Feedback.java b/Server/src/main/java/modulemanagement/ls1/models/Feedback.java index b3aa0191..3d01d696 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/Feedback.java +++ b/Server/src/main/java/modulemanagement/ls1/models/Feedback.java @@ -24,6 +24,18 @@ public class Feedback { @JoinColumn(name = "feedback_from") private User feedbackFrom; + @Column(name = "invalidated") + private boolean invalidated; + + /** + * When set, this feedback is for whoever is currently responsible for this + * specialization (position-based). + */ + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "degree_program_specialization_id") + @JsonIgnore + private DegreeProgramSpecialization degreeProgramSpecialization; + @Column(name = "comment") private String Comment; @@ -33,7 +45,8 @@ public class Feedback { @Enumerated(EnumType.STRING) @Column(name = "feedback_status") - @NotNull private FeedbackStatus status; + @NotNull + private FeedbackStatus status; @Column(name = "submission_date") private LocalDateTime submissionDate; @@ -206,23 +219,23 @@ public void insert(FeedbackDTO dto) { public boolean isAllFeedbackPositive() { return this.titleAccepted - && this.levelAccepted - && this.languageAccepted - && this.frequencyAccepted - && this.creditsAccepted - && this.durationAccepted - && this.hoursTotalAccepted - && this.hoursSelfStudyAccepted - && this.hoursPresenceAccepted - && this.examinationAchievementsAccepted - && this.repetitionAccepted - && this.recommendedPrerequisitesAccepted - && this.contentAccepted - && this.learningOutcomesAccepted - && this.teachingMethodsAccepted - && this.mediaAccepted - && this.literatureAccepted - && this.responsiblesAccepted - && this.lvSwsLecturerAccepted; + && this.levelAccepted + && this.languageAccepted + && this.frequencyAccepted + && this.creditsAccepted + && this.durationAccepted + && this.hoursTotalAccepted + && this.hoursSelfStudyAccepted + && this.hoursPresenceAccepted + && this.examinationAchievementsAccepted + && this.repetitionAccepted + && this.recommendedPrerequisitesAccepted + && this.contentAccepted + && this.learningOutcomesAccepted + && this.teachingMethodsAccepted + && this.mediaAccepted + && this.literatureAccepted + && this.responsiblesAccepted + && this.lvSwsLecturerAccepted; } } diff --git a/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java b/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java index 9e1a7f3b..07bb03bb 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java +++ b/Server/src/main/java/modulemanagement/ls1/models/ModuleVersion.java @@ -143,6 +143,26 @@ public class ModuleVersion { @OneToMany(mappedBy = "moduleVersion", cascade = CascadeType.ALL, orphanRemoval = true) private List degreeProgramAssignments = new ArrayList<>(); + @JsonIgnore + public boolean isFirstStepComplete() { + if (StringUtils.isEmpty(titleEng) || StringUtils.isEmpty(titleDe)) + return false; + if (credits == null) + return false; + if (StringUtils.isEmpty(frequencyEng)) + return false; + if (hoursLecture == null || hoursExercise == null || hoursPractical == null || hoursSeminar == null) + return false; + if (StringUtils.isEmpty(firstSemesterAvailable) || StringUtils.isEmpty(successorModuleName)) + return false; + if (languageEng == null) + return false; + if (degreeProgramAssignments == null || degreeProgramAssignments.isEmpty()) + return false; + return degreeProgramAssignments.stream() + .allMatch(a -> a.getDegreeProgramSpecialization() != null && a.getDegreeProgram() != null); + } + @JsonIgnore public boolean isCompleted() { return !StringUtils.isEmpty(titleEng) @@ -166,7 +186,4 @@ public boolean isCompleted() { && !StringUtils.isEmpty(lvSwsLecturerEng); } - public boolean isFeedbackGiven() { - return this.getStatus() == ModuleVersionStatus.FEEDBACK_GIVEN; - } } diff --git a/Server/src/main/java/modulemanagement/ls1/models/Proposal.java b/Server/src/main/java/modulemanagement/ls1/models/Proposal.java index a45f3156..607e5961 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/Proposal.java +++ b/Server/src/main/java/modulemanagement/ls1/models/Proposal.java @@ -3,13 +3,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.NotNull; import modulemanagement.ls1.dtos.ModuleVersionViewFeedbackDTO; -import modulemanagement.ls1.enums.ModuleVersionStatus; import modulemanagement.ls1.enums.ProposalStatus; import jakarta.persistence.*; import lombok.Data; import lombok.NoArgsConstructor; -import modulemanagement.ls1.services.ProposalService; - import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; @@ -40,11 +37,6 @@ public class Proposal { @OneToMany(mappedBy = "proposal", cascade = CascadeType.ALL, orphanRemoval = true) private List moduleVersions = new ArrayList<>(); - @JsonIgnore - public void addModuleVersion(ModuleVersion moduleVersion) { - moduleVersions.add(moduleVersion); - } - @JsonIgnore public ModuleVersion getLatestModuleVersionWithContent() { return moduleVersions.stream().max(Comparator.comparing(ModuleVersion::getVersion)).orElse(null); @@ -62,7 +54,7 @@ public void addNewModuleVersion() { newMv.setProposal(this); newMv.setVersion(latestMv.getVersion() + 1); newMv.setCreationDate(LocalDateTime.now()); - newMv.setStatus(ModuleVersionStatus.PENDING_SUBMISSION); + newMv.setStatus(latestMv.getStatus()); newMv.setBulletPoints(latestMv.getBulletPoints()); newMv.setTitleEng(latestMv.getTitleEng()); newMv.setTitleDe(latestMv.getTitleDe()); @@ -100,27 +92,22 @@ public void addNewModuleVersion() { newMv.getDegreeProgramAssignments().add(newAssig); } } - List requiredFeedbacks = new ArrayList<>(); - ProposalService.createNewFeedbacks(newMv, requiredFeedbacks); - newMv.setRequiredFeedbacks(requiredFeedbacks); - addModuleVersion(newMv); - this.setStatus(ProposalStatus.PENDING_SUBMISSION); + newMv.setRequiredFeedbacks(new ArrayList<>()); + moduleVersions.add(newMv); } public List getPreviousModuleVersionFeedback() { List mvs = this.getModuleVersions(); Collections.reverse(mvs); for (ModuleVersion mv : mvs) { - if (mv.isFeedbackGiven()) { - List previousFeedbacks = new ArrayList<>(); - List feedbacks = mv.getRequiredFeedbacks(); - for (Feedback feedback : feedbacks) { - if (feedback.isFeedbackGiven()) { - previousFeedbacks.add(ModuleVersionViewFeedbackDTO.from(feedback)); - } + List previousFeedbacks = new ArrayList<>(); + List feedbacks = mv.getRequiredFeedbacks(); + for (Feedback feedback : feedbacks) { + if (feedback.isFeedbackGiven()) { + previousFeedbacks.add(ModuleVersionViewFeedbackDTO.from(feedback)); } - return previousFeedbacks; } + return previousFeedbacks; } return Collections.emptyList(); } diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java index 6f16a27d..b68d4146 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java @@ -8,10 +8,15 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; @Repository public interface DegreeProgramRepository extends JpaRepository { + boolean existsByResponsibleUser_UserId(UUID userId); + + List findByResponsibleUser_UserId(UUID userId); + @EntityGraph(attributePaths = { "responsibleUser" }) @Query("SELECT p FROM DegreeProgram p") List findAllWithResponsibleUser(); diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java index 92d3170d..0de01c33 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramSpecializationRepository.java @@ -9,10 +9,15 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; @Repository public interface DegreeProgramSpecializationRepository extends JpaRepository { + boolean existsByResponsibleUser_UserId(UUID userId); + + List findByResponsibleUser_UserId(UUID userId); + @EntityGraph(attributePaths = { "responsibleUser" }) @Query("SELECT s FROM DegreeProgramSpecialization s") List findAllWithResponsibleUser(); diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java index 65b70cc4..1b6909f9 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java @@ -8,8 +8,12 @@ import java.util.Collection; import java.util.List; +import java.util.UUID; @Repository public interface FeedbackRepository extends JpaRepository { List findByRequiredRoleInAndStatus(Collection requiredRoles, FeedbackStatus status); + + /** Feedbacks for specializations that this user is currently responsible for (position-based). */ + List findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatus(UUID userId, FeedbackStatus status); } diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/ProposalRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/ProposalRepository.java index a3be1e58..088e03ae 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/ProposalRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/ProposalRepository.java @@ -4,7 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.UUID; + @Repository public interface ProposalRepository extends JpaRepository { + + List findByCreatedBy_UserId(UUID userId); } diff --git a/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java b/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java index 1388666f..43d4747b 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java @@ -3,22 +3,27 @@ import modulemanagement.ls1.dtos.PageResponseDTO; import modulemanagement.ls1.dtos.UpdateUserRoleDTO; import modulemanagement.ls1.dtos.UserDTO; +import modulemanagement.ls1.enums.UserRole; import modulemanagement.ls1.models.User; import modulemanagement.ls1.repositories.UserRepository; import modulemanagement.ls1.shared.ResourceNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.UUID; @Service public class AdminUserService { private final UserRepository userRepository; + private final ResponsibleUserRoleService responsibleUserRoleService; - public AdminUserService(UserRepository userRepository) { + public AdminUserService(UserRepository userRepository, ResponsibleUserRoleService responsibleUserRoleService) { this.userRepository = userRepository; + this.responsibleUserRoleService = responsibleUserRoleService; } public PageResponseDTO getUsersPage(Pageable pageable, String search) { @@ -28,9 +33,18 @@ public PageResponseDTO getUsersPage(Pageable pageable, String search) { return PageResponseDTO.from(page.map(UserDTO::fromUser)); } + @Transactional public UserDTO updateUserRole(UUID userId, UpdateUserRoleDTO dto) { User user = userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("User not found: " + userId)); + List currentRoles = user.getRoles() != null ? user.getRoles() : List.of(); + List newRoles = dto.getRoles() != null ? dto.getRoles() : List.of(); + if (currentRoles.contains(UserRole.PROGRAM_COORDINATOR) && !newRoles.contains(UserRole.PROGRAM_COORDINATOR)) { + responsibleUserRoleService.unassignFromAllPrograms(userId); + } + if (currentRoles.contains(UserRole.SPECIALIZATION_AREA_RESPONSIBLE) && !newRoles.contains(UserRole.SPECIALIZATION_AREA_RESPONSIBLE)) { + responsibleUserRoleService.unassignFromAllSpecializations(userId); + } user.setRoles(dto.getRoles()); user = userRepository.save(user); return UserDTO.fromUser(user); diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java index ad5098b3..16daf7dd 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -18,13 +19,16 @@ public class DegreeProgramService { private final DegreeProgramRepository degreeProgramRepository; private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; private final UserRepository userRepository; + private final ResponsibleUserRoleService responsibleUserRoleService; public DegreeProgramService(DegreeProgramRepository degreeProgramRepository, DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, - UserRepository userRepository) { + UserRepository userRepository, + ResponsibleUserRoleService responsibleUserRoleService) { this.degreeProgramRepository = degreeProgramRepository; this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; this.userRepository = userRepository; + this.responsibleUserRoleService = responsibleUserRoleService; } public List getAllDegreePrograms() { @@ -50,6 +54,7 @@ public DegreeProgramDTO createDegreeProgram(CreateDegreeProgramDTO dto) { program.setName(dto.getName()); program.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); program = degreeProgramRepository.save(program); + responsibleUserRoleService.ensureProgramCoordinatorRole(dto.getResponsibleUserId()); program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(program.getDegreeProgramId()) .orElse(program); return DegreeProgramDTO.fromDegreeProgram(program); @@ -60,18 +65,29 @@ public DegreeProgramDTO updateDegreeProgram(Long id, UpdateDegreeProgramDTO dto) .orElseThrow(() -> new ResourceNotFoundException("Degree program not found: " + id)); if (dto.getName() != null) program.setName(dto.getName()); - if (dto.getResponsibleUserId() != null) + if (dto.getResponsibleUserId() != null) { + UUID previousUserId = program.getResponsibleUser() != null ? program.getResponsibleUser().getUserId() : null; program.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); - program = degreeProgramRepository.save(program); + program = degreeProgramRepository.save(program); + responsibleUserRoleService.ensureProgramCoordinatorRole(dto.getResponsibleUserId()); + if (previousUserId != null && !previousUserId.equals(dto.getResponsibleUserId())) + responsibleUserRoleService.removeProgramCoordinatorRoleIfNotResponsible(previousUserId); + } else { + program = degreeProgramRepository.save(program); + } program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id).orElse(program); return DegreeProgramDTO.fromDegreeProgram(program); } public void deleteDegreeProgram(Long id) { - if (!degreeProgramRepository.existsById(id)) { + DegreeProgram program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id).orElse(null); + if (program == null) { throw new ResourceNotFoundException("Degree program not found: " + id); } + UUID responsibleUserId = program.getResponsibleUser() != null ? program.getResponsibleUser().getUserId() : null; degreeProgramRepository.deleteById(id); + if (responsibleUserId != null) + responsibleUserRoleService.removeProgramCoordinatorRoleIfNotResponsible(responsibleUserId); } public DegreeProgramDTO addSpecializationsToDegreeProgram(Long degreeProgramId, diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java index 97a6a3a4..99e66588 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -15,12 +16,15 @@ public class DegreeProgramSpecializationService { private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; private final UserRepository userRepository; + private final ResponsibleUserRoleService responsibleUserRoleService; public DegreeProgramSpecializationService( DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, - UserRepository userRepository) { + UserRepository userRepository, + ResponsibleUserRoleService responsibleUserRoleService) { this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; this.userRepository = userRepository; + this.responsibleUserRoleService = responsibleUserRoleService; } public List getAllDegreeProgramSpecializations() { @@ -34,6 +38,7 @@ public DegreeProgramSpecializationDTO createDegreeProgramSpecialization(CreateDe entity.setName(dto.getName()); entity.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); entity = degreeProgramSpecializationRepository.save(entity); + responsibleUserRoleService.ensureSpecializationAreaResponsibleRole(dto.getResponsibleUserId()); return DegreeProgramSpecializationDTO.fromEntity( degreeProgramSpecializationRepository .findByIdWithResponsibleUser(entity.getDegreeProgramSpecializationId()).orElse(entity)); @@ -45,16 +50,27 @@ public DegreeProgramSpecializationDTO updateDegreeProgramSpecialization(Long id, .orElseThrow(() -> new ResourceNotFoundException("Degree program specialization not found: " + id)); if (dto.getName() != null) entity.setName(dto.getName()); - if (dto.getResponsibleUserId() != null) + if (dto.getResponsibleUserId() != null) { + UUID previousUserId = entity.getResponsibleUser() != null ? entity.getResponsibleUser().getUserId() : null; entity.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); - entity = degreeProgramSpecializationRepository.save(entity); + entity = degreeProgramSpecializationRepository.save(entity); + responsibleUserRoleService.ensureSpecializationAreaResponsibleRole(dto.getResponsibleUserId()); + if (previousUserId != null && !previousUserId.equals(dto.getResponsibleUserId())) + responsibleUserRoleService.removeSpecializationAreaResponsibleRoleIfNotResponsible(previousUserId); + } else { + entity = degreeProgramSpecializationRepository.save(entity); + } return DegreeProgramSpecializationDTO.fromEntity(entity); } public void deleteDegreeProgramSpecialization(Long id) { - if (!degreeProgramSpecializationRepository.existsById(id)) { + DegreeProgramSpecialization entity = degreeProgramSpecializationRepository.findByIdWithResponsibleUser(id).orElse(null); + if (entity == null) { throw new ResourceNotFoundException("Degree program specialization not found: " + id); } + UUID responsibleUserId = entity.getResponsibleUser() != null ? entity.getResponsibleUser().getUserId() : null; degreeProgramSpecializationRepository.deleteById(id); + if (responsibleUserId != null) + responsibleUserRoleService.removeSpecializationAreaResponsibleRoleIfNotResponsible(responsibleUserId); } } diff --git a/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java b/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java index 4bc2fa95..80e70625 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java @@ -11,13 +11,18 @@ import modulemanagement.ls1.shared.ResourceNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; +import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; @Service +@Validated public class FeedbackService { private final FeedbackRepository feedbackRepository; @@ -27,7 +32,7 @@ public FeedbackService(FeedbackRepository feedbackRepository) { public Feedback Accept(Long feedbackId, User user) { Feedback feedback = getPendingFeedback(feedbackId); - if (user.getRoles() == null || !user.getRoles().contains(feedback.getRequiredRole())) + if (!canUserRespondToFeedback(feedback, user)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "You do not have permission to accept this feedback"); feedback.setFeedbackFrom(user); @@ -39,7 +44,7 @@ public Feedback Accept(Long feedbackId, User user) { public Feedback GiveFeedback(Long feedbackId, User user, FeedbackDTO givenFeedback) { Feedback feedback = getPendingFeedback(feedbackId); - if (user.getRoles() == null || !user.getRoles().contains(feedback.getRequiredRole())) + if (!canUserRespondToFeedback(feedback, user)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "You do not have permission to accept this feedback"); feedback.setFeedbackFrom(user); @@ -53,7 +58,7 @@ public Feedback GiveFeedback(Long feedbackId, User user, FeedbackDTO givenFeedba public Feedback RejectFeedback(Long feedbackId, User user, @NotBlank String comment) { Feedback feedback = getPendingFeedback(feedbackId); - if (user.getRoles() == null || !user.getRoles().contains(feedback.getRequiredRole())) + if (!canUserRespondToFeedback(feedback, user)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "You do not have permission to accept this feedback"); feedback.setFeedbackFrom(user); @@ -65,13 +70,32 @@ public Feedback RejectFeedback(Long feedbackId, User user, @NotBlank String comm } public List getAllFeedbacksForUser(User user) { - return feedbackRepository.findByRequiredRoleInAndStatus(user.getRoles(), FeedbackStatus.PENDING_FEEDBACK) - .stream() + List roleBased = user.getRoles() != null && !user.getRoles().isEmpty() + ? feedbackRepository.findByRequiredRoleInAndStatus(user.getRoles(), FeedbackStatus.PENDING_FEEDBACK) + : Collections.emptyList(); + List bySpecialization = feedbackRepository + .findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatus(user.getUserId(), + FeedbackStatus.PENDING_FEEDBACK); + return Stream.of(roleBased.stream(), bySpecialization.stream()) + .flatMap(s -> s) + .distinct() .sorted(Comparator.comparing(Feedback::getFeedbackId)) .map(FeedbackListItemDto::fromFeedback) .toList(); } + private boolean canUserRespondToFeedback(Feedback feedback, User user) { + if (user == null) + return false; + if (feedback.getDegreeProgramSpecialization() != null + && feedback.getDegreeProgramSpecialization().getResponsibleUser() != null) { + return Objects.equals(user.getUserId(), + feedback.getDegreeProgramSpecialization().getResponsibleUser().getUserId()); + } + return user.getRoles() != null && feedback.getRequiredRole() != null + && user.getRoles().contains(feedback.getRequiredRole()); + } + private Feedback getPendingFeedback(Long feedbackId) { Feedback feedback = feedbackRepository.findById(feedbackId) .orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); @@ -81,9 +105,12 @@ private Feedback getPendingFeedback(Long feedbackId) { return feedback; } - public ModuleVersionViewDTO getModuleVersionOfFeedback(Long feedbackId) { + public ModuleVersionViewDTO getModuleVersionOfFeedback(Long feedbackId, User user) { Feedback feedback = feedbackRepository.findById(feedbackId) .orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); + if (!canUserRespondToFeedback(feedback, user)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You do not have access to this feedback."); + } return ModuleVersionViewDTO.from(feedback.getModuleVersion()); } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index b6ea2b4a..865fc02f 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -30,6 +30,8 @@ import org.springframework.web.server.ResponseStatusException; import jakarta.persistence.EntityManager; + +import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -58,16 +60,6 @@ public ModuleVersionService(ModuleVersionRepository moduleVersionRepository, this.entityManager = entityManager; } - private boolean hasAccessPermission(Proposal proposal, User user) { - if (proposal.getCreatedBy().getUserId().equals(user.getUserId())) { - return true; - } - - return user.getRoles() != null && (user.getRoles().contains(UserRole.QUALITY_MANAGEMENT) || - user.getRoles().contains(UserRole.EXAMINATION_BOARD) || - user.getRoles().contains(UserRole.ACADEMIC_PROGRAM_ADVISOR)); - } - @Transactional public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long moduleVersionId, ModuleVersionUpdateRequestDTO request) { @@ -79,11 +71,91 @@ public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long mod if (!mv.getVersion().equals(mv.getProposal().getLatestModuleVersionWithContent().getVersion())) { throw new OptimisticLockingFailureException("Cannot update an outdated ModuleVersion"); } - - if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_SUBMISSION)) { - throw new IllegalStateException("Cannot update a submitted ModuleVersion"); + if (!mv.getRequiredFeedbacks().isEmpty()) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "Cannot update ModuleVersion with feedback."); } + applyUpdateRequest(mv, request); + mv = moduleVersionRepository.save(mv); + return ModuleVersionViewDTO.from(mv); + } + + /** + * True if any step-1 field (basic info + assignments) differs between request + * and mv. + */ + // private boolean isStep1DataChanged(ModuleVersionUpdateRequestDTO request, + // ModuleVersion mv) { + // if (!Objects.equals(nullToEmpty(request.getTitleEng()), + // nullToEmpty(mv.getTitleEng()))) + // return true; + // if (!Objects.equals(nullToEmpty(request.getTitleDe()), + // nullToEmpty(mv.getTitleDe()))) + // return true; + // if (!Objects.equals(request.getCredits(), mv.getCredits())) + // return true; + // if (!Objects.equals(nullToEmpty(request.getFrequencyEng()), + // nullToEmpty(mv.getFrequencyEng()))) + // return true; + // if (!Objects.equals(request.getHoursLecture(), mv.getHoursLecture())) + // return true; + // if (!Objects.equals(request.getHoursExercise(), mv.getHoursExercise())) + // return true; + // if (!Objects.equals(request.getHoursPractical(), mv.getHoursPractical())) + // return true; + // if (!Objects.equals(request.getHoursSeminar(), mv.getHoursSeminar())) + // return true; + // if (!Objects.equals(nullToEmpty(request.getFirstSemesterAvailable()), + // nullToEmpty(mv.getFirstSemesterAvailable()))) + // return true; + // if (!Objects.equals(nullToEmpty(request.getSuccessorModuleName()), + // nullToEmpty(mv.getSuccessorModuleName()))) + // return true; + // if (!Objects.equals(request.getLanguageEng(), mv.getLanguageEng())) + // return true; + // if (request.getDegreeProgramAssignments() != null + // && !assignmentSetEquals(request.getDegreeProgramAssignments(), + // mv.getDegreeProgramAssignments())) { + // return true; + // } + // return false; + // } + + // private static String nullToEmpty(String s) { + // return s == null ? "" : s.trim(); + // } + + // private static boolean + // assignmentSetEquals(List requestList, + // List mvList) { + // Set requestSet = new HashSet<>(); + // if (requestList != null) { + // for (ModuleDegreeProgramAssignmentDTO a : requestList) { + // if (a != null && a.getDegreeProgramId() != null && + // a.getDegreeProgramSpecializationId() != null) { + // requestSet.add(a.getDegreeProgramId() + "," + + // a.getDegreeProgramSpecializationId()); + // } + // } + // } + // Set mvSet = new HashSet<>(); + // if (mvList != null) { + // for (ModuleVersionDegreeProgramAssignment a : mvList) { + // if (a.getDegreeProgram() != null && a.getDegreeProgramSpecialization() != + // null) { + // mvSet.add(a.getDegreeProgram().getDegreeProgramId() + "," + // + a.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); + // } + // } + // } + // return requestSet.equals(mvSet); + // } + + /** + * Applies request content and degree program assignments to the given module + * version. + */ + private void applyUpdateRequest(ModuleVersion mv, ModuleVersionUpdateRequestDTO request) { mv.setBulletPoints(request.getBulletPoints()); mv.setTitleEng(request.getTitleEng()); mv.setTitleDe(request.getTitleDe()); @@ -127,9 +199,10 @@ public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long mod "A module cannot be assigned to the same degree program more than once."); } } + assignmentRepository.deleteByModuleVersion_ModuleVersionId(mv.getModuleVersionId()); - entityManager.flush(); mv.getDegreeProgramAssignments().clear(); + entityManager.flush(); for (ModuleDegreeProgramAssignmentDTO item : request.getDegreeProgramAssignments()) { DegreeProgram program = degreeProgramRepository .findWithSpecializationsByDegreeProgramId(item.getDegreeProgramId()) @@ -149,9 +222,6 @@ public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long mod mv.getDegreeProgramAssignments().add(assignment); } } - - mv = moduleVersionRepository.save(mv); - return ModuleVersionViewDTO.from(mv); } public void updateStatus(Long moduleVersionId) { @@ -165,10 +235,22 @@ public void updateStatus(Long moduleVersionId) { return; } + List allFeedbacks = mv.getRequiredFeedbacks() != null ? mv.getRequiredFeedbacks() : new ArrayList<>(); + // Coordinator feedbacks for current assignments + all role-based feedbacks (we + // only have one active set of pending at a time). + List coordinatorFeedbacks = allFeedbacks.stream() + .filter(f -> f.getDegreeProgramSpecialization() != null) + .toList(); + List roleBased = allFeedbacks.stream() + .filter(f -> f.getRequiredRole() != null) + .toList(); + List feedbacksToEvaluate = new ArrayList<>(coordinatorFeedbacks); + feedbacksToEvaluate.addAll(roleBased); + boolean allFeedbackPositive = true; boolean oneFeedbackNegative = false; boolean oneFeedbackRejected = false; - for (Feedback feedback : mv.getRequiredFeedbacks()) { + for (Feedback feedback : feedbacksToEvaluate) { if (!feedback.getStatus().equals(FeedbackStatus.APPROVED)) { allFeedbackPositive = false; } @@ -181,12 +263,17 @@ public void updateStatus(Long moduleVersionId) { } if (allFeedbackPositive) { - mv.setStatus(ModuleVersionStatus.ACCEPTED); - p.setStatus(ProposalStatus.ACCEPTED); - mv.getProposal().setStatus(ProposalStatus.ACCEPTED); + boolean anyRoleBased = !roleBased.isEmpty(); + if (anyRoleBased) { + mv.setStatus(ModuleVersionStatus.ACCEPTED); + p.setStatus(ProposalStatus.ACCEPTED); + } else { + mv.setStatus(ModuleVersionStatus.PENDING_FULL_SUBMISSION); + p.setStatus(ProposalStatus.PENDING_FULL_SUBMISSION); + } } if (oneFeedbackNegative) { - mv.setStatus(ModuleVersionStatus.FEEDBACK_GIVEN); + mv.setStatus(ModuleVersionStatus.REQUIRES_REVIEW); p.setStatus(ProposalStatus.REQUIRES_REVIEW); } if (oneFeedbackRejected) { @@ -197,22 +284,7 @@ public void updateStatus(Long moduleVersionId) { moduleVersionRepository.save(mv); } - public ModuleVersionViewDTO getModuleVersionUpdateDtoFromId(Long moduleVersionId, UUID userId) { - var mv = moduleVersionRepository.findById(moduleVersionId) - .orElseThrow(() -> new ResourceNotFoundException("Module Version not found")); - Proposal p = mv.getProposal(); - if (!p.getCreatedBy().getUserId().equals(userId)) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); - } - - if (p.getModuleVersions() == null || p.getModuleVersions().isEmpty()) { - throw new IllegalStateException("Proposal must have at least one ModuleVersion."); - } - - return ModuleVersionViewDTO.from(mv); - } - - public ModuleVersionViewDTO getModuleVersionViewDto(Long moduleVersionId, UUID userId) { + public ModuleVersionViewDTO getModuleVersion(Long moduleVersionId, UUID userId) { ModuleVersion mv = moduleVersionRepository.findById(moduleVersionId) .orElseThrow(() -> new ResourceNotFoundException("Could not find a module version with this ID.")); Proposal p = mv.getProposal(); @@ -260,4 +332,16 @@ public Resource generateProfessorModuleVersionPdf(Long moduleVersionId, UUID use return pdfCreator.createProfessorModuleVersionPdf(mv); } + + /// + /// + private boolean hasAccessPermission(Proposal proposal, User user) { + if (proposal.getCreatedBy().getUserId().equals(user.getUserId())) { + return true; + } + + return user.getRoles() != null && (user.getRoles().contains(UserRole.QUALITY_MANAGEMENT) + || user.getRoles().contains(UserRole.EXAMINATION_BOARD) + || user.getRoles().contains(UserRole.ACADEMIC_PROGRAM_ADVISOR)); + } } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java index 810fc519..be2ab559 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java @@ -1,6 +1,5 @@ package modulemanagement.ls1.services; -import jakarta.validation.Valid; import modulemanagement.ls1.dtos.*; import modulemanagement.ls1.enums.*; import modulemanagement.ls1.models.DegreeProgram; @@ -18,16 +17,19 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @Service +@Validated public class ProposalService { private final ProposalRepository proposalRepository; @@ -48,7 +50,7 @@ public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO r Proposal p = new Proposal(); p.setCreatedBy(user); p.setCreationDate(LocalDateTime.now()); - p.setStatus(ProposalStatus.PENDING_SUBMISSION); + p.setStatus(ProposalStatus.PENDING_FIRST_SUBMISSION); p = proposalRepository.save(p); ModuleVersion mv = new ModuleVersion(); @@ -56,7 +58,7 @@ public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO r mv.setModuleId(null); mv.setCreationDate(LocalDateTime.now()); mv.setProposal(p); - mv.setStatus(ModuleVersionStatus.PENDING_SUBMISSION); + mv.setStatus(ModuleVersionStatus.PENDING_FIRST_SUBMISSION); mv.setBulletPoints(request.getBulletPoints()); mv.setTitleEng(request.getTitleEng()); mv.setTitleDe(request.getTitleDe()); @@ -88,7 +90,6 @@ public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO r mv.setLiteratureEng(request.getLiteratureEng()); mv.setResponsiblesEng(request.getResponsiblesEng()); mv.setLvSwsLecturerEng(request.getLvSwsLecturerEng()); - mv = moduleVersionRepository.save(mv); if (request.getDegreeProgramAssignments() != null && !request.getDegreeProgramAssignments().isEmpty()) { List programIds = request.getDegreeProgramAssignments().stream() @@ -116,56 +117,17 @@ public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO r assignment.setDegreeProgramSpecialization(spec); mv.getDegreeProgramAssignments().add(assignment); } - moduleVersionRepository.save(mv); } - List feedbacks = new ArrayList<>(); - createNewFeedbacks(mv, feedbacks); - feedbacks = feedbackRepository.saveAll(feedbacks); - mv.setRequiredFeedbacks(feedbacks); + mv.setRequiredFeedbacks(new ArrayList<>()); moduleVersionRepository.save(mv); - p.addModuleVersion(mv); - proposalRepository.save(p); - return ProposalViewDTO.from(p); - } - - public static void createNewFeedbacks(ModuleVersion mv, List feedbacks) { - for (UserRole ad : UserRole.values()) { - if (ad.equals(UserRole.PROFESSOR) || ad.equals(UserRole.ADMIN)) - continue; - Feedback feedback = new Feedback(); - feedback.setStatus(FeedbackStatus.PENDING_SUBMISSION); - feedback.setRequiredRole(ad); - feedback.setModuleVersion(mv); - feedbacks.add(feedback); - } - } - - public ProposalViewDTO addModuleVersion(UUID userId, @Valid AddModuleVersionDTO request) { - Proposal p = proposalRepository.findById(request.getProposalId()) - .orElseThrow(() -> new ResourceNotFoundException("Proposal not found")); - if (!userId.equals(p.getCreatedBy().getUserId())) - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, - "You cannot add a module version to a module you did not create."); - if (!p.getStatus().equals(ProposalStatus.REQUIRES_REVIEW)) - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, - "You can only add a new module version, if the proposal requires a review."); - - for (Feedback f : p.getLatestModuleVersionWithContent().getRequiredFeedbacks()) { - if (f.getStatus().equals(FeedbackStatus.PENDING_FEEDBACK)) { - f.setStatus(FeedbackStatus.OBSOLETE); - } - } - - p.addNewModuleVersion(); + p.getModuleVersions().add(mv); proposalRepository.save(p); return ProposalViewDTO.from(p); } public List getCompactProposalsOfUser(UUID userId) { - return proposalRepository.findAll() - .stream() - .filter(proposal -> proposal.getCreatedBy().getUserId().equals(userId)) + return proposalRepository.findByCreatedBy_UserId(userId).stream() .map(p -> new ProposalsCompactDTO( p.getProposalId(), p.getCreatedBy().getFirstName(), @@ -190,7 +152,8 @@ public ProposalViewDTO getProposalViewDtoById(UUID userId, long id) { } - public ProposalViewDTO submitProposal(Long proposalId, UUID userId) { + @Transactional + public ProposalViewDTO requestCoordinatorsFeedback(Long proposalId, UUID userId) { Proposal proposal = proposalRepository.findById(proposalId) .orElseThrow(() -> new IllegalArgumentException("No proposal with id " + proposalId + " found")); if (!proposal.getCreatedBy().getUserId().equals(userId)) { @@ -202,64 +165,102 @@ public ProposalViewDTO submitProposal(Long proposalId, UUID userId) { ModuleVersion mv = proposal.getLatestModuleVersionWithContent(); - if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_SUBMISSION)) { - throw new IllegalStateException("Proposal is not pending submission. It is " + mv.getStatus() + "."); + if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_FIRST_SUBMISSION)) { + throw new IllegalStateException("Proposal is not pending first submission. It is " + mv.getStatus() + "."); } - if (!mv.isCompleted()) { - throw new IllegalStateException("All required fields in ModuleVersion must be filled."); + if (!mv.isFirstStepComplete()) { + throw new IllegalStateException( + "Step 1 must be complete: English title and at least one degree program assignment with a chosen specialization."); } - mv.setStatus(ModuleVersionStatus.PENDING_FEEDBACK); - proposal.setStatus(ProposalStatus.PENDING_FEEDBACK); - List feedbacks = mv.getRequiredFeedbacks(); - for (Feedback feedback : feedbacks) { + List requiredFeedbacks = new ArrayList<>(); + + for (ModuleVersionDegreeProgramAssignment assignment : mv.getDegreeProgramAssignments()) { + var spec = assignment.getDegreeProgramSpecialization(); + Feedback feedback = new Feedback(); feedback.setStatus(FeedbackStatus.PENDING_FEEDBACK); + feedback.setDegreeProgramSpecialization(spec); + feedback.setRequiredRole(null); + feedback.setModuleVersion(mv); + requiredFeedbacks.add(feedbackRepository.save(feedback)); } - feedbackRepository.saveAll(feedbacks); + + mv.setStatus(ModuleVersionStatus.PENDING_COORDINATOR_FEEDBACK); + proposal.setStatus(ProposalStatus.PENDING_COORDINATOR_FEEDBACK); moduleVersionRepository.save(mv); + + // to keep the version with requested feedback immutable + proposal.addNewModuleVersion(); proposalRepository.save(proposal); + return ProposalViewDTO.from(proposal); } - public ProposalViewDTO cancelSubmission(Long proposalId, UUID userId) { + /** + * Roles that receive feedback in the second submission (after coordinator + * feedback is accepted). + */ + private static final Set FULL_FEEDBACK_ROLES = Set.of( + UserRole.QUALITY_MANAGEMENT, + UserRole.ACADEMIC_PROGRAM_ADVISOR, + UserRole.EXAMINATION_BOARD); + + @Transactional + public ProposalViewDTO requestFullFeedback(Long proposalId, UUID userId) { Proposal proposal = proposalRepository.findById(proposalId) .orElseThrow(() -> new IllegalArgumentException("No proposal with id " + proposalId + " found")); - if (!proposal.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); } + if (proposal.getModuleVersions() == null || proposal.getModuleVersions().isEmpty()) { + throw new IllegalStateException("Proposal must have at least one ModuleVersion."); + } ModuleVersion mv = proposal.getLatestModuleVersionWithContent(); - if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_FEEDBACK)) { + if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_FULL_SUBMISSION)) { throw new IllegalStateException( - "Only submitted proposals can cancel their submission. This proposal is " + mv.getStatus() + "."); + "Proposal must be in PENDING_FULL_SUBMISSION (coordinator feedback accepted). It is " + + mv.getStatus() + "."); + } + if (!mv.isCompleted()) { + throw new IllegalStateException("All steps must be completed before submitting for full feedback."); } - boolean oneFeedbackNotPending = false; - for (Feedback f : mv.getRequiredFeedbacks()) { - if (!f.getStatus().equals(FeedbackStatus.PENDING_FEEDBACK)) { - oneFeedbackNotPending = true; - break; - } + List requiredFeedbacks = mv.getRequiredFeedbacks() != null ? mv.getRequiredFeedbacks() + : new ArrayList<>(); + List coordinatorFeedbacks = requiredFeedbacks.stream() + .filter(f -> f.getDegreeProgramSpecialization() != null) + .toList(); + if (coordinatorFeedbacks.isEmpty()) { + throw new IllegalStateException( + "No coordinator feedbacks for current assignments. Submit for coordinator feedback first."); + } + boolean allCoordinatorAccepted = coordinatorFeedbacks.stream() + .allMatch(f -> f.getStatus() == FeedbackStatus.APPROVED); + if (!allCoordinatorAccepted) { + throw new IllegalStateException( + "All feedback from program and area coordinators (for current assignments) must be accepted before submitting for full feedback."); } - if (oneFeedbackNotPending) { - for (Feedback f : mv.getRequiredFeedbacks()) { - if (f.getStatus().equals(FeedbackStatus.PENDING_FEEDBACK)) { - f.setStatus(FeedbackStatus.CANCELLED); - } - } - mv.setStatus(ModuleVersionStatus.CANCELLED); - proposal.setStatus(ProposalStatus.REQUIRES_REVIEW); - } else { - for (Feedback f : mv.getRequiredFeedbacks()) { - f.setStatus(FeedbackStatus.PENDING_SUBMISSION); - } - mv.setStatus(ModuleVersionStatus.PENDING_SUBMISSION); - proposal.setStatus(ProposalStatus.PENDING_SUBMISSION); + // Create new feedbacks (one per role); do not reuse old ones. + for (UserRole role : FULL_FEEDBACK_ROLES) { + Feedback feedback = new Feedback(); + feedback.setStatus(FeedbackStatus.PENDING_FEEDBACK); + feedback.setRequiredRole(role); + feedback.setDegreeProgramSpecialization(null); + feedback.setModuleVersion(mv); + requiredFeedbacks.add(feedbackRepository.save(feedback)); } + + mv.setStatus(ModuleVersionStatus.PENDING_FULL_FEEDBACK); + proposal.setStatus(ProposalStatus.PENDING_FULL_FEEDBACK); + moduleVersionRepository.save(mv); + + // to keep the version with requested feedback immutable + proposal.addNewModuleVersion(); proposalRepository.save(proposal); + return ProposalViewDTO.from(proposal); } @@ -269,9 +270,9 @@ public void deleteProposalById(long proposalId, UUID userId) { if (!p.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); } - if (p.getStatus() != ProposalStatus.PENDING_SUBMISSION) { + if (!p.getStatus().equals(ProposalStatus.PENDING_FIRST_SUBMISSION)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, - "You can only delete a proposal that is not already submit. This module proposal is " + "You can only delete a proposal that is not already submitted. This module proposal is " + p.getStatus() + "."); } proposalRepository.delete(p); diff --git a/Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java b/Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java new file mode 100644 index 00000000..a1c99a66 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java @@ -0,0 +1,96 @@ +package modulemanagement.ls1.services; + +import modulemanagement.ls1.enums.UserRole; +import modulemanagement.ls1.models.DegreeProgram; +import modulemanagement.ls1.models.DegreeProgramSpecialization; +import modulemanagement.ls1.models.User; +import modulemanagement.ls1.repositories.DegreeProgramRepository; +import modulemanagement.ls1.repositories.DegreeProgramSpecializationRepository; +import modulemanagement.ls1.repositories.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Keeps PROGRAM_COORDINATOR and SPECIALIZATION_AREA_RESPONSIBLE roles in sync: + * - Program coordinators (responsible for a degree program) have PROGRAM_COORDINATOR. + * - Specialization area responsibles have SPECIALIZATION_AREA_RESPONSIBLE. + */ +@Service +public class ResponsibleUserRoleService { + + private final UserRepository userRepository; + private final DegreeProgramRepository degreeProgramRepository; + private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; + + public ResponsibleUserRoleService(UserRepository userRepository, + DegreeProgramRepository degreeProgramRepository, + DegreeProgramSpecializationRepository degreeProgramSpecializationRepository) { + this.userRepository = userRepository; + this.degreeProgramRepository = degreeProgramRepository; + this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; + } + + @Transactional + public void ensureProgramCoordinatorRole(UUID userId) { + User user = userRepository.findById(userId).orElse(null); + if (user == null) return; + if (user.getRoles() != null && user.getRoles().contains(UserRole.PROGRAM_COORDINATOR)) + return; + if (user.getRoles() == null) user.setRoles(new ArrayList<>()); + user.getRoles().add(UserRole.PROGRAM_COORDINATOR); + userRepository.save(user); + } + + @Transactional + public void removeProgramCoordinatorRoleIfNotResponsible(UUID userId) { + if (degreeProgramRepository.existsByResponsibleUser_UserId(userId)) + return; + User user = userRepository.findById(userId).orElse(null); + if (user == null || user.getRoles() == null) return; + if (user.getRoles().remove(UserRole.PROGRAM_COORDINATOR)) + userRepository.save(user); + } + + @Transactional + public void ensureSpecializationAreaResponsibleRole(UUID userId) { + User user = userRepository.findById(userId).orElse(null); + if (user == null) return; + if (user.getRoles() != null && user.getRoles().contains(UserRole.SPECIALIZATION_AREA_RESPONSIBLE)) + return; + if (user.getRoles() == null) user.setRoles(new ArrayList<>()); + user.getRoles().add(UserRole.SPECIALIZATION_AREA_RESPONSIBLE); + userRepository.save(user); + } + + @Transactional + public void removeSpecializationAreaResponsibleRoleIfNotResponsible(UUID userId) { + if (degreeProgramSpecializationRepository.existsByResponsibleUser_UserId(userId)) + return; + User user = userRepository.findById(userId).orElse(null); + if (user == null || user.getRoles() == null) return; + if (user.getRoles().remove(UserRole.SPECIALIZATION_AREA_RESPONSIBLE)) + userRepository.save(user); + } + + @Transactional + public void unassignFromAllPrograms(UUID userId) { + List programs = degreeProgramRepository.findByResponsibleUser_UserId(userId); + for (DegreeProgram p : programs) { + p.setResponsibleUser(null); + } + if (!programs.isEmpty()) degreeProgramRepository.saveAll(programs); + } + + @Transactional + public void unassignFromAllSpecializations(UUID userId) { + List specs = degreeProgramSpecializationRepository.findByResponsibleUser_UserId(userId); + for (DegreeProgramSpecialization s : specs) { + s.setResponsibleUser(null); + } + if (!specs.isEmpty()) degreeProgramSpecializationRepository.saveAll(specs); + } +} diff --git a/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml b/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml index d37e43dc..e82ed550 100644 --- a/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml +++ b/Server/src/main/resources/db/changelog/changes/0008_degree_program_and_area_tables.yaml @@ -21,7 +21,7 @@ databaseChangeLog: name: responsible_user_id type: UUID constraints: - nullable: false + nullable: true - addForeignKeyConstraint: baseTableName: degree_program baseColumnNames: responsible_user_id @@ -46,7 +46,7 @@ databaseChangeLog: name: responsible_user_id type: UUID constraints: - nullable: false + nullable: true - addForeignKeyConstraint: baseTableName: degree_program_specialization baseColumnNames: responsible_user_id diff --git a/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml b/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml new file mode 100644 index 00000000..8d273cd0 --- /dev/null +++ b/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: + - changeSet: + id: 0012_feedback_requested_from_user + author: module-management + changes: + - addColumn: + tableName: feedback + columns: + - column: + name: degree_program_specialization_id + type: BIGINT + constraints: + nullable: true + foreignKeyName: feedback_degree_program_specialization_fk + references: degree_program_specialization(degree_program_specialization_id) + - column: + name: invalidated + type: BOOLEAN + constraints: + nullable: false + defaultValueBoolean: false + + - dropNotNullConstraint: + tableName: feedback + columnName: required_role + columnDataType: VARCHAR(255) diff --git a/Server/src/main/resources/db/changelog/changes/0013_grant_program_area_responsible_role.yaml b/Server/src/main/resources/db/changelog/changes/0013_grant_program_area_responsible_role.yaml new file mode 100644 index 00000000..d3842eb3 --- /dev/null +++ b/Server/src/main/resources/db/changelog/changes/0013_grant_program_area_responsible_role.yaml @@ -0,0 +1,33 @@ +databaseChangeLog: + # Grant PROGRAM_COORDINATOR to users responsible for a degree program + - changeSet: + id: 0013a_grant_program_coordinator_role + author: module-management + changes: + - sql: + dbms: postgresql + sql: | + INSERT INTO app_user_role (user_id, role) + SELECT DISTINCT responsible_user_id, 'PROGRAM_COORDINATOR' + FROM degree_program + WHERE responsible_user_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM app_user_role r + WHERE r.user_id = degree_program.responsible_user_id AND r.role = 'PROGRAM_COORDINATOR' + ) + # Grant SPECIALIZATION_AREA_RESPONSIBLE to users responsible for a specialization + - changeSet: + id: 0013b_grant_specialization_area_responsible_role + author: module-management + changes: + - sql: + dbms: postgresql + sql: | + INSERT INTO app_user_role (user_id, role) + SELECT DISTINCT responsible_user_id, 'SPECIALIZATION_AREA_RESPONSIBLE' + FROM degree_program_specialization + WHERE responsible_user_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM app_user_role r + WHERE r.user_id = degree_program_specialization.responsible_user_id AND r.role = 'SPECIALIZATION_AREA_RESPONSIBLE' + ) diff --git a/Server/src/main/resources/db/changelog/master.yaml b/Server/src/main/resources/db/changelog/master.yaml index d2170be5..40436095 100644 --- a/Server/src/main/resources/db/changelog/master.yaml +++ b/Server/src/main/resources/db/changelog/master.yaml @@ -32,3 +32,9 @@ databaseChangeLog: - include: relativeToChangelogFile: true file: initialize/0011_seed_degree_program_specializations.yaml + - include: + relativeToChangelogFile: true + file: changes/0012_feedback_requested_from_user.yaml + - include: + relativeToChangelogFile: true + file: changes/0013_grant_program_area_responsible_role.yaml From 3d22936990e92e2cc810eb84375bc94c035994b7 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Tue, 17 Mar 2026 22:14:30 +0100 Subject: [PATCH 08/42] rename --- .../create-edit-base.component.html | 4 ++-- .../src/app/core/modules/openapi/model/feedback.ts | 4 ++-- .../model/module-version-view-feedback-dto.ts | 4 ++-- .../modules/openapi/model/update-user-role-dto.ts | 4 ++-- .../src/app/core/modules/openapi/model/user-dto.ts | 4 ++-- Client/src/app/core/modules/openapi/model/user.ts | 4 ++-- Client/src/app/core/shared/user-role.utils.ts | 4 ++-- .../module-version-view.component.html | 2 +- Client/src/app/pipes/feedbackDepartment.pipe.ts | 6 +++--- Client/src/app/pipes/proposalStatus.pipe.ts | 2 +- .../ls1/controllers/FeedbackController.java | 12 ++++++------ .../java/modulemanagement/ls1/enums/UserRole.java | 4 ++-- .../ls1/services/AdminUserService.java | 2 +- .../DegreeProgramSpecializationService.java | 8 ++++---- .../ls1/services/ResponsibleUserRoleService.java | 14 +++++++------- .../0013_grant_program_area_responsible_role.yaml | 8 ++++---- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index a30ab661..494d5c15 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -207,11 +207,11 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '
@if (isCreateMode()) { - Complete the first step and create the proposal. After saving, you can use this step to submit for feedback from program coordinators and area responsibles. + Complete the first step and create the proposal. After saving, you can use this step to submit for feedback from program coordinators and area coordinators. } @else {

- Submit to request feedback from program coordinators and area responsibles (first submission). One feedback is required per chosen degree program and area. Complete + Submit to request feedback from program coordinators and area coordinators (first submission). One feedback is required per chosen degree program and area. Complete step 1 first, then use the button below. You can cancel and resubmit later if needed.

@if (!isFirstStepComplete()) { diff --git a/Client/src/app/core/modules/openapi/model/feedback.ts b/Client/src/app/core/modules/openapi/model/feedback.ts index a1c66884..85d9d062 100644 --- a/Client/src/app/core/modules/openapi/model/feedback.ts +++ b/Client/src/app/core/modules/openapi/model/feedback.ts @@ -60,7 +60,7 @@ export interface Feedback { comment?: string; } export namespace Feedback { - export type RequiredRoleEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_RESPONSIBLE'; + export type RequiredRoleEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_COORDINATOR'; export const RequiredRoleEnum = { Admin: 'ADMIN' as RequiredRoleEnum, QualityManagement: 'QUALITY_MANAGEMENT' as RequiredRoleEnum, @@ -68,7 +68,7 @@ export namespace Feedback { ExaminationBoard: 'EXAMINATION_BOARD' as RequiredRoleEnum, Professor: 'PROFESSOR' as RequiredRoleEnum, ProgramCoordinator: 'PROGRAM_COORDINATOR' as RequiredRoleEnum, - SpecializationAreaResponsible: 'SPECIALIZATION_AREA_RESPONSIBLE' as RequiredRoleEnum + SpecializationAreaCoordinator: 'SPECIALIZATION_AREA_COORDINATOR' as RequiredRoleEnum }; export type StatusEnum = 'PENDING_FEEDBACK' | 'APPROVED' | 'FEEDBACK_GIVEN' | 'REJECTED'; export const StatusEnum = { diff --git a/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts index b940b671..c420ed0a 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts @@ -40,7 +40,7 @@ export interface ModuleVersionViewFeedbackDTO { lvSwsLecturerFeedback?: string; } export namespace ModuleVersionViewFeedbackDTO { - export type FeedbackRoleEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_RESPONSIBLE'; + export type FeedbackRoleEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_COORDINATOR'; export const FeedbackRoleEnum = { Admin: 'ADMIN' as FeedbackRoleEnum, QualityManagement: 'QUALITY_MANAGEMENT' as FeedbackRoleEnum, @@ -48,7 +48,7 @@ export namespace ModuleVersionViewFeedbackDTO { ExaminationBoard: 'EXAMINATION_BOARD' as FeedbackRoleEnum, Professor: 'PROFESSOR' as FeedbackRoleEnum, ProgramCoordinator: 'PROGRAM_COORDINATOR' as FeedbackRoleEnum, - SpecializationAreaResponsible: 'SPECIALIZATION_AREA_RESPONSIBLE' as FeedbackRoleEnum + SpecializationAreaCoordinator: 'SPECIALIZATION_AREA_COORDINATOR' as FeedbackRoleEnum }; export type FeedbackStatusEnum = 'PENDING_FEEDBACK' | 'APPROVED' | 'FEEDBACK_GIVEN' | 'REJECTED'; export const FeedbackStatusEnum = { diff --git a/Client/src/app/core/modules/openapi/model/update-user-role-dto.ts b/Client/src/app/core/modules/openapi/model/update-user-role-dto.ts index 14813c0a..81cbb4d9 100644 --- a/Client/src/app/core/modules/openapi/model/update-user-role-dto.ts +++ b/Client/src/app/core/modules/openapi/model/update-user-role-dto.ts @@ -13,7 +13,7 @@ export interface UpdateUserRoleDTO { roles: Array; } export namespace UpdateUserRoleDTO { - export type RolesEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_RESPONSIBLE'; + export type RolesEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_COORDINATOR'; export const RolesEnum = { Admin: 'ADMIN' as RolesEnum, QualityManagement: 'QUALITY_MANAGEMENT' as RolesEnum, @@ -21,7 +21,7 @@ export namespace UpdateUserRoleDTO { ExaminationBoard: 'EXAMINATION_BOARD' as RolesEnum, Professor: 'PROFESSOR' as RolesEnum, ProgramCoordinator: 'PROGRAM_COORDINATOR' as RolesEnum, - SpecializationAreaResponsible: 'SPECIALIZATION_AREA_RESPONSIBLE' as RolesEnum + SpecializationAreaCoordinator: 'SPECIALIZATION_AREA_COORDINATOR' as RolesEnum }; } diff --git a/Client/src/app/core/modules/openapi/model/user-dto.ts b/Client/src/app/core/modules/openapi/model/user-dto.ts index 683e78ea..04ae31aa 100644 --- a/Client/src/app/core/modules/openapi/model/user-dto.ts +++ b/Client/src/app/core/modules/openapi/model/user-dto.ts @@ -18,7 +18,7 @@ export interface UserDTO { roles?: Array; } export namespace UserDTO { - export type RolesEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_RESPONSIBLE'; + export type RolesEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_COORDINATOR'; export const RolesEnum = { Admin: 'ADMIN' as RolesEnum, QualityManagement: 'QUALITY_MANAGEMENT' as RolesEnum, @@ -26,7 +26,7 @@ export namespace UserDTO { ExaminationBoard: 'EXAMINATION_BOARD' as RolesEnum, Professor: 'PROFESSOR' as RolesEnum, ProgramCoordinator: 'PROGRAM_COORDINATOR' as RolesEnum, - SpecializationAreaResponsible: 'SPECIALIZATION_AREA_RESPONSIBLE' as RolesEnum + SpecializationAreaCoordinator: 'SPECIALIZATION_AREA_COORDINATOR' as RolesEnum }; } diff --git a/Client/src/app/core/modules/openapi/model/user.ts b/Client/src/app/core/modules/openapi/model/user.ts index 536f879a..4dadbd6c 100644 --- a/Client/src/app/core/modules/openapi/model/user.ts +++ b/Client/src/app/core/modules/openapi/model/user.ts @@ -18,7 +18,7 @@ export interface User { roles?: Array; } export namespace User { - export type RolesEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_RESPONSIBLE'; + export type RolesEnum = 'ADMIN' | 'QUALITY_MANAGEMENT' | 'ACADEMIC_PROGRAM_ADVISOR' | 'EXAMINATION_BOARD' | 'PROFESSOR' | 'PROGRAM_COORDINATOR' | 'SPECIALIZATION_AREA_COORDINATOR'; export const RolesEnum = { Admin: 'ADMIN' as RolesEnum, QualityManagement: 'QUALITY_MANAGEMENT' as RolesEnum, @@ -26,7 +26,7 @@ export namespace User { ExaminationBoard: 'EXAMINATION_BOARD' as RolesEnum, Professor: 'PROFESSOR' as RolesEnum, ProgramCoordinator: 'PROGRAM_COORDINATOR' as RolesEnum, - SpecializationAreaResponsible: 'SPECIALIZATION_AREA_RESPONSIBLE' as RolesEnum + SpecializationAreaCoordinator: 'SPECIALIZATION_AREA_COORDINATOR' as RolesEnum }; } diff --git a/Client/src/app/core/shared/user-role.utils.ts b/Client/src/app/core/shared/user-role.utils.ts index cf6843a9..cd674760 100644 --- a/Client/src/app/core/shared/user-role.utils.ts +++ b/Client/src/app/core/shared/user-role.utils.ts @@ -1,12 +1,12 @@ import { User } from '../modules/openapi'; -/** Roles that can review proposals and see pending feedbacks (approval staff, program coordinators, specialization area responsibles). */ +/** Roles that can review proposals and see pending feedbacks (approval staff, program coordinators, specialization area coordinators). */ export const REVIEWER_ROLES: readonly User.RolesEnum[] = [ User.RolesEnum.QualityManagement, User.RolesEnum.AcademicProgramAdvisor, User.RolesEnum.ExaminationBoard, User.RolesEnum.ProgramCoordinator, - User.RolesEnum.SpecializationAreaResponsible + User.RolesEnum.SpecializationAreaCoordinator ] as const; export function isAdminRole(roles: User.RolesEnum[] | undefined | null): boolean { diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.html b/Client/src/app/pages/module-version-view/module-version-view.component.html index 8945f03d..990117da 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.html +++ b/Client/src/app/pages/module-version-view/module-version-view.component.html @@ -87,7 +87,7 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ @if (currentStepIndex() === 1) { + }
@if (canRequestCoordinatorsFeedback()) { fb.feedbackStatus === ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Rejected); + if (hasRejection) return StepperStatus.ActionRequired; + const allApproved = feedbacks.every((fb) => fb.feedbackStatus === ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Approved); + return allApproved ? StepperStatus.Completed : StepperStatus.Pending; } + if (step.id === 'submit-full-feedback') { return StepperStatus.Default; } @@ -114,6 +120,28 @@ export abstract class ProposalBaseComponent { return status === 'PENDING_FULL_SUBMISSION' && this.allFormStepsComplete(); }); + /** Coordinator feedbacks for this version (for current assignments). From moduleVersionDto().feedbacks. */ + coordinatorFeedbacksForCurrentAssignmentsFromDto = computed(() => { + const dto = this.moduleVersionDto(); + const feedbacks = (dto as ModuleVersionViewDTO)?.feedbacks ?? []; + const coordinator = feedbacks.filter((f) => f.feedbackRole == null); + const specIds = new Set((dto as ModuleVersionViewDTO)?.degreeProgramAssignments?.map((a) => a.degreeProgramSpecializationId).filter((id): id is number => id != null) ?? []); + if (specIds.size === 0) return coordinator; + return coordinator.filter((f) => f.degreeProgramSpecializationId != null && specIds.has(f.degreeProgramSpecializationId)); + }); + + /** Coordinator feedbacks to show in step 1: current version if any, otherwise previous version (feedbacks() from API). */ + coordinatorFeedbacksForStep1 = computed(() => { + const fromDto = this.coordinatorFeedbacksForCurrentAssignmentsFromDto(); + if (fromDto.length > 0) return { feedbacks: fromDto, fromPrevious: false }; + const prev = this.feedbacks() ?? []; + const coordinator = prev.filter((f) => f.feedbackRole == null); + const dto = this.moduleVersionDto() as ModuleVersionViewDTO | null; + const specIds = new Set(dto?.degreeProgramAssignments?.map((a) => a.degreeProgramSpecializationId).filter((id): id is number => id != null) ?? []); + const filtered = specIds.size === 0 ? coordinator : coordinator.filter((f) => f.degreeProgramSpecializationId != null && specIds.has(f.degreeProgramSpecializationId)); + return { feedbacks: filtered, fromPrevious: true }; + }); + showPrompt: { [key: string]: boolean } = { examination: false, content: false, @@ -239,7 +267,7 @@ export abstract class ProposalBaseComponent { if (newId != null && newId !== this.moduleVersionId) { this.moduleVersionId = newId; this.breadcrumbLabels.versionLabel.set(response?.latestVersion != null ? `Version ${response.latestVersion}` : null); - this.router.navigate(['/proposals/view', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); + this.router.navigate(['/proposals', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); } }, error: (err: HttpErrorResponse) => { @@ -264,7 +292,7 @@ export abstract class ProposalBaseComponent { if (newId != null && newId !== this.moduleVersionId) { this.moduleVersionId = newId; this.breadcrumbLabels.versionLabel.set(response?.latestVersion != null ? `Version ${response.latestVersion}` : null); - this.router.navigate(['/proposals/view', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); + this.router.navigate(['/proposals', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); } }, error: (err: HttpErrorResponse) => { diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css index 59540382..186c910a 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.css @@ -38,6 +38,11 @@ color: var(--p-primary-contrast-color, #1a1a1a); } +.step-item--action-required .step-number { + background: var(--p-error-color, #ef4444); + color: white; +} + .step-number { flex-shrink: 0; width: 1.75rem; @@ -57,31 +62,3 @@ font-size: 1rem; } -.step-feedbacks { - display: flex; - gap: 0.25rem; -} - -.feedback-dot { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--p-surface-400, #adb5bd); -} - -.feedback-dot--PENDING_SUBMISSION { - background: #94a3b8; -} - -.feedback-dot--PENDING_FEEDBACK { - background: #eab308; -} - -.feedback-dot--APPROVED, -.feedback-dot--FEEDBACK_GIVEN { - background: #22c55e; -} - -.feedback-dot--REJECTED { - background: #ef4444; -} diff --git a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html index 3b49daf3..ca313ecd 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html +++ b/Client/src/app/components/module-edit-stepper/module-edit-stepper.component.html @@ -6,6 +6,7 @@ [class.step-item--active]="i === currentIndex()" [class.step-item--completed]="stepStatuses()[i] === StepperStatus.Completed" [class.step-item--pending]="stepStatuses()[i] === StepperStatus.Pending" + [class.step-item--action-required]="stepStatuses()[i] === StepperStatus.ActionRequired" (click)="goToStep(i)" > diff --git a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts index f4df2148..750903ca 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts +++ b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts @@ -1,8 +1,11 @@ +import { Action } from 'rxjs/internal/scheduler/Action'; + /** Status of a step in the stepper; determines how the step is displayed (number, check, or pending color). */ export const StepperStatus = { Default: 'default', Pending: 'pending', - Completed: 'completed' + Completed: 'completed', + ActionRequired: 'action-required' } as const; export type StepperStatus = (typeof StepperStatus)[keyof typeof StepperStatus]; @@ -66,9 +69,3 @@ export const MODULE_EDIT_STEPS: ModuleEditStepConfig[] = [ controlNames: [] } ]; - -/** Steps for the module overview (view) page: edit steps + Feedbacks as final step. */ -export const MODULE_VIEW_STEPS: ModuleEditStepConfig[] = [ - ...MODULE_EDIT_STEPS, - { id: 'feedbacks', title: 'Feedbacks', controlNames: [] } -]; diff --git a/Client/src/app/components/proposal-list-table/proposal-list-table.component.html b/Client/src/app/components/proposal-list-table/proposal-list-table.component.html index 185ef0f5..29684b57 100644 --- a/Client/src/app/components/proposal-list-table/proposal-list-table.component.html +++ b/Client/src/app/components/proposal-list-table/proposal-list-table.component.html @@ -8,7 +8,7 @@ - + {{ proposal.proposalId }} {{ proposal.latestTitle }} @@ -17,7 +17,7 @@ - + diff --git a/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts b/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts index c98a5eba..936209b4 100644 --- a/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts +++ b/Client/src/app/core/modules/openapi/api/module-version-controller.service.ts @@ -622,7 +622,7 @@ export class ModuleVersionControllerService implements ModuleVersionControllerSe } } - let localVarPath = `/api/module-versions/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/previous-module-version-feedback`; + let localVarPath = `/api/module-versions/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/previous-module-versions-feedback`; return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, diff --git a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts index 07a83c44..7b5d6838 100644 --- a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts +++ b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute, RouterModule } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { ProposalBaseComponent } from '../../components/create-edit-base/create-edit-base.component'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; +import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; import { ModuleEditStepperComponent } from '../../components/module-edit-stepper/module-edit-stepper.component'; import { ModuleDegreeProgramAssignmentDTO, ModuleVersionUpdateRequestDTO, ModuleVersionViewDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; import { ToggleButtonGroupComponent } from '../../components/toggle-button-group/toggle-button-group.component'; @@ -26,6 +27,7 @@ import { TagModule } from 'primeng/tag'; CommonModule, RouterModule, FeedbackDepartmentPipe, + FeedbackStatusPipe, ToggleButtonGroupComponent, ModuleEditStepperComponent, ButtonModule, diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.html b/Client/src/app/pages/module-version-view/module-version-view.component.html index 990117da..0c274e5c 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.html +++ b/Client/src/app/pages/module-version-view/module-version-view.component.html @@ -14,7 +14,7 @@ } @else if (moduleVersionDto(); as moduleVersionDto) {

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ moduleVersionDto.version }}

@@ -22,10 +22,10 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{
@if (proposalId) { - + } @if (isLatestVersion() && proposalId) { - + }

@@ -85,16 +85,16 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ } @if (currentStepIndex() === 1) { - + } @if (currentStepIndex() === 2) { diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.ts b/Client/src/app/pages/module-version-view/module-version-view.component.ts index 2c6925e7..2a46056a 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.ts +++ b/Client/src/app/pages/module-version-view/module-version-view.component.ts @@ -5,7 +5,7 @@ import { RouterModule, ActivatedRoute } from '@angular/router'; import { ModuleVersionControllerService, ModuleVersionViewDTO, ModuleVersionViewFeedbackDTO } from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { ModuleEditStepperComponent } from '../../components/module-edit-stepper/module-edit-stepper.component'; -import { MODULE_VIEW_STEPS, StepperStatus } from '../../components/module-edit-stepper/module-edit-steps.config'; +import { MODULE_EDIT_STEPS, StepperStatus } from '../../components/module-edit-stepper/module-edit-steps.config'; import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; import { ModuleVersionStatusPipe } from '../../pipes/moduleVersionStatus.pipe'; @@ -56,9 +56,34 @@ export class ModuleVersionViewComponent { moduleVersionStatus = ModuleVersionViewDTO.StatusEnum; error = signal(null); - readonly MODULE_VIEW_STEPS = MODULE_VIEW_STEPS; + readonly MODULE_EDIT_STEPS = MODULE_EDIT_STEPS; currentStepIndex = signal(0); + /** Coordinator feedbacks for this version (for current assignments). From moduleVersionDto().feedbacks. */ + coordinatorFeedbacksForCurrentAssignments = computed(() => { + const dto = this.moduleVersionDto(); + const feedbacks = dto?.feedbacks ?? []; + const coordinator = feedbacks.filter((f) => f.feedbackRole == null); + const specIds = new Set(dto?.degreeProgramAssignments?.map((a) => a.degreeProgramSpecializationId).filter((id): id is number => id != null) ?? []); + if (specIds.size === 0) return coordinator; + return coordinator.filter((f) => f.degreeProgramSpecializationId != null && specIds.has(f.degreeProgramSpecializationId)); + }); + + /** Previous module version feedback (all non-invalidated for proposal). Fetched when loading; used when current version has no feedbacks yet. */ + previousVersionFeedbacks = signal([]); + + /** Coordinator feedbacks to show in step 1: current version if any, otherwise previous version feedback (so latest MV still shows status). */ + coordinatorFeedbacksForStep1 = computed(() => { + const fromCurrent = this.coordinatorFeedbacksForCurrentAssignments(); + if (fromCurrent.length > 0) return { feedbacks: fromCurrent, fromPrevious: false }; + const prev = this.previousVersionFeedbacks(); + const coordinator = prev.filter((f) => f.feedbackRole == null); + const dto = this.moduleVersionDto(); + const specIds = new Set(dto?.degreeProgramAssignments?.map((a) => a.degreeProgramSpecializationId).filter((id): id is number => id != null) ?? []); + const filtered = specIds.size === 0 ? coordinator : coordinator.filter((f) => f.degreeProgramSpecializationId != null && specIds.has(f.degreeProgramSpecializationId)); + return { feedbacks: filtered, fromPrevious: true }; + }); + moduleFields: ModuleField[] = [ { key: 'titleEng', label: 'Title', section: 'basic', feedbackKey: 'titleFeedback' }, { key: 'titleDe', label: 'Title (German)', section: 'basic' }, @@ -105,14 +130,22 @@ export class ModuleVersionViewComponent { stepStatuses = computed(() => { const dto = this.moduleVersionDto(); - if (!dto) return MODULE_VIEW_STEPS.map(() => StepperStatus.Default); + if (!dto) return MODULE_EDIT_STEPS.map(() => StepperStatus.Default); - return MODULE_VIEW_STEPS.map((step, index) => { + return MODULE_EDIT_STEPS.map((step, index) => { if (step.id === 'feedbacks') return StepperStatus.Default; - if (step.id === 'submit-coordinator-feedback') return StepperStatus.Default; + if (step.id === 'submit-coordinator-feedback') { + const feedbacks = this.coordinatorFeedbacksForStep1().feedbacks; + if (feedbacks.length === 0) return StepperStatus.Default; + const hasRejection = feedbacks.some((fb) => fb.feedbackStatus === ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Rejected); + if (hasRejection) return StepperStatus.ActionRequired; + const allApproved = feedbacks.every((fb) => fb.feedbackStatus === ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Approved); + return allApproved ? StepperStatus.Completed : StepperStatus.Pending; + } + if (step.id === 'submit-full-feedback') return StepperStatus.Default; - const keys = MODULE_VIEW_STEPS[index].controlNames as (keyof ModuleVersionViewDTO)[]; + const keys = MODULE_EDIT_STEPS[index].controlNames as (keyof ModuleVersionViewDTO)[]; if (!keys?.length) return StepperStatus.Default; const allFilled = keys.every((k) => { const v = dto[k]; @@ -131,6 +164,7 @@ export class ModuleVersionViewComponent { const versionId = params.get('versionId'); this.moduleVersionId = Number(versionId); this.fetchModuleVersionViewDto(this.moduleVersionId); + this.fetchPreviousVersionsFeedbacks(this.moduleVersionId); } goToStep(index: number) { @@ -138,7 +172,7 @@ export class ModuleVersionViewComponent { } getFieldsForViewStep(stepIndex: number): ModuleField[] { - const keys = MODULE_VIEW_STEPS[stepIndex].controlNames as (keyof ModuleVersionViewDTO)[]; + const keys = MODULE_EDIT_STEPS[stepIndex].controlNames as (keyof ModuleVersionViewDTO)[]; if (!keys?.length) return []; return this.moduleFields.filter((f) => keys.includes(f.key)); } @@ -157,6 +191,11 @@ export class ModuleVersionViewComponent { }); } + private fetchPreviousVersionsFeedbacks(moduleVersionId: number) { + this.moduleVersionService.getPreviousModuleVersionFeedback(moduleVersionId).subscribe({ + next: (list) => this.previousVersionFeedbacks.set(list) + }); + } pdfExport() { const mvid = this.moduleVersionId; if (!mvid) { diff --git a/Client/src/app/pages/proposal-create/proposal-create.component.ts b/Client/src/app/pages/proposal-create/proposal-create.component.ts index e9ee0d85..ad54260c 100644 --- a/Client/src/app/pages/proposal-create/proposal-create.component.ts +++ b/Client/src/app/pages/proposal-create/proposal-create.component.ts @@ -72,7 +72,7 @@ export class ProposalCreateComponent extends ProposalBaseComponent { const proposalId = res.proposalId; const moduleVersionId = res.latestModuleVersion?.moduleVersionId; if (proposalId != null && moduleVersionId != null) { - await this.router.navigate(['/proposals/view', proposalId, 'version', moduleVersionId, 'edit'], { queryParams: { created: true } }); + await this.router.navigate(['/proposals', proposalId, 'version', moduleVersionId, 'edit'], { queryParams: { created: true } }); } else { this.error.set('Unexpected response from server.'); } diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.html b/Client/src/app/pages/proposal-view/proposal-view.component.html index 1b1c0391..5f5adeb4 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.html +++ b/Client/src/app/pages/proposal-view/proposal-view.component.html @@ -59,7 +59,7 @@

Current Module Versions

- + {{ proposal.latestVersion }} {{ proposal.latestModuleVersion!.titleEng }} @@ -87,14 +87,14 @@

Current Module Versions

label="Edit" outlined severity="contrast" - [routerLink]="['/proposals/view', proposal.proposalId, 'version', proposal.latestModuleVersion!.moduleVersionId, 'edit']" + [routerLink]="['/proposals', proposal.proposalId, 'version', proposal.latestModuleVersion!.moduleVersionId, 'edit']" (click)="$event.stopPropagation()" />

@@ -116,7 +116,7 @@

Previous Module Versions - + {{ version.version }} {{ version.titleEng }} @@ -144,7 +144,7 @@

Previous Module Versions

diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java index 6695ab3d..97cb8b44 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ModuleVersionController.java @@ -56,13 +56,13 @@ public ResponseEntity getModuleVersion(@CurrentUser User u return ResponseEntity.ok(dto); } - @GetMapping("/{id}/previous-module-version-feedback") + @GetMapping("/{id}/previous-module-versions-feedback") @PreAuthorize("hasAnyRole('PROFESSOR')") public ResponseEntity> getPreviousModuleVersionFeedback( @CurrentUser User user, @PathVariable Long id) { - List lastRejectionReasons = moduleVersionService + List previousFeedbacks = moduleVersionService .getPreviousModuleVersionFeedback(user.getUserId(), id); - return ResponseEntity.ok(lastRejectionReasons); + return ResponseEntity.ok(previousFeedbacks); } @PostMapping("/generate/content") diff --git a/Server/src/main/java/modulemanagement/ls1/models/Proposal.java b/Server/src/main/java/modulemanagement/ls1/models/Proposal.java index 607e5961..172a527c 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/Proposal.java +++ b/Server/src/main/java/modulemanagement/ls1/models/Proposal.java @@ -2,14 +2,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.NotNull; -import modulemanagement.ls1.dtos.ModuleVersionViewFeedbackDTO; import modulemanagement.ls1.enums.ProposalStatus; import jakarta.persistence.*; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -96,19 +94,4 @@ public void addNewModuleVersion() { moduleVersions.add(newMv); } - public List getPreviousModuleVersionFeedback() { - List mvs = this.getModuleVersions(); - Collections.reverse(mvs); - for (ModuleVersion mv : mvs) { - List previousFeedbacks = new ArrayList<>(); - List feedbacks = mv.getRequiredFeedbacks(); - for (Feedback feedback : feedbacks) { - if (feedback.isFeedbackGiven()) { - previousFeedbacks.add(ModuleVersionViewFeedbackDTO.from(feedback)); - } - } - return previousFeedbacks; - } - return Collections.emptyList(); - } } diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java index 1b6909f9..e617eedb 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java @@ -16,4 +16,7 @@ public interface FeedbackRepository extends JpaRepository { /** Feedbacks for specializations that this user is currently responsible for (position-based). */ List findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatus(UUID userId, FeedbackStatus status); + + /** All feedbacks for a proposal that are not invalidated (for display on view/edit). */ + List findByModuleVersion_Proposal_ProposalIdAndInvalidatedFalse(Long proposalId); } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index 865fc02f..f4fb8909 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -17,6 +17,7 @@ import modulemanagement.ls1.models.Proposal; import modulemanagement.ls1.models.User; import modulemanagement.ls1.repositories.DegreeProgramRepository; +import modulemanagement.ls1.repositories.FeedbackRepository; import modulemanagement.ls1.repositories.ModuleVersionDegreeProgramAssignmentRepository; import modulemanagement.ls1.repositories.ModuleVersionRepository; import modulemanagement.ls1.repositories.ProposalRepository; @@ -45,17 +46,19 @@ public class ModuleVersionService { private final OverlapDetectionService overlapDetectionService; private final PdfCreator pdfCreator; private final EntityManager entityManager; + private final FeedbackRepository feedbackRepository; public ModuleVersionService(ModuleVersionRepository moduleVersionRepository, ModuleVersionDegreeProgramAssignmentRepository assignmentRepository, DegreeProgramRepository degreeProgramRepository, ProposalRepository proposalRepository, OverlapDetectionService overlapDetectionService, PdfCreator pdfCreator, - EntityManager entityManager) { + EntityManager entityManager, FeedbackRepository feedbackRepository) { this.moduleVersionRepository = moduleVersionRepository; this.assignmentRepository = assignmentRepository; this.degreeProgramRepository = degreeProgramRepository; this.proposalRepository = proposalRepository; this.overlapDetectionService = overlapDetectionService; + this.feedbackRepository = feedbackRepository; this.pdfCreator = pdfCreator; this.entityManager = entityManager; } @@ -311,8 +314,9 @@ public List getPreviousModuleVersionFeedback(UUID if (!proposal.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); } - - return proposal.getPreviousModuleVersionFeedback(); + List list = feedbackRepository + .findByModuleVersion_Proposal_ProposalIdAndInvalidatedFalse(proposal.getProposalId()); + return list.stream().map(ModuleVersionViewFeedbackDTO::from).toList(); } public Resource generateReviewerModuleVersionPdf(Long moduleVersionId, User user) { From 3c1e5a474c30966ac4a94461459114571f94033d Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Wed, 18 Mar 2026 22:20:32 +0100 Subject: [PATCH 10/42] invalidate previous feedbacks when needed --- .../module-version-edit.component.ts | 6 +- .../module-version-view.component.html | 5 +- .../proposal-view.component.html | 22 +++- .../ls1/repositories/FeedbackRepository.java | 4 +- .../ls1/services/FeedbackService.java | 7 +- .../ls1/services/ModuleVersionService.java | 104 ++++++------------ .../ls1/services/ProposalService.java | 2 +- .../ModuleVersionStepsChangeDetector.java | 82 ++++++++++++++ 8 files changed, 145 insertions(+), 87 deletions(-) create mode 100644 Server/src/main/java/modulemanagement/ls1/shared/ModuleVersionStepsChangeDetector.java diff --git a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts index 7b5d6838..f1a0017a 100644 --- a/Client/src/app/pages/module-version-edit/module-version-edit.component.ts +++ b/Client/src/app/pages/module-version-edit/module-version-edit.component.ts @@ -105,7 +105,11 @@ export class ModuleVersionEditComponent extends ProposalBaseComponent { degreeProgramAssignments }; this.moduleVersionService.updateModuleVersion(this.moduleVersionId, payload).subscribe({ - next: (response: ModuleVersionViewDTO) => this.moduleVersionDto.set(response), + next: (response: ModuleVersionViewDTO) => { + this.moduleVersionDto.set(response); + // Step-1 changes can invalidate previous feedbacks; refresh so the UI reflects it. + this.fetchPreviousModuleVersionFeedback(this.moduleVersionId); + }, error: (err: HttpErrorResponse) => this.error.set(err.error), complete: () => this.loading.set(false) }); diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.html b/Client/src/app/pages/module-version-view/module-version-view.component.html index 0c274e5c..7603a9f3 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.html +++ b/Client/src/app/pages/module-version-view/module-version-view.component.html @@ -86,10 +86,7 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ @if (currentStepIndex() === 1) { -

- First submission: request feedback from program coordinators and area coordinators (one per chosen degree program/area). Feedback is tied to the module version that was - submitted (immutable). When viewing the latest version, status below is from the previous submitted version. -

+

First submission: request feedback from program coordinators and area coordinators (one per chosen degree program/area).

@if (coordinatorFeedbacksForStep1().feedbacks.length > 0) {

Coordinator feedback status {{ coordinatorFeedbacksForStep1().fromPrevious ? '(from previous version)' : '(this version)' }}

diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.html b/Client/src/app/pages/proposal-view/proposal-view.component.html index 5f5adeb4..f7f0cb83 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.html +++ b/Client/src/app/pages/proposal-view/proposal-view.component.html @@ -72,10 +72,16 @@

Current Module Versions

@if (proposal.latestModuleVersion!.feedbackList!.length) { @for (feedback of proposal.latestModuleVersion!.feedbackList; track feedback.requiredRole) {
} } @@ -129,10 +135,16 @@

Previous Module Versions

} } diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java index e617eedb..1422ee1c 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/FeedbackRepository.java @@ -12,10 +12,10 @@ @Repository public interface FeedbackRepository extends JpaRepository { - List findByRequiredRoleInAndStatus(Collection requiredRoles, FeedbackStatus status); + List findByRequiredRoleInAndStatusAndInvalidatedFalse(Collection requiredRoles, FeedbackStatus status); /** Feedbacks for specializations that this user is currently responsible for (position-based). */ - List findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatus(UUID userId, FeedbackStatus status); + List findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatusAndInvalidatedFalse(UUID userId, FeedbackStatus status); /** All feedbacks for a proposal that are not invalidated (for display on view/edit). */ List findByModuleVersion_Proposal_ProposalIdAndInvalidatedFalse(Long proposalId); diff --git a/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java b/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java index 80e70625..4162203f 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/FeedbackService.java @@ -71,10 +71,10 @@ public Feedback RejectFeedback(Long feedbackId, User user, @NotBlank String comm public List getAllFeedbacksForUser(User user) { List roleBased = user.getRoles() != null && !user.getRoles().isEmpty() - ? feedbackRepository.findByRequiredRoleInAndStatus(user.getRoles(), FeedbackStatus.PENDING_FEEDBACK) + ? feedbackRepository.findByRequiredRoleInAndStatusAndInvalidatedFalse(user.getRoles(), FeedbackStatus.PENDING_FEEDBACK) : Collections.emptyList(); List bySpecialization = feedbackRepository - .findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatus(user.getUserId(), + .findByDegreeProgramSpecialization_ResponsibleUser_UserIdAndStatusAndInvalidatedFalse(user.getUserId(), FeedbackStatus.PENDING_FEEDBACK); return Stream.of(roleBased.stream(), bySpecialization.stream()) .flatMap(s -> s) @@ -99,6 +99,9 @@ private boolean canUserRespondToFeedback(Feedback feedback, User user) { private Feedback getPendingFeedback(Long feedbackId) { Feedback feedback = feedbackRepository.findById(feedbackId) .orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); + if (feedback.isInvalidated()) { + throw new IllegalStateException("This feedback has been invalidated."); + } if (feedback.getStatus() != FeedbackStatus.PENDING_FEEDBACK) { throw new IllegalStateException("This module is not " + FeedbackStatus.PENDING_FEEDBACK); } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index f4fb8909..4e0676fb 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -23,6 +23,7 @@ import modulemanagement.ls1.repositories.ProposalRepository; import modulemanagement.ls1.shared.PdfCreator; import modulemanagement.ls1.shared.ResourceNotFoundException; +import modulemanagement.ls1.shared.ModuleVersionStepsChangeDetector; import org.springframework.core.io.Resource; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; @@ -78,81 +79,36 @@ public ModuleVersionViewDTO updateModuleVersionFromRequest(UUID userId, Long mod throw new ResponseStatusException(HttpStatus.CONFLICT, "Cannot update ModuleVersion with feedback."); } + boolean step1Changed = ModuleVersionStepsChangeDetector.isStep1DataChanged(request, mv); + applyUpdateRequest(mv, request); + + if (step1Changed) { + invalidateActiveFeedbacksAndResetStatuses(mv); + } + mv = moduleVersionRepository.save(mv); return ModuleVersionViewDTO.from(mv); } - /** - * True if any step-1 field (basic info + assignments) differs between request - * and mv. - */ - // private boolean isStep1DataChanged(ModuleVersionUpdateRequestDTO request, - // ModuleVersion mv) { - // if (!Objects.equals(nullToEmpty(request.getTitleEng()), - // nullToEmpty(mv.getTitleEng()))) - // return true; - // if (!Objects.equals(nullToEmpty(request.getTitleDe()), - // nullToEmpty(mv.getTitleDe()))) - // return true; - // if (!Objects.equals(request.getCredits(), mv.getCredits())) - // return true; - // if (!Objects.equals(nullToEmpty(request.getFrequencyEng()), - // nullToEmpty(mv.getFrequencyEng()))) - // return true; - // if (!Objects.equals(request.getHoursLecture(), mv.getHoursLecture())) - // return true; - // if (!Objects.equals(request.getHoursExercise(), mv.getHoursExercise())) - // return true; - // if (!Objects.equals(request.getHoursPractical(), mv.getHoursPractical())) - // return true; - // if (!Objects.equals(request.getHoursSeminar(), mv.getHoursSeminar())) - // return true; - // if (!Objects.equals(nullToEmpty(request.getFirstSemesterAvailable()), - // nullToEmpty(mv.getFirstSemesterAvailable()))) - // return true; - // if (!Objects.equals(nullToEmpty(request.getSuccessorModuleName()), - // nullToEmpty(mv.getSuccessorModuleName()))) - // return true; - // if (!Objects.equals(request.getLanguageEng(), mv.getLanguageEng())) - // return true; - // if (request.getDegreeProgramAssignments() != null - // && !assignmentSetEquals(request.getDegreeProgramAssignments(), - // mv.getDegreeProgramAssignments())) { - // return true; - // } - // return false; - // } - - // private static String nullToEmpty(String s) { - // return s == null ? "" : s.trim(); - // } - - // private static boolean - // assignmentSetEquals(List requestList, - // List mvList) { - // Set requestSet = new HashSet<>(); - // if (requestList != null) { - // for (ModuleDegreeProgramAssignmentDTO a : requestList) { - // if (a != null && a.getDegreeProgramId() != null && - // a.getDegreeProgramSpecializationId() != null) { - // requestSet.add(a.getDegreeProgramId() + "," + - // a.getDegreeProgramSpecializationId()); - // } - // } - // } - // Set mvSet = new HashSet<>(); - // if (mvList != null) { - // for (ModuleVersionDegreeProgramAssignment a : mvList) { - // if (a.getDegreeProgram() != null && a.getDegreeProgramSpecialization() != - // null) { - // mvSet.add(a.getDegreeProgram().getDegreeProgramId() + "," - // + a.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); - // } - // } - // } - // return requestSet.equals(mvSet); - // } + private void invalidateActiveFeedbacksAndResetStatuses(ModuleVersion mv) { + Proposal proposal = mv.getProposal(); + + List activeFeedbacks = feedbackRepository + .findByModuleVersion_Proposal_ProposalIdAndInvalidatedFalse(proposal.getProposalId()); + if (activeFeedbacks == null || activeFeedbacks.isEmpty()) { + return; + } + + for (Feedback f : activeFeedbacks) { + f.setInvalidated(true); + } + feedbackRepository.saveAll(activeFeedbacks); + + proposal.setStatus(ProposalStatus.PENDING_FIRST_SUBMISSION); + mv.setStatus(ModuleVersionStatus.PENDING_FIRST_SUBMISSION); + proposalRepository.save(proposal); + } /** * Applies request content and degree program assignments to the given module @@ -239,12 +195,16 @@ public void updateStatus(Long moduleVersionId) { } List allFeedbacks = mv.getRequiredFeedbacks() != null ? mv.getRequiredFeedbacks() : new ArrayList<>(); + // Invalidated feedbacks must not affect proposal/module version status. + List nonInvalidatedFeedbacks = allFeedbacks.stream() + .filter(f -> f != null && !f.isInvalidated()) + .toList(); // Coordinator feedbacks for current assignments + all role-based feedbacks (we // only have one active set of pending at a time). - List coordinatorFeedbacks = allFeedbacks.stream() + List coordinatorFeedbacks = nonInvalidatedFeedbacks.stream() .filter(f -> f.getDegreeProgramSpecialization() != null) .toList(); - List roleBased = allFeedbacks.stream() + List roleBased = nonInvalidatedFeedbacks.stream() .filter(f -> f.getRequiredRole() != null) .toList(); List feedbacksToEvaluate = new ArrayList<>(coordinatorFeedbacks); diff --git a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java index be2ab559..2048c687 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java @@ -230,7 +230,7 @@ public ProposalViewDTO requestFullFeedback(Long proposalId, UUID userId) { List requiredFeedbacks = mv.getRequiredFeedbacks() != null ? mv.getRequiredFeedbacks() : new ArrayList<>(); List coordinatorFeedbacks = requiredFeedbacks.stream() - .filter(f -> f.getDegreeProgramSpecialization() != null) + .filter(f -> f.getDegreeProgramSpecialization() != null && !f.isInvalidated()) .toList(); if (coordinatorFeedbacks.isEmpty()) { throw new IllegalStateException( diff --git a/Server/src/main/java/modulemanagement/ls1/shared/ModuleVersionStepsChangeDetector.java b/Server/src/main/java/modulemanagement/ls1/shared/ModuleVersionStepsChangeDetector.java new file mode 100644 index 00000000..13d5840c --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/shared/ModuleVersionStepsChangeDetector.java @@ -0,0 +1,82 @@ +package modulemanagement.ls1.shared; + +import modulemanagement.ls1.dtos.ModuleDegreeProgramAssignmentDTO; +import modulemanagement.ls1.dtos.ModuleVersionUpdateRequestDTO; +import modulemanagement.ls1.models.ModuleVersion; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class ModuleVersionStepsChangeDetector { + + private ModuleVersionStepsChangeDetector() { + } + + public static boolean isStep1DataChanged(ModuleVersionUpdateRequestDTO request, ModuleVersion mv) { + // Step-1 (basic + assignment) fields. + if (!Objects.equals(normalize(request.getTitleEng()), normalize(mv.getTitleEng()))) + return true; + if (!Objects.equals(normalize(request.getTitleDe()), normalize(mv.getTitleDe()))) + return true; + if (!Objects.equals(request.getCredits(), mv.getCredits())) + return true; + if (!Objects.equals(normalize(request.getFrequencyEng()), normalize(mv.getFrequencyEng()))) + return true; + if (!Objects.equals(request.getHoursLecture(), mv.getHoursLecture())) + return true; + if (!Objects.equals(request.getHoursExercise(), mv.getHoursExercise())) + return true; + if (!Objects.equals(request.getHoursPractical(), mv.getHoursPractical())) + return true; + if (!Objects.equals(request.getHoursSeminar(), mv.getHoursSeminar())) + return true; + if (!Objects.equals(normalize(request.getFirstSemesterAvailable()), normalize(mv.getFirstSemesterAvailable()))) + return true; + if (!Objects.equals(normalize(request.getSuccessorModuleName()), normalize(mv.getSuccessorModuleName()))) + return true; + if (!Objects.equals(request.getLanguageEng(), mv.getLanguageEng())) + return true; + + Set requestAssignments = assignmentKeySetFromRequest(request.getDegreeProgramAssignments()); + Set mvAssignments = assignmentKeySetFromMv(mv); + return !requestAssignments.equals(mvAssignments); + } + + private static String normalize(String s) { + if (s == null) + return null; + String t = s.trim(); + return t.isEmpty() ? null : t; + } + + private static Set assignmentKeySetFromRequest(List assignments) { + Set set = new HashSet<>(); + if (assignments == null) { + return set; + } + for (ModuleDegreeProgramAssignmentDTO a : assignments) { + if (a == null || a.getDegreeProgramId() == null || a.getDegreeProgramSpecializationId() == null) { + continue; + } + set.add(a.getDegreeProgramId() + "," + a.getDegreeProgramSpecializationId()); + } + return set; + } + + private static Set assignmentKeySetFromMv(ModuleVersion mv) { + Set set = new HashSet<>(); + if (mv.getDegreeProgramAssignments() == null) { + return set; + } + for (var a : mv.getDegreeProgramAssignments()) { + if (a == null || a.getDegreeProgram() == null || a.getDegreeProgramSpecialization() == null) { + continue; + } + set.add(a.getDegreeProgram().getDegreeProgramId() + "," + + a.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); + } + return set; + } +} From f7c20a92b89dbe2f64506220f944e4f597756519 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Wed, 18 Mar 2026 22:57:26 +0100 Subject: [PATCH 11/42] fix --- .../create-edit-base.component.ts | 12 ++++++++++++ .../proposal-view/proposal-view.component.html | 4 ++-- Client/src/app/pipes/feedbackStatus.pipe.ts | 18 ++++++------------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.ts b/Client/src/app/components/create-edit-base/create-edit-base.component.ts index 4e6c96aa..5a53ed85 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.ts +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.ts @@ -265,6 +265,12 @@ export abstract class ProposalBaseComponent { // When backend created a new version (immutable versioning), switch to editing the new version const newId = response?.latestModuleVersion?.moduleVersionId; if (newId != null && newId !== this.moduleVersionId) { + + this.moduleVersionService.getPreviousModuleVersionFeedback(newId).subscribe({ + next: (feedbacks) => this.feedbacks.set([...feedbacks]), + error: (err: HttpErrorResponse) => this.error.set(err.error) + }); + this.moduleVersionId = newId; this.breadcrumbLabels.versionLabel.set(response?.latestVersion != null ? `Version ${response.latestVersion}` : null); this.router.navigate(['/proposals', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); @@ -290,6 +296,12 @@ export abstract class ProposalBaseComponent { // When backend created a new version (immutable versioning), switch to editing the new version const newId = response?.latestModuleVersion?.moduleVersionId; if (newId != null && newId !== this.moduleVersionId) { + // Refresh feedbacks so the next step reflects statuses from the freezed version. + this.moduleVersionService.getPreviousModuleVersionFeedback(newId).subscribe({ + next: (feedbacks) => this.feedbacks.set([...feedbacks]), + error: (err: HttpErrorResponse) => this.error.set(err.error) + }); + this.moduleVersionId = newId; this.breadcrumbLabels.versionLabel.set(response?.latestVersion != null ? `Version ${response.latestVersion}` : null); this.router.navigate(['/proposals', proposalId, 'version', newId, 'edit'], { replaceUrl: true }); diff --git a/Client/src/app/pages/proposal-view/proposal-view.component.html b/Client/src/app/pages/proposal-view/proposal-view.component.html index f7f0cb83..fa586d8f 100644 --- a/Client/src/app/pages/proposal-view/proposal-view.component.html +++ b/Client/src/app/pages/proposal-view/proposal-view.component.html @@ -81,7 +81,7 @@

Current Module Versions

" tooltipPosition="top" class="w-8 h-8 mr-2 rounded" - [ngClass]="feedback.invalidated ? (feedback.status | feedbackStatus).fadedColor : (feedback.status | feedbackStatus).normalColor" + [ngClass]="feedback.invalidated ? 'bg-gray-300 text-white' : (feedback.status | feedbackStatus).color" >

} } @@ -144,7 +144,7 @@

Previous Module Versions

} } diff --git a/Client/src/app/pipes/feedbackStatus.pipe.ts b/Client/src/app/pipes/feedbackStatus.pipe.ts index 3b78d102..73b194ca 100644 --- a/Client/src/app/pipes/feedbackStatus.pipe.ts +++ b/Client/src/app/pipes/feedbackStatus.pipe.ts @@ -6,45 +6,39 @@ import { Tag } from 'primeng/tag'; export class FeedbackStatusPipe implements PipeTransform { transform(status: Feedback.StatusEnum | ModuleVersionViewFeedbackDTO.FeedbackStatusEnum): { text: string; - normalColor: string; - fadedColor: string; + color: string; severity: Tag['severity']; } { switch (status) { case 'PENDING_FEEDBACK': return { text: 'Pending Feedback', - normalColor: 'bg-yellow-500 text-white', - fadedColor: 'bg-yellow-300 text-white', + color: 'bg-yellow-500 text-white', severity: 'warn' }; case 'APPROVED': return { text: 'Approved', - normalColor: 'bg-green-500 text-white', - fadedColor: 'bg-green-300 text-white', + color: 'bg-green-500 text-white', severity: 'success' }; case 'FEEDBACK_GIVEN': return { text: 'Feedback given', - normalColor: 'bg-blue-500 text-white', - fadedColor: 'bg-blue-300 text-white', + color: 'bg-blue-500 text-white', severity: 'info' }; case 'REJECTED': return { text: 'Rejected', - normalColor: 'bg-red-500 text-white', - fadedColor: 'bg-red-300 text-white', + color: 'bg-red-500 text-white', severity: 'danger' }; default: return { text: status, - normalColor: 'bg-gray-400 text-white', - fadedColor: 'bg-gray-300 text-white', + color: 'bg-gray-400 text-white', severity: 'secondary' }; } From 54500e7917bb19e5cc1656345e5ecf4510ce6850 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Thu, 19 Mar 2026 23:56:48 +0100 Subject: [PATCH 12/42] imrpove feedback view --- .../core/modules/openapi/model/feedback.ts | 1 + .../module-degree-program-assignment-dto.ts | 2 + .../model/module-version-view-feedback-dto.ts | 1 + .../feedback-view.component.html | 45 +++++++++++++++++++ .../feedback-view/feedback-view.component.ts | 33 +++++++++++++- .../ModuleDegreeProgramAssignmentDTO.java | 2 + .../ls1/dtos/ModuleVersionViewDTO.java | 2 + .../dtos/ModuleVersionViewFeedbackDTO.java | 2 + .../modulemanagement/ls1/models/Feedback.java | 3 ++ .../ls1/services/ProposalService.java | 2 + .../0012_feedback_requested_from_user.yaml | 5 +++ 11 files changed, 96 insertions(+), 2 deletions(-) diff --git a/Client/src/app/core/modules/openapi/model/feedback.ts b/Client/src/app/core/modules/openapi/model/feedback.ts index 85d9d062..e54237e1 100644 --- a/Client/src/app/core/modules/openapi/model/feedback.ts +++ b/Client/src/app/core/modules/openapi/model/feedback.ts @@ -17,6 +17,7 @@ export interface Feedback { requiredRole?: Feedback.RequiredRoleEnum; status: Feedback.StatusEnum; submissionDate?: string; + createdAt?: string; titleFeedback?: string; titleAccepted?: boolean; levelFeedback?: string; diff --git a/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts b/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts index 7ce6a604..3c1ae253 100644 --- a/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-degree-program-assignment-dto.ts @@ -12,5 +12,7 @@ export interface ModuleDegreeProgramAssignmentDTO { degreeProgramId: number; degreeProgramSpecializationId: number; + degreeProgramName?: string; + degreeProgramSpecializationName?: string; } diff --git a/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts index c420ed0a..075ba0bd 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts @@ -17,6 +17,7 @@ export interface ModuleVersionViewFeedbackDTO { feedbackRole?: ModuleVersionViewFeedbackDTO.FeedbackRoleEnum; requestedFromUserName?: string; feedbackStatus?: ModuleVersionViewFeedbackDTO.FeedbackStatusEnum; + createdAt?: string; submissionDate?: string; degreeProgramSpecializationId?: number; titleFeedback?: string; diff --git a/Client/src/app/pages/feedback-view/feedback-view.component.html b/Client/src/app/pages/feedback-view/feedback-view.component.html index dad9676a..9a2faedd 100644 --- a/Client/src/app/pages/feedback-view/feedback-view.component.html +++ b/Client/src/app/pages/feedback-view/feedback-view.component.html @@ -20,6 +20,51 @@

Module Proposal for '{{ moduleVersion.titleEng }}'

+ + @if (currentFeedback(); as fb) { +
+
+
+

Feedback Status

+ +
+ +
+

Requested From

+

{{ (fb.feedbackRole | feedbackDepartment).text }} {{ fb.requestedFromUserName ?? 'N/A' }}

+
+
+ + @if (fb.createdAt) { +

Requested at: {{ fb.createdAt | date: 'yyyy-MM-dd HH:mm' }}

+ } + + @if (fb.submissionDate) { +

Submitted at: {{ fb.submissionDate | date: 'yyyy-MM-dd HH:mm' }}

+ } + + @if (fb.rejectionComment) { +
+

Rejection Comment

+

{{ fb.rejectionComment }}

+
+ } +
+ } + + +
+

Assignments

+
    + @for (a of moduleVersion.degreeProgramAssignments ?? []; track a.degreeProgramSpecializationId) { +
  • + {{ a.degreeProgramName ?? 'Unknown Program' }} + {{ a.degreeProgramSpecializationName ?? 'Unknown Specialization' }} +
  • + } +
+
+
@for (field of moduleFields; track field.key) { diff --git a/Client/src/app/pages/feedback-view/feedback-view.component.ts b/Client/src/app/pages/feedback-view/feedback-view.component.ts index 9b18779f..cb9ec894 100644 --- a/Client/src/app/pages/feedback-view/feedback-view.component.ts +++ b/Client/src/app/pages/feedback-view/feedback-view.component.ts @@ -1,22 +1,47 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Component, inject, signal } from '@angular/core'; -import { FeedbackControllerService, ModuleVersionViewDTO, FeedbackDTO, GiveFeedbackDTO, ModuleVersionControllerService } from '../../core/modules/openapi'; +import { + FeedbackControllerService, + ModuleVersionViewDTO, + FeedbackDTO, + GiveFeedbackDTO, + ModuleVersionControllerService, + ModuleVersionViewFeedbackDTO +} from '../../core/modules/openapi'; import { BreadcrumbLabelsService } from '../../components/breadcrumb/breadcrumb-labels.service'; import { FormBuilder, FormGroup, FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; +import { DatePipe } from '@angular/common'; import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; +import { TagModule } from 'primeng/tag'; import { TextareaModule } from 'primeng/textarea'; import { DialogModule } from 'primeng/dialog'; import { ToastModule } from 'primeng/toast'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { TooltipModule } from 'primeng/tooltip'; import { MessageModule } from 'primeng/message'; +import { FeedbackStatusPipe } from '../../pipes/feedbackStatus.pipe'; +import { FeedbackDepartmentPipe } from '../../pipes/feedbackDepartment.pipe'; @Component({ selector: 'app-feedback-view', standalone: true, - imports: [FormsModule, RouterModule, ButtonModule, TextareaModule, DialogModule, ToastModule, ProgressSpinnerModule, TooltipModule, MessageModule], + imports: [ + FormsModule, + RouterModule, + ButtonModule, + TagModule, + TextareaModule, + DialogModule, + ToastModule, + ProgressSpinnerModule, + TooltipModule, + MessageModule, + FeedbackStatusPipe, + FeedbackDepartmentPipe, + DatePipe + ], templateUrl: './feedback-view.component.html' }) export class FeedbackViewComponent { @@ -28,6 +53,7 @@ export class FeedbackViewComponent { feedbackForm: FormGroup; feedbackId: number | null = null; moduleVersion = signal(null); + currentFeedback = signal(null); loading = signal(true); error = signal(null); rejectionReason: string = ''; @@ -124,6 +150,9 @@ export class FeedbackViewComponent { this.feedbackService.getModuleVersionOfFeedback(feedbackId).subscribe({ next: (response: ModuleVersionViewDTO) => { this.moduleVersion.set(response); + const found = + response.feedbacks?.find((f) => f.feedbackId === feedbackId) ?? null; + this.currentFeedback.set(found); this.breadcrumbLabels.feedbackLabel.set(response?.titleEng ?? null); }, error: (err: HttpErrorResponse) => this.error.set(err.error), diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java index 36c0682e..8aa04e81 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleDegreeProgramAssignmentDTO.java @@ -9,4 +9,6 @@ public class ModuleDegreeProgramAssignmentDTO { private Long degreeProgramId; @NotNull private Long degreeProgramSpecializationId; + private String degreeProgramName; + private String degreeProgramSpecializationName; } diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java index 2a97beca..6920e441 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewDTO.java @@ -80,8 +80,10 @@ public static ModuleVersionViewDTO from(ModuleVersion moduleVersion) { .map(a -> { ModuleDegreeProgramAssignmentDTO item = new ModuleDegreeProgramAssignmentDTO(); item.setDegreeProgramId(a.getDegreeProgram().getDegreeProgramId()); + item.setDegreeProgramName(a.getDegreeProgram().getName()); item.setDegreeProgramSpecializationId( a.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); + item.setDegreeProgramSpecializationName(a.getDegreeProgramSpecialization().getName()); return item; }) .collect(Collectors.toList()); diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java index f25df9af..8e8f2cd7 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java @@ -18,6 +18,7 @@ public class ModuleVersionViewFeedbackDTO { private UserRole feedbackRole; private String requestedFromUserName; private FeedbackStatus feedbackStatus; + private LocalDateTime createdAt; private LocalDateTime submissionDate; private Long degreeProgramSpecializationId; @@ -57,6 +58,7 @@ public static ModuleVersionViewFeedbackDTO from(Feedback f) { + f.getDegreeProgramSpecialization().getName() + ")"); } dto.setFeedbackStatus(f.getStatus()); + dto.setCreatedAt(f.getCreatedAt()); dto.setSubmissionDate(f.getSubmissionDate()); if (f.getDegreeProgramSpecialization() != null) { dto.setDegreeProgramSpecializationId(f.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); diff --git a/Server/src/main/java/modulemanagement/ls1/models/Feedback.java b/Server/src/main/java/modulemanagement/ls1/models/Feedback.java index 3d01d696..48c66652 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/Feedback.java +++ b/Server/src/main/java/modulemanagement/ls1/models/Feedback.java @@ -51,6 +51,9 @@ public class Feedback { @Column(name = "submission_date") private LocalDateTime submissionDate; + @Column(name = "created_at") + private LocalDateTime createdAt; + @ManyToOne @JoinColumn(name = "module_version_id", nullable = false) @JsonIgnore diff --git a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java index 2048c687..71f39853 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java @@ -180,6 +180,7 @@ public ProposalViewDTO requestCoordinatorsFeedback(Long proposalId, UUID userId) var spec = assignment.getDegreeProgramSpecialization(); Feedback feedback = new Feedback(); feedback.setStatus(FeedbackStatus.PENDING_FEEDBACK); + feedback.setCreatedAt(LocalDateTime.now()); feedback.setDegreeProgramSpecialization(spec); feedback.setRequiredRole(null); feedback.setModuleVersion(mv); @@ -247,6 +248,7 @@ public ProposalViewDTO requestFullFeedback(Long proposalId, UUID userId) { for (UserRole role : FULL_FEEDBACK_ROLES) { Feedback feedback = new Feedback(); feedback.setStatus(FeedbackStatus.PENDING_FEEDBACK); + feedback.setCreatedAt(LocalDateTime.now()); feedback.setRequiredRole(role); feedback.setDegreeProgramSpecialization(null); feedback.setModuleVersion(mv); diff --git a/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml b/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml index 8d273cd0..74c3e56d 100644 --- a/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml +++ b/Server/src/main/resources/db/changelog/changes/0012_feedback_requested_from_user.yaml @@ -19,6 +19,11 @@ databaseChangeLog: constraints: nullable: false defaultValueBoolean: false + - column: + name: created_at + type: TIMESTAMP + constraints: + nullable: true - dropNotNullConstraint: tableName: feedback From d881878f389c0e2e2c609280c034782f4903770d Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Thu, 19 Mar 2026 23:59:46 +0100 Subject: [PATCH 13/42] fix --- .../ls1/services/ModuleVersionService.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index 4e0676fb..8a9769db 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -18,7 +18,6 @@ import modulemanagement.ls1.models.User; import modulemanagement.ls1.repositories.DegreeProgramRepository; import modulemanagement.ls1.repositories.FeedbackRepository; -import modulemanagement.ls1.repositories.ModuleVersionDegreeProgramAssignmentRepository; import modulemanagement.ls1.repositories.ModuleVersionRepository; import modulemanagement.ls1.repositories.ProposalRepository; import modulemanagement.ls1.shared.PdfCreator; @@ -32,7 +31,6 @@ import org.springframework.web.server.ResponseStatusException; import jakarta.persistence.EntityManager; - import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -41,21 +39,18 @@ @Service public class ModuleVersionService { private final ModuleVersionRepository moduleVersionRepository; - private final ModuleVersionDegreeProgramAssignmentRepository assignmentRepository; private final DegreeProgramRepository degreeProgramRepository; private final ProposalRepository proposalRepository; private final OverlapDetectionService overlapDetectionService; private final PdfCreator pdfCreator; - private final EntityManager entityManager; private final FeedbackRepository feedbackRepository; + private final EntityManager entityManager; public ModuleVersionService(ModuleVersionRepository moduleVersionRepository, - ModuleVersionDegreeProgramAssignmentRepository assignmentRepository, DegreeProgramRepository degreeProgramRepository, ProposalRepository proposalRepository, OverlapDetectionService overlapDetectionService, PdfCreator pdfCreator, - EntityManager entityManager, FeedbackRepository feedbackRepository) { + FeedbackRepository feedbackRepository, EntityManager entityManager) { this.moduleVersionRepository = moduleVersionRepository; - this.assignmentRepository = assignmentRepository; this.degreeProgramRepository = degreeProgramRepository; this.proposalRepository = proposalRepository; this.overlapDetectionService = overlapDetectionService; @@ -159,7 +154,6 @@ private void applyUpdateRequest(ModuleVersion mv, ModuleVersionUpdateRequestDTO } } - assignmentRepository.deleteByModuleVersion_ModuleVersionId(mv.getModuleVersionId()); mv.getDegreeProgramAssignments().clear(); entityManager.flush(); for (ModuleDegreeProgramAssignmentDTO item : request.getDegreeProgramAssignments()) { From 66637dd583b8f829f57c01cae429a073041b7c0e Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Fri, 20 Mar 2026 00:07:04 +0100 Subject: [PATCH 14/42] display new module fields in feedback --- .../modules/openapi/model/feedback-dto.ts | 24 ++++ .../core/modules/openapi/model/feedback.ts | 24 ++++ .../model/module-version-view-feedback-dto.ts | 12 ++ .../feedback-view/feedback-view.component.ts | 36 ++++++ .../ls1/dtos/FeedbackDTO.java | 24 ++++ .../dtos/ModuleVersionViewFeedbackDTO.java | 24 ++++ .../modulemanagement/ls1/models/Feedback.java | 108 ++++++++++++++++++ .../ls1/services/ModuleVersionService.java | 2 +- .../0014_feedback_additional_fields.yaml | 92 +++++++++++++++ .../main/resources/db/changelog/master.yaml | 3 + 10 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 Server/src/main/resources/db/changelog/changes/0014_feedback_additional_fields.yaml diff --git a/Client/src/app/core/modules/openapi/model/feedback-dto.ts b/Client/src/app/core/modules/openapi/model/feedback-dto.ts index a0cb5186..6d604a17 100644 --- a/Client/src/app/core/modules/openapi/model/feedback-dto.ts +++ b/Client/src/app/core/modules/openapi/model/feedback-dto.ts @@ -12,6 +12,10 @@ export interface FeedbackDTO { titleFeedback?: string; titleAccepted?: boolean; + titleDeFeedback?: string; + titleDeAccepted?: boolean; + bulletPointsFeedback?: string; + bulletPointsAccepted?: boolean; levelFeedback?: string; levelAccepted?: boolean; languageFeedback?: string; @@ -28,18 +32,38 @@ export interface FeedbackDTO { hoursSelfStudyAccepted?: boolean; hoursPresenceFeedback?: string; hoursPresenceAccepted?: boolean; + hoursLectureFeedback?: string; + hoursLectureAccepted?: boolean; + hoursExerciseFeedback?: string; + hoursExerciseAccepted?: boolean; + hoursPracticalFeedback?: string; + hoursPracticalAccepted?: boolean; + hoursSeminarFeedback?: string; + hoursSeminarAccepted?: boolean; + firstSemesterAvailableFeedback?: string; + firstSemesterAvailableAccepted?: boolean; + successorModuleNameFeedback?: string; + successorModuleNameAccepted?: boolean; examinationAchievementsFeedback?: string; examinationAchievementsAccepted?: boolean; + examinationAchievementsPromptFeedback?: string; + examinationAchievementsPromptAccepted?: boolean; repetitionFeedback?: string; repetitionAccepted?: boolean; recommendedPrerequisitesFeedback?: string; recommendedPrerequisitesAccepted?: boolean; contentFeedback?: string; contentAccepted?: boolean; + contentPromptFeedback?: string; + contentPromptAccepted?: boolean; learningOutcomesFeedback?: string; learningOutcomesAccepted?: boolean; + learningOutcomesPromptFeedback?: string; + learningOutcomesPromptAccepted?: boolean; teachingMethodsFeedback?: string; teachingMethodsAccepted?: boolean; + teachingMethodsPromptFeedback?: string; + teachingMethodsPromptAccepted?: boolean; mediaFeedback?: string; mediaAccepted?: boolean; literatureFeedback?: string; diff --git a/Client/src/app/core/modules/openapi/model/feedback.ts b/Client/src/app/core/modules/openapi/model/feedback.ts index e54237e1..e89e2cd3 100644 --- a/Client/src/app/core/modules/openapi/model/feedback.ts +++ b/Client/src/app/core/modules/openapi/model/feedback.ts @@ -20,6 +20,10 @@ export interface Feedback { createdAt?: string; titleFeedback?: string; titleAccepted?: boolean; + titleDeFeedback?: string; + titleDeAccepted?: boolean; + bulletPointsFeedback?: string; + bulletPointsAccepted?: boolean; levelFeedback?: string; levelAccepted?: boolean; languageFeedback?: string; @@ -36,18 +40,38 @@ export interface Feedback { hoursSelfStudyAccepted?: boolean; hoursPresenceFeedback?: string; hoursPresenceAccepted?: boolean; + hoursLectureFeedback?: string; + hoursLectureAccepted?: boolean; + hoursExerciseFeedback?: string; + hoursExerciseAccepted?: boolean; + hoursPracticalFeedback?: string; + hoursPracticalAccepted?: boolean; + hoursSeminarFeedback?: string; + hoursSeminarAccepted?: boolean; + firstSemesterAvailableFeedback?: string; + firstSemesterAvailableAccepted?: boolean; + successorModuleNameFeedback?: string; + successorModuleNameAccepted?: boolean; examinationAchievementsFeedback?: string; examinationAchievementsAccepted?: boolean; + examinationAchievementsPromptFeedback?: string; + examinationAchievementsPromptAccepted?: boolean; repetitionFeedback?: string; repetitionAccepted?: boolean; recommendedPrerequisitesFeedback?: string; recommendedPrerequisitesAccepted?: boolean; contentFeedback?: string; contentAccepted?: boolean; + contentPromptFeedback?: string; + contentPromptAccepted?: boolean; learningOutcomesFeedback?: string; learningOutcomesAccepted?: boolean; + learningOutcomesPromptFeedback?: string; + learningOutcomesPromptAccepted?: boolean; teachingMethodsFeedback?: string; teachingMethodsAccepted?: boolean; + teachingMethodsPromptFeedback?: string; + teachingMethodsPromptAccepted?: boolean; mediaFeedback?: string; mediaAccepted?: boolean; literatureFeedback?: string; diff --git a/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts index 075ba0bd..3f067f8d 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-view-feedback-dto.ts @@ -21,6 +21,8 @@ export interface ModuleVersionViewFeedbackDTO { submissionDate?: string; degreeProgramSpecializationId?: number; titleFeedback?: string; + titleDeFeedback?: string; + bulletPointsFeedback?: string; levelFeedback?: string; languageFeedback?: string; frequencyFeedback?: string; @@ -29,12 +31,22 @@ export interface ModuleVersionViewFeedbackDTO { hoursTotalFeedback?: string; hoursSelfStudyFeedback?: string; hoursPresenceFeedback?: string; + hoursLectureFeedback?: string; + hoursExerciseFeedback?: string; + hoursPracticalFeedback?: string; + hoursSeminarFeedback?: string; + firstSemesterAvailableFeedback?: string; + successorModuleNameFeedback?: string; examinationAchievementsFeedback?: string; + examinationAchievementsPromptFeedback?: string; repetitionFeedback?: string; recommendedPrerequisitesFeedback?: string; contentFeedback?: string; + contentPromptFeedback?: string; learningOutcomesFeedback?: string; + learningOutcomesPromptFeedback?: string; teachingMethodsFeedback?: string; + teachingMethodsPromptFeedback?: string; mediaFeedback?: string; literatureFeedback?: string; responsiblesFeedback?: string; diff --git a/Client/src/app/pages/feedback-view/feedback-view.component.ts b/Client/src/app/pages/feedback-view/feedback-view.component.ts index cb9ec894..7a30d56f 100644 --- a/Client/src/app/pages/feedback-view/feedback-view.component.ts +++ b/Client/src/app/pages/feedback-view/feedback-view.component.ts @@ -73,20 +73,32 @@ export class FeedbackViewComponent { moduleFields = [ { key: 'titleEng', label: 'Title' }, + { key: 'titleDe', label: 'Title (German)' }, + { key: 'bulletPoints', label: 'Key Points' }, { key: 'levelEng', label: 'Level' }, { key: 'languageEng', label: 'Language' }, { key: 'frequencyEng', label: 'Frequency' }, { key: 'credits', label: 'Credits' }, + { key: 'hoursLecture', label: 'Hours (Lecture)' }, + { key: 'hoursExercise', label: 'Hours (Exercise)' }, + { key: 'hoursPractical', label: 'Hours (Practical)' }, + { key: 'hoursSeminar', label: 'Hours (Seminar)' }, + { key: 'firstSemesterAvailable', label: 'First Semester Available' }, + { key: 'successorModuleName', label: 'Successor Module Name' }, { key: 'duration', label: 'Duration' }, { key: 'hoursTotal', label: 'Total Hours' }, { key: 'hoursSelfStudy', label: 'Self-Study Hours' }, { key: 'hoursPresence', label: 'Presence Hours' }, { key: 'examinationAchievementsEng', label: 'Examination Achievements' }, + { key: 'examinationAchievementsPromptEng', label: 'Examination Prompt' }, { key: 'repetitionEng', label: 'Repetition' }, { key: 'recommendedPrerequisitesEng', label: 'Recommended Prerequisites' }, { key: 'contentEng', label: 'Content' }, + { key: 'contentPromptEng', label: 'Content Prompt' }, { key: 'learningOutcomesEng', label: 'Learning Outcomes' }, + { key: 'learningOutcomesPromptEng', label: 'Learning Outcomes Prompt' }, { key: 'teachingMethodsEng', label: 'Teaching Methods' }, + { key: 'teachingMethodsPromptEng', label: 'Teaching Methods Prompt' }, { key: 'mediaEng', label: 'Media' }, { key: 'literatureEng', label: 'Literature' }, { key: 'responsiblesEng', label: 'Responsibles' }, @@ -101,6 +113,10 @@ export class FeedbackViewComponent { this.feedbackForm = formBulider.group({ titleAccepted: [null], titleFeedback: [''], + titleDeAccepted: [null], + titleDeFeedback: [''], + bulletPointsAccepted: [null], + bulletPointsFeedback: [''], levelAccepted: [null], levelFeedback: [''], languageAccepted: [null], @@ -117,18 +133,38 @@ export class FeedbackViewComponent { hoursSelfStudyFeedback: [''], hoursPresenceAccepted: [null], hoursPresenceFeedback: [''], + hoursLectureAccepted: [null], + hoursLectureFeedback: [''], + hoursExerciseAccepted: [null], + hoursExerciseFeedback: [''], + hoursPracticalAccepted: [null], + hoursPracticalFeedback: [''], + hoursSeminarAccepted: [null], + hoursSeminarFeedback: [''], + firstSemesterAvailableAccepted: [null], + firstSemesterAvailableFeedback: [''], + successorModuleNameAccepted: [null], + successorModuleNameFeedback: [''], examinationAchievementsAccepted: [null], examinationAchievementsFeedback: [''], + examinationAchievementsPromptAccepted: [null], + examinationAchievementsPromptFeedback: [''], repetitionAccepted: [null], repetitionFeedback: [''], recommendedPrerequisitesAccepted: [null], recommendedPrerequisitesFeedback: [''], contentAccepted: [null], contentFeedback: [''], + contentPromptAccepted: [null], + contentPromptFeedback: [''], learningOutcomesAccepted: [null], learningOutcomesFeedback: [''], + learningOutcomesPromptAccepted: [null], + learningOutcomesPromptFeedback: [''], teachingMethodsAccepted: [null], teachingMethodsFeedback: [''], + teachingMethodsPromptAccepted: [null], + teachingMethodsPromptFeedback: [''], mediaAccepted: [null], mediaFeedback: [''], literatureAccepted: [null], diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/FeedbackDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/FeedbackDTO.java index 59b7a822..040b945e 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/FeedbackDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/FeedbackDTO.java @@ -6,6 +6,10 @@ public class FeedbackDTO { private String titleFeedback; private boolean titleAccepted; + private String titleDeFeedback; + private boolean titleDeAccepted; + private String bulletPointsFeedback; + private boolean bulletPointsAccepted; private String levelFeedback; private boolean levelAccepted; private String languageFeedback; @@ -22,18 +26,38 @@ public class FeedbackDTO { private boolean hoursSelfStudyAccepted; private String hoursPresenceFeedback; private boolean hoursPresenceAccepted; + private String hoursLectureFeedback; + private boolean hoursLectureAccepted; + private String hoursExerciseFeedback; + private boolean hoursExerciseAccepted; + private String hoursPracticalFeedback; + private boolean hoursPracticalAccepted; + private String hoursSeminarFeedback; + private boolean hoursSeminarAccepted; + private String firstSemesterAvailableFeedback; + private boolean firstSemesterAvailableAccepted; + private String successorModuleNameFeedback; + private boolean successorModuleNameAccepted; private String examinationAchievementsFeedback; private boolean examinationAchievementsAccepted; + private String examinationAchievementsPromptFeedback; + private boolean examinationAchievementsPromptAccepted; private String repetitionFeedback; private boolean repetitionAccepted; private String recommendedPrerequisitesFeedback; private boolean recommendedPrerequisitesAccepted; private String contentFeedback; private boolean contentAccepted; + private String contentPromptFeedback; + private boolean contentPromptAccepted; private String learningOutcomesFeedback; private boolean learningOutcomesAccepted; + private String learningOutcomesPromptFeedback; + private boolean learningOutcomesPromptAccepted; private String teachingMethodsFeedback; private boolean teachingMethodsAccepted; + private String teachingMethodsPromptFeedback; + private boolean teachingMethodsPromptAccepted; private String mediaFeedback; private boolean mediaAccepted; private String literatureFeedback; diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java index 8e8f2cd7..5b75e3f0 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ModuleVersionViewFeedbackDTO.java @@ -23,6 +23,8 @@ public class ModuleVersionViewFeedbackDTO { private Long degreeProgramSpecializationId; private String titleFeedback; + private String titleDeFeedback; + private String bulletPointsFeedback; private String levelFeedback; private String languageFeedback; private String frequencyFeedback; @@ -31,12 +33,22 @@ public class ModuleVersionViewFeedbackDTO { private String hoursTotalFeedback; private String hoursSelfStudyFeedback; private String hoursPresenceFeedback; + private String hoursLectureFeedback; + private String hoursExerciseFeedback; + private String hoursPracticalFeedback; + private String hoursSeminarFeedback; + private String firstSemesterAvailableFeedback; + private String successorModuleNameFeedback; private String examinationAchievementsFeedback; + private String examinationAchievementsPromptFeedback; private String repetitionFeedback; private String recommendedPrerequisitesFeedback; private String contentFeedback; + private String contentPromptFeedback; private String learningOutcomesFeedback; + private String learningOutcomesPromptFeedback; private String teachingMethodsFeedback; + private String teachingMethodsPromptFeedback; private String mediaFeedback; private String literatureFeedback; private String responsiblesFeedback; @@ -64,6 +76,8 @@ public static ModuleVersionViewFeedbackDTO from(Feedback f) { dto.setDegreeProgramSpecializationId(f.getDegreeProgramSpecialization().getDegreeProgramSpecializationId()); } dto.setTitleFeedback(f.getTitleFeedback()); + dto.setTitleDeFeedback(f.getTitleDeFeedback()); + dto.setBulletPointsFeedback(f.getBulletPointsFeedback()); dto.setLevelFeedback(f.getLevelFeedback()); dto.setLanguageFeedback(f.getLanguageFeedback()); dto.setFrequencyFeedback(f.getFrequencyFeedback()); @@ -72,12 +86,22 @@ public static ModuleVersionViewFeedbackDTO from(Feedback f) { dto.setHoursTotalFeedback(f.getHoursTotalFeedback()); dto.setHoursSelfStudyFeedback(f.getHoursSelfStudyFeedback()); dto.setHoursPresenceFeedback(f.getHoursPresenceFeedback()); + dto.setHoursLectureFeedback(f.getHoursLectureFeedback()); + dto.setHoursExerciseFeedback(f.getHoursExerciseFeedback()); + dto.setHoursPracticalFeedback(f.getHoursPracticalFeedback()); + dto.setHoursSeminarFeedback(f.getHoursSeminarFeedback()); + dto.setFirstSemesterAvailableFeedback(f.getFirstSemesterAvailableFeedback()); + dto.setSuccessorModuleNameFeedback(f.getSuccessorModuleNameFeedback()); dto.setExaminationAchievementsFeedback(f.getExaminationAchievementsFeedback()); + dto.setExaminationAchievementsPromptFeedback(f.getExaminationAchievementsPromptFeedback()); dto.setRepetitionFeedback(f.getRepetitionFeedback()); dto.setRecommendedPrerequisitesFeedback(f.getRecommendedPrerequisitesFeedback()); dto.setContentFeedback(f.getContentFeedback()); + dto.setContentPromptFeedback(f.getContentPromptFeedback()); dto.setLearningOutcomesFeedback(f.getLearningOutcomesFeedback()); + dto.setLearningOutcomesPromptFeedback(f.getLearningOutcomesPromptFeedback()); dto.setTeachingMethodsFeedback(f.getTeachingMethodsFeedback()); + dto.setTeachingMethodsPromptFeedback(f.getTeachingMethodsPromptFeedback()); dto.setMediaFeedback(f.getMediaFeedback()); dto.setLiteratureFeedback(f.getLiteratureFeedback()); dto.setResponsiblesFeedback(f.getResponsiblesFeedback()); diff --git a/Server/src/main/java/modulemanagement/ls1/models/Feedback.java b/Server/src/main/java/modulemanagement/ls1/models/Feedback.java index 48c66652..660469b8 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/Feedback.java +++ b/Server/src/main/java/modulemanagement/ls1/models/Feedback.java @@ -67,6 +67,18 @@ public class Feedback { @Column(name = "title_accepted") private boolean titleAccepted; + @Column(name = "title_de_feedback", columnDefinition = "CLOB") + private String titleDeFeedback; + + @Column(name = "title_de_accepted") + private boolean titleDeAccepted; + + @Column(name = "bullet_points_feedback", columnDefinition = "CLOB") + private String bulletPointsFeedback; + + @Column(name = "bullet_points_accepted") + private boolean bulletPointsAccepted; + @Column(name = "level_feedback") private String levelFeedback; @@ -115,12 +127,54 @@ public class Feedback { @Column(name = "hours_presence_accepted") private boolean hoursPresenceAccepted; + @Column(name = "hours_lecture_feedback", columnDefinition = "CLOB") + private String hoursLectureFeedback; + + @Column(name = "hours_lecture_accepted") + private boolean hoursLectureAccepted; + + @Column(name = "hours_exercise_feedback", columnDefinition = "CLOB") + private String hoursExerciseFeedback; + + @Column(name = "hours_exercise_accepted") + private boolean hoursExerciseAccepted; + + @Column(name = "hours_practical_feedback", columnDefinition = "CLOB") + private String hoursPracticalFeedback; + + @Column(name = "hours_practical_accepted") + private boolean hoursPracticalAccepted; + + @Column(name = "hours_seminar_feedback", columnDefinition = "CLOB") + private String hoursSeminarFeedback; + + @Column(name = "hours_seminar_accepted") + private boolean hoursSeminarAccepted; + + @Column(name = "first_semester_available_feedback", columnDefinition = "CLOB") + private String firstSemesterAvailableFeedback; + + @Column(name = "first_semester_available_accepted") + private boolean firstSemesterAvailableAccepted; + + @Column(name = "successor_module_name_feedback", columnDefinition = "CLOB") + private String successorModuleNameFeedback; + + @Column(name = "successor_module_name_accepted") + private boolean successorModuleNameAccepted; + @Column(name = "examination_feedback", columnDefinition = "CLOB") private String examinationAchievementsFeedback; @Column(name = "examination_accepted") private boolean examinationAchievementsAccepted; + @Column(name = "examination_prompt_feedback", columnDefinition = "CLOB") + private String examinationAchievementsPromptFeedback; + + @Column(name = "examination_prompt_accepted") + private boolean examinationAchievementsPromptAccepted; + @Column(name = "repetition_feedback", columnDefinition = "CLOB") private String repetitionFeedback; @@ -139,18 +193,36 @@ public class Feedback { @Column(name = "content_accepted") private boolean contentAccepted; + @Column(name = "content_prompt_feedback", columnDefinition = "CLOB") + private String contentPromptFeedback; + + @Column(name = "content_prompt_accepted") + private boolean contentPromptAccepted; + @Column(name = "learning_feedback", columnDefinition = "CLOB") private String learningOutcomesFeedback; @Column(name = "learning_accepted") private boolean learningOutcomesAccepted; + @Column(name = "learning_prompt_feedback", columnDefinition = "CLOB") + private String learningOutcomesPromptFeedback; + + @Column(name = "learning_prompt_accepted") + private boolean learningOutcomesPromptAccepted; + @Column(name = "teaching_feedback", columnDefinition = "CLOB") private String teachingMethodsFeedback; @Column(name = "teaching_accepted") private boolean teachingMethodsAccepted; + @Column(name = "teaching_prompt_feedback", columnDefinition = "CLOB") + private String teachingMethodsPromptFeedback; + + @Column(name = "teaching_prompt_accepted") + private boolean teachingMethodsPromptAccepted; + @Column(name = "media_feedback", columnDefinition = "CLOB") private String mediaFeedback; @@ -182,6 +254,10 @@ public boolean isFeedbackGiven() { public void insert(FeedbackDTO dto) { this.titleFeedback = dto.getTitleFeedback(); this.titleAccepted = dto.isTitleAccepted(); + this.titleDeFeedback = dto.getTitleDeFeedback(); + this.titleDeAccepted = dto.isTitleDeAccepted(); + this.bulletPointsFeedback = dto.getBulletPointsFeedback(); + this.bulletPointsAccepted = dto.isBulletPointsAccepted(); this.levelFeedback = dto.getLevelFeedback(); this.levelAccepted = dto.isLevelAccepted(); this.languageFeedback = dto.getLanguageFeedback(); @@ -198,18 +274,38 @@ public void insert(FeedbackDTO dto) { this.hoursSelfStudyAccepted = dto.isHoursSelfStudyAccepted(); this.hoursPresenceFeedback = dto.getHoursPresenceFeedback(); this.hoursPresenceAccepted = dto.isHoursPresenceAccepted(); + this.hoursLectureFeedback = dto.getHoursLectureFeedback(); + this.hoursLectureAccepted = dto.isHoursLectureAccepted(); + this.hoursExerciseFeedback = dto.getHoursExerciseFeedback(); + this.hoursExerciseAccepted = dto.isHoursExerciseAccepted(); + this.hoursPracticalFeedback = dto.getHoursPracticalFeedback(); + this.hoursPracticalAccepted = dto.isHoursPracticalAccepted(); + this.hoursSeminarFeedback = dto.getHoursSeminarFeedback(); + this.hoursSeminarAccepted = dto.isHoursSeminarAccepted(); + this.firstSemesterAvailableFeedback = dto.getFirstSemesterAvailableFeedback(); + this.firstSemesterAvailableAccepted = dto.isFirstSemesterAvailableAccepted(); + this.successorModuleNameFeedback = dto.getSuccessorModuleNameFeedback(); + this.successorModuleNameAccepted = dto.isSuccessorModuleNameAccepted(); this.examinationAchievementsFeedback = dto.getExaminationAchievementsFeedback(); this.examinationAchievementsAccepted = dto.isExaminationAchievementsAccepted(); + this.examinationAchievementsPromptFeedback = dto.getExaminationAchievementsPromptFeedback(); + this.examinationAchievementsPromptAccepted = dto.isExaminationAchievementsPromptAccepted(); this.repetitionFeedback = dto.getRepetitionFeedback(); this.repetitionAccepted = dto.isRepetitionAccepted(); this.recommendedPrerequisitesFeedback = dto.getRecommendedPrerequisitesFeedback(); this.recommendedPrerequisitesAccepted = dto.isRecommendedPrerequisitesAccepted(); this.contentFeedback = dto.getContentFeedback(); this.contentAccepted = dto.isContentAccepted(); + this.contentPromptFeedback = dto.getContentPromptFeedback(); + this.contentPromptAccepted = dto.isContentPromptAccepted(); this.learningOutcomesFeedback = dto.getLearningOutcomesFeedback(); this.learningOutcomesAccepted = dto.isLearningOutcomesAccepted(); + this.learningOutcomesPromptFeedback = dto.getLearningOutcomesPromptFeedback(); + this.learningOutcomesPromptAccepted = dto.isLearningOutcomesPromptAccepted(); this.teachingMethodsFeedback = dto.getTeachingMethodsFeedback(); this.teachingMethodsAccepted = dto.isTeachingMethodsAccepted(); + this.teachingMethodsPromptFeedback = dto.getTeachingMethodsPromptFeedback(); + this.teachingMethodsPromptAccepted = dto.isTeachingMethodsPromptAccepted(); this.mediaFeedback = dto.getMediaFeedback(); this.mediaAccepted = dto.isMediaAccepted(); this.literatureFeedback = dto.getLiteratureFeedback(); @@ -222,6 +318,8 @@ public void insert(FeedbackDTO dto) { public boolean isAllFeedbackPositive() { return this.titleAccepted + && this.titleDeAccepted + && this.bulletPointsAccepted && this.levelAccepted && this.languageAccepted && this.frequencyAccepted @@ -230,12 +328,22 @@ public boolean isAllFeedbackPositive() { && this.hoursTotalAccepted && this.hoursSelfStudyAccepted && this.hoursPresenceAccepted + && this.hoursLectureAccepted + && this.hoursExerciseAccepted + && this.hoursPracticalAccepted + && this.hoursSeminarAccepted + && this.firstSemesterAvailableAccepted + && this.successorModuleNameAccepted && this.examinationAchievementsAccepted + && this.examinationAchievementsPromptAccepted && this.repetitionAccepted && this.recommendedPrerequisitesAccepted && this.contentAccepted + && this.contentPromptAccepted && this.learningOutcomesAccepted + && this.learningOutcomesPromptAccepted && this.teachingMethodsAccepted + && this.teachingMethodsPromptAccepted && this.mediaAccepted && this.literatureAccepted && this.responsiblesAccepted diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index 8a9769db..081bdf9b 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -155,7 +155,7 @@ private void applyUpdateRequest(ModuleVersion mv, ModuleVersionUpdateRequestDTO } mv.getDegreeProgramAssignments().clear(); - entityManager.flush(); +th entityManager.flush(); for (ModuleDegreeProgramAssignmentDTO item : request.getDegreeProgramAssignments()) { DegreeProgram program = degreeProgramRepository .findWithSpecializationsByDegreeProgramId(item.getDegreeProgramId()) diff --git a/Server/src/main/resources/db/changelog/changes/0014_feedback_additional_fields.yaml b/Server/src/main/resources/db/changelog/changes/0014_feedback_additional_fields.yaml new file mode 100644 index 00000000..895a0398 --- /dev/null +++ b/Server/src/main/resources/db/changelog/changes/0014_feedback_additional_fields.yaml @@ -0,0 +1,92 @@ +databaseChangeLog: + - changeSet: + id: 0014_feedback_additional_fields + author: module-management + changes: + - addColumn: + tableName: feedback + columns: + - column: + name: title_de_feedback + type: CLOB + - column: + name: title_de_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: bullet_points_feedback + type: CLOB + - column: + name: bullet_points_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: hours_lecture_feedback + type: CLOB + - column: + name: hours_lecture_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: hours_exercise_feedback + type: CLOB + - column: + name: hours_exercise_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: hours_practical_feedback + type: CLOB + - column: + name: hours_practical_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: hours_seminar_feedback + type: CLOB + - column: + name: hours_seminar_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: first_semester_available_feedback + type: CLOB + - column: + name: first_semester_available_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: successor_module_name_feedback + type: CLOB + - column: + name: successor_module_name_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: examination_prompt_feedback + type: CLOB + - column: + name: examination_prompt_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: content_prompt_feedback + type: CLOB + - column: + name: content_prompt_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: learning_prompt_feedback + type: CLOB + - column: + name: learning_prompt_accepted + type: BOOLEAN + defaultValueBoolean: false + - column: + name: teaching_prompt_feedback + type: CLOB + - column: + name: teaching_prompt_accepted + type: BOOLEAN + defaultValueBoolean: false diff --git a/Server/src/main/resources/db/changelog/master.yaml b/Server/src/main/resources/db/changelog/master.yaml index 40436095..50f77c43 100644 --- a/Server/src/main/resources/db/changelog/master.yaml +++ b/Server/src/main/resources/db/changelog/master.yaml @@ -38,3 +38,6 @@ databaseChangeLog: - include: relativeToChangelogFile: true file: changes/0013_grant_program_area_responsible_role.yaml + - include: + relativeToChangelogFile: true + file: changes/0014_feedback_additional_fields.yaml From 58f774273413a718e9f8792f9224a615c232dcce Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Fri, 20 Mar 2026 00:29:52 +0100 Subject: [PATCH 15/42] imrpove feedback given visualisation --- .../create-edit-base-layout.css | 57 ++++++ .../create-edit-base.component.html | 162 +++++++++++++++++- .../module-version-view.component.css | 39 +++++ .../module-version-view.component.html | 99 +++++------ .../module-version-view.component.ts | 39 +++-- 5 files changed, 323 insertions(+), 73 deletions(-) diff --git a/Client/src/app/components/create-edit-base/create-edit-base-layout.css b/Client/src/app/components/create-edit-base/create-edit-base-layout.css index fbbb0231..e280d4c5 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base-layout.css +++ b/Client/src/app/components/create-edit-base/create-edit-base-layout.css @@ -20,3 +20,60 @@ .module-edit-form-column { min-width: 0; } + +.feedback-message, +.my-4.inline-block.w-fit, +.my-2.inline-block.w-fit { + position: relative; + border-left: 4px solid #dc2626; + border: 1px solid #fecaca !important; + background: #fef2f2 !important; + border-radius: 8px; + width: 100%; + box-shadow: 0 1px 2px rgba(153, 27, 27, 0.08); + padding: 0.55rem 0.75rem; +} + +.feedback-message::before, +.my-4.inline-block.w-fit::before, +.my-2.inline-block.w-fit::before { + content: "⚠ Reviewer feedback"; + display: inline-flex !important; + align-items: center; + width: fit-content; + max-width: max-content; + margin-right: 0.55rem; + margin-bottom: 0.35rem; + padding: 0.05rem 0.4rem; + border-radius: 999px; + background: #fee2e2; + color: #b91c1c; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.01em; + text-transform: uppercase; + white-space: nowrap; +} + +.feedback-message p, +.my-4.inline-block.w-fit p, +.my-2.inline-block.w-fit p { + color: #7f1d1d; + margin: 0.15rem 0 0; + font-size: 0.92rem; + line-height: 1.25rem; +} + +.feedback-message p:first-of-type, +.my-4.inline-block.w-fit p:first-of-type, +.my-2.inline-block.w-fit p:first-of-type { + margin-top: 0; +} + +.feedback-message p span, +.my-4.inline-block.w-fit p span, +.my-2.inline-block.w-fit p span { + color: #991b1b; + font-weight: 600; + text-decoration: none !important; +} diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index ced0ec6c..8167fd5e 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -23,7 +23,7 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' * @if (moduleVersionId && hasFeedback('titleFeedback')) { - + @for (feedback of feedbacks(); track feedback.feedbackId) { @if (feedback.titleFeedback) {

@@ -43,12 +43,36 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

+ @if (moduleVersionId && hasFeedback('titleDeFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.titleDeFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.titleDeFeedback }} +

+ } + } +
+ }
- +
@@ -70,6 +94,18 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '

+ @if (moduleVersionId && hasFeedback('frequencyFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.frequencyFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.frequencyFeedback }} +

+ } + } +
+ }
{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '
+ @if (moduleVersionId && hasFeedback('hoursLectureFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.hoursLectureFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.hoursLectureFeedback }} +

+ } + } +
+ }
+ @if (moduleVersionId && hasFeedback('hoursExerciseFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.hoursExerciseFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.hoursExerciseFeedback }} +

+ } + } +
+ }
+ @if (moduleVersionId && hasFeedback('hoursPracticalFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.hoursPracticalFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.hoursPracticalFeedback }} +

+ } + } +
+ }
+ @if (moduleVersionId && hasFeedback('hoursSeminarFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.hoursSeminarFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.hoursSeminarFeedback }} +

+ } + } +
+ }
+ @if (moduleVersionId && hasFeedback('firstSemesterAvailableFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.firstSemesterAvailableFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.firstSemesterAvailableFeedback }} +

+ } + } +
+ }
+ @if (moduleVersionId && hasFeedback('successorModuleNameFeedback')) { + + @for (feedback of feedbacks(); track feedback.feedbackId) { + @if (feedback.successorModuleNameFeedback) { +

+ {{ feedback.requestedFromUserName ?? (feedback.feedbackRole | feedbackDepartment).text }}: + {{ feedback.successorModuleNameFeedback }} +

+ } + } +
+ }
- - - @if (user() !== undefined) { - - - - } @else { - - - - - } +
diff --git a/Client/src/app/components/header/header.component.ts b/Client/src/app/components/header/header.component.ts index 64b94949..ab342555 100644 --- a/Client/src/app/components/header/header.component.ts +++ b/Client/src/app/components/header/header.component.ts @@ -4,17 +4,14 @@ import { SecurityStore } from '../../core/security/security-store.service'; import { ThemeService } from '../../core/theme/theme.service'; import { SidebarService } from '../side-bar/sidebar.service'; import { ButtonModule } from 'primeng/button'; -import { ButtonGroupModule } from 'primeng/buttongroup'; -import { AvatarModule } from 'primeng/avatar'; -import { MenuModule } from 'primeng/menu'; import { TooltipModule } from 'primeng/tooltip'; -import { MenuItem } from 'primeng/api'; +import { SignInComponent } from '../sign-in/sign-in.component'; @Component({ selector: 'app-header', templateUrl: './header.component.html', standalone: true, - imports: [RouterLink, ButtonModule, ButtonGroupModule, AvatarModule, MenuModule, TooltipModule] + imports: [RouterLink, ButtonModule, TooltipModule, SignInComponent] }) export class HeaderComponent { securityStore = inject(SecurityStore); @@ -24,35 +21,7 @@ export class HeaderComponent { user = this.securityStore.user; isDarkMode = this.themeService.isDarkMode; - menuItems: MenuItem[] = [ - { - label: 'Settings', - icon: 'pi pi-cog', - routerLink: '/account' - }, - { - separator: true - }, - { - label: 'Sign Out', - icon: 'pi pi-sign-out', - command: () => this.signOut() - } - ]; - toggleTheme() { this.themeService.toggleTheme(); } - - signIn() { - this.securityStore.signIn(); - } - - signInWithPasskey() { - this.securityStore.signInWithPasskey(); - } - - signOut() { - this.securityStore.signOut(); - } } diff --git a/Client/src/app/components/sign-in/sign-in.component.html b/Client/src/app/components/sign-in/sign-in.component.html new file mode 100644 index 00000000..057109c0 --- /dev/null +++ b/Client/src/app/components/sign-in/sign-in.component.html @@ -0,0 +1,33 @@ +@if (user() !== undefined) { + + + +} @else { + + + + +} diff --git a/Client/src/app/components/sign-in/sign-in.component.ts b/Client/src/app/components/sign-in/sign-in.component.ts new file mode 100644 index 00000000..d92b434a --- /dev/null +++ b/Client/src/app/components/sign-in/sign-in.component.ts @@ -0,0 +1,35 @@ +import { Component, inject } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ButtonModule } from 'primeng/button'; +import { ButtonGroupModule } from 'primeng/buttongroup'; +import { MenuModule } from 'primeng/menu'; +import { TooltipModule } from 'primeng/tooltip'; +import { MenuItem } from 'primeng/api'; +import { SecurityStore } from '../../core/security/security-store.service'; + +@Component({ + selector: 'app-sign-in', + standalone: true, + imports: [RouterModule, ButtonModule, ButtonGroupModule, MenuModule, TooltipModule], + templateUrl: './sign-in.component.html' +}) +export class SignInComponent { + readonly securityStore = inject(SecurityStore); + readonly user = this.securityStore.user; + + menuItems: MenuItem[] = [ + { + label: 'Settings', + icon: 'pi pi-cog', + routerLink: '/account' + }, + { + separator: true + }, + { + label: 'Sign Out', + icon: 'pi pi-sign-out', + command: () => this.securityStore.signOut() + } + ]; +} diff --git a/Client/src/app/pages/index/index.component.html b/Client/src/app/pages/index/index.component.html index 496a1791..bed247af 100644 --- a/Client/src/app/pages/index/index.component.html +++ b/Client/src/app/pages/index/index.component.html @@ -7,26 +7,9 @@

Sign in to access the Module Management application.

- - - - +
+ +
diff --git a/Client/src/app/pages/index/index.component.ts b/Client/src/app/pages/index/index.component.ts index 3ed0689d..63ac1578 100644 --- a/Client/src/app/pages/index/index.component.ts +++ b/Client/src/app/pages/index/index.component.ts @@ -1,17 +1,16 @@ import { Component, inject } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ButtonModule } from 'primeng/button'; -import { ButtonGroupModule } from 'primeng/buttongroup'; import { DividerModule } from 'primeng/divider'; import { PanelModule } from 'primeng/panel'; -import { TooltipModule } from 'primeng/tooltip'; import { SecurityStore } from '../../core/security/security-store.service'; +import { SignInComponent } from '../../components/sign-in/sign-in.component'; import { isAdminRole, isProfessorRole, isReviewerRole } from '../../core/shared/user-role.utils'; @Component({ selector: 'index-component', standalone: true, - imports: [RouterModule, ButtonModule, ButtonGroupModule, DividerModule, PanelModule, TooltipModule], + imports: [RouterModule, ButtonModule, DividerModule, PanelModule, SignInComponent], templateUrl: './index.component.html' }) export class IndexComponent { From 9c8682a2f21f14da33812bb79b80badba24696e5 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 16:47:01 +0100 Subject: [PATCH 35/42] db migration --- .../changes/0015_examination_board.yaml | 62 +++++++++++++++++++ .../main/resources/db/changelog/master.yaml | 3 + 2 files changed, 65 insertions(+) create mode 100644 Server/src/main/resources/db/changelog/changes/0015_examination_board.yaml diff --git a/Server/src/main/resources/db/changelog/changes/0015_examination_board.yaml b/Server/src/main/resources/db/changelog/changes/0015_examination_board.yaml new file mode 100644 index 00000000..608b5fe8 --- /dev/null +++ b/Server/src/main/resources/db/changelog/changes/0015_examination_board.yaml @@ -0,0 +1,62 @@ +databaseChangeLog: + - changeSet: + id: 0015a_examination_board_tables + author: module-management + changes: + - createTable: + tableName: examination_board + columns: + - column: + name: examination_board_id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: name + type: VARCHAR(512) + constraints: + nullable: false + - createTable: + tableName: examination_board_user + columns: + - column: + name: examination_board_id + type: BIGINT + constraints: + nullable: false + - column: + name: user_id + type: UUID + constraints: + nullable: false + - addPrimaryKey: + tableName: examination_board_user + columnNames: examination_board_id, user_id + constraintName: pk_examination_board_user + - addForeignKeyConstraint: + baseTableName: examination_board_user + baseColumnNames: examination_board_id + referencedTableName: examination_board + referencedColumnNames: examination_board_id + constraintName: fk_ebu_examination_board + - addForeignKeyConstraint: + baseTableName: examination_board_user + baseColumnNames: user_id + referencedTableName: app_user + referencedColumnNames: user_id + constraintName: fk_ebu_user + - addColumn: + tableName: degree_program + columns: + - column: + name: examination_board_id + type: BIGINT + constraints: + nullable: true + - addForeignKeyConstraint: + baseTableName: degree_program + baseColumnNames: examination_board_id + referencedTableName: examination_board + referencedColumnNames: examination_board_id + constraintName: fk_degree_program_examination_board diff --git a/Server/src/main/resources/db/changelog/master.yaml b/Server/src/main/resources/db/changelog/master.yaml index 50f77c43..46fe4a34 100644 --- a/Server/src/main/resources/db/changelog/master.yaml +++ b/Server/src/main/resources/db/changelog/master.yaml @@ -41,3 +41,6 @@ databaseChangeLog: - include: relativeToChangelogFile: true file: changes/0014_feedback_additional_fields.yaml + - include: + relativeToChangelogFile: true + file: changes/0015_examination_board.yaml From 367785b7749187626f9bdd62aa1d87a6352e9fda Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 16:49:09 +0100 Subject: [PATCH 36/42] entity model --- .../ls1/models/DegreeProgram.java | 4 +++ .../ls1/models/ExaminationBoard.java | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 Server/src/main/java/modulemanagement/ls1/models/ExaminationBoard.java diff --git a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java index 41adbdad..66518f38 100644 --- a/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java +++ b/Server/src/main/java/modulemanagement/ls1/models/DegreeProgram.java @@ -29,4 +29,8 @@ public class DegreeProgram { @JoinTable(name = "degree_program_specialization_assignment", joinColumns = @JoinColumn(name = "degree_program_id"), inverseJoinColumns = @JoinColumn(name = "degree_program_specialization_id")) private List degreeProgramSpecializations = new ArrayList<>(); + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "examination_board_id") + private ExaminationBoard examinationBoard; + } diff --git a/Server/src/main/java/modulemanagement/ls1/models/ExaminationBoard.java b/Server/src/main/java/modulemanagement/ls1/models/ExaminationBoard.java new file mode 100644 index 00000000..f5027914 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/models/ExaminationBoard.java @@ -0,0 +1,31 @@ +package modulemanagement.ls1.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.Set; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "examination_board") +public class ExaminationBoard { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "examination_board_id") + private Long examinationBoardId; + + @Column(name = "name", nullable = false, length = 512) + private String name; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "examination_board_user", + joinColumns = @JoinColumn(name = "examination_board_id"), + inverseJoinColumns = @JoinColumn(name = "user_id")) + @JsonIgnore + private Set members = new HashSet<>(); +} From 5524448293faf82a462cf7b979f904f459bc3c9e Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 20:21:44 +0100 Subject: [PATCH 37/42] exmaination board crud backend --- .../ExaminationBoardController.java | 51 ++++++++++ .../ls1/dtos/CreateExaminationBoardDTO.java | 12 +++ .../ls1/dtos/DegreeProgramDTO.java | 3 + .../ls1/dtos/ExaminationBoardDTO.java | 26 +++++ .../ls1/dtos/ExaminationBoardSummaryDTO.java | 19 ++++ .../ls1/dtos/UpdateDegreeProgramDTO.java | 1 + .../ls1/dtos/UpdateExaminationBoardDTO.java | 21 ++++ .../repositories/DegreeProgramRepository.java | 12 ++- .../ExaminationBoardRepository.java | 31 ++++++ .../ls1/services/AdminUserService.java | 13 ++- .../ls1/services/DegreeProgramService.java | 28 ++++-- .../DegreeProgramSpecializationService.java | 14 +-- .../ls1/services/ExaminationBoardService.java | 98 +++++++++++++++++++ ...Service.java => UserRolesSyncService.java} | 75 +++++++++++--- 14 files changed, 367 insertions(+), 37 deletions(-) create mode 100644 Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/CreateExaminationBoardDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardSummaryDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/dtos/UpdateExaminationBoardDTO.java create mode 100644 Server/src/main/java/modulemanagement/ls1/repositories/ExaminationBoardRepository.java create mode 100644 Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java rename Server/src/main/java/modulemanagement/ls1/services/{ResponsibleUserRoleService.java => UserRolesSyncService.java} (58%) diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java new file mode 100644 index 00000000..304da11c --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java @@ -0,0 +1,51 @@ +package modulemanagement.ls1.controllers; + +import jakarta.validation.Valid; +import modulemanagement.ls1.dtos.*; +import modulemanagement.ls1.services.ExaminationBoardService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/examination-boards") +@PreAuthorize("hasRole('ADMIN')") +public class ExaminationBoardController { + + private final ExaminationBoardService examinationBoardService; + + public ExaminationBoardController(ExaminationBoardService examinationBoardService) { + this.examinationBoardService = examinationBoardService; + } + + @GetMapping + public ResponseEntity> getAllExaminationBoards() { + return ResponseEntity.ok(examinationBoardService.getAllExaminationBoards()); + } + + @GetMapping("/{id}") + public ResponseEntity getExaminationBoard(@PathVariable Long id) { + return ResponseEntity.ok(examinationBoardService.getExaminationBoard(id)); + } + + @PostMapping + public ResponseEntity createExaminationBoard( + @Valid @RequestBody CreateExaminationBoardDTO dto) { + return ResponseEntity.ok(examinationBoardService.createExaminationBoard(dto)); + } + + @PutMapping("/{id}") + public ResponseEntity updateExaminationBoard( + @PathVariable Long id, + @Valid @RequestBody UpdateExaminationBoardDTO dto) { + return ResponseEntity.ok(examinationBoardService.updateExaminationBoard(id, dto)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteExaminationBoard(@PathVariable Long id) { + examinationBoardService.deleteExaminationBoard(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/CreateExaminationBoardDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/CreateExaminationBoardDTO.java new file mode 100644 index 00000000..cd09ba9e --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/CreateExaminationBoardDTO.java @@ -0,0 +1,12 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class CreateExaminationBoardDTO { + @NotBlank + @Size(max = 512) + private String name; +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java index d05a4005..abe666df 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/DegreeProgramDTO.java @@ -18,6 +18,8 @@ public class DegreeProgramDTO { private List degreeProgramSpecializations; + private ExaminationBoardSummaryDTO examinationBoard; + public static DegreeProgramDTO fromDegreeProgram(DegreeProgram program) { DegreeProgramDTO dto = new DegreeProgramDTO(); dto.setDegreeProgramId(program.getDegreeProgramId()); @@ -25,6 +27,7 @@ public static DegreeProgramDTO fromDegreeProgram(DegreeProgram program) { if (program.getResponsibleUser() != null) { dto.setResponsibleUser(ResponsibleUserDTO.fromUser(program.getResponsibleUser())); } + dto.setExaminationBoard(ExaminationBoardSummaryDTO.fromEntity(program.getExaminationBoard())); if (program.getDegreeProgramSpecializations() != null) { dto.setDegreeProgramSpecializations(program.getDegreeProgramSpecializations().stream() .map(DegreeProgramSpecializationDTO::fromEntity).collect(Collectors.toList())); diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardDTO.java new file mode 100644 index 00000000..978d5f28 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardDTO.java @@ -0,0 +1,26 @@ +package modulemanagement.ls1.dtos; + +import lombok.Data; +import modulemanagement.ls1.models.ExaminationBoard; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Data +public class ExaminationBoardDTO { + private Long examinationBoardId; + private String name; + private List members = new ArrayList<>(); + + public static ExaminationBoardDTO fromEntity(ExaminationBoard board) { + ExaminationBoardDTO dto = new ExaminationBoardDTO(); + dto.setExaminationBoardId(board.getExaminationBoardId()); + dto.setName(board.getName()); + if (board.getMembers() != null) { + dto.setMembers(board.getMembers().stream().map(ResponsibleUserDTO::fromUser) + .collect(Collectors.toList())); + } + return dto; + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardSummaryDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardSummaryDTO.java new file mode 100644 index 00000000..5792b31c --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/ExaminationBoardSummaryDTO.java @@ -0,0 +1,19 @@ +package modulemanagement.ls1.dtos; + +import lombok.Data; +import modulemanagement.ls1.models.ExaminationBoard; + +@Data +public class ExaminationBoardSummaryDTO { + private Long examinationBoardId; + private String name; + + public static ExaminationBoardSummaryDTO fromEntity(ExaminationBoard board) { + if (board == null) + return null; + ExaminationBoardSummaryDTO dto = new ExaminationBoardSummaryDTO(); + dto.setExaminationBoardId(board.getExaminationBoardId()); + dto.setName(board.getName()); + return dto; + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java index dfd14230..8fd053d0 100644 --- a/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java +++ b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateDegreeProgramDTO.java @@ -8,4 +8,5 @@ public class UpdateDegreeProgramDTO { private String name; private UUID responsibleUserId; + private Long examinationBoardId; } diff --git a/Server/src/main/java/modulemanagement/ls1/dtos/UpdateExaminationBoardDTO.java b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateExaminationBoardDTO.java new file mode 100644 index 00000000..6597586c --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/dtos/UpdateExaminationBoardDTO.java @@ -0,0 +1,21 @@ +package modulemanagement.ls1.dtos; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Data +public class UpdateExaminationBoardDTO { + + @NotBlank + @Size(max = 512) + private String name; + + @NotNull + private List userIds = new ArrayList<>(); +} diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java index b68d4146..bb1b7dde 100644 --- a/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java +++ b/Server/src/main/java/modulemanagement/ls1/repositories/DegreeProgramRepository.java @@ -3,7 +3,9 @@ import modulemanagement.ls1.models.DegreeProgram; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -17,15 +19,19 @@ public interface DegreeProgramRepository extends JpaRepository findByResponsibleUser_UserId(UUID userId); - @EntityGraph(attributePaths = { "responsibleUser" }) + @EntityGraph(attributePaths = { "responsibleUser", "examinationBoard" }) @Query("SELECT p FROM DegreeProgram p") List findAllWithResponsibleUser(); @EntityGraph(attributePaths = { "responsibleUser", "degreeProgramSpecializations", - "degreeProgramSpecializations.responsibleUser" }) + "degreeProgramSpecializations.responsibleUser", "examinationBoard" }) Optional findWithSpecializationsByDegreeProgramId(Long degreeProgramId); - @EntityGraph(attributePaths = { "degreeProgramSpecializations" }) + @EntityGraph(attributePaths = { "degreeProgramSpecializations", "examinationBoard" }) @Query("SELECT p FROM DegreeProgram p ORDER BY p.name") List findAllWithSpecializations(); + + @Modifying + @Query("UPDATE DegreeProgram p SET p.examinationBoard = null WHERE p.examinationBoard.examinationBoardId = :examinationBoardId") + void clearExaminationBoardIdByExaminationBoardId(@Param("examinationBoardId") Long examinationBoardId); } diff --git a/Server/src/main/java/modulemanagement/ls1/repositories/ExaminationBoardRepository.java b/Server/src/main/java/modulemanagement/ls1/repositories/ExaminationBoardRepository.java new file mode 100644 index 00000000..f283d4b9 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/repositories/ExaminationBoardRepository.java @@ -0,0 +1,31 @@ +package modulemanagement.ls1.repositories; + +import modulemanagement.ls1.models.ExaminationBoard; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ExaminationBoardRepository extends JpaRepository { + + @EntityGraph(attributePaths = { "members" }) + @Query("SELECT e FROM ExaminationBoard e WHERE e.examinationBoardId = :id") + Optional findByIdWithMembers(@Param("id") Long id); + + @EntityGraph(attributePaths = { "members" }) + @Query("SELECT e FROM ExaminationBoard e") + List findAllWithMembers(); + + @Query("SELECT COUNT(e) > 0 FROM ExaminationBoard e JOIN e.members m WHERE m.userId = :userId") + boolean existsByMemberUserId(@Param("userId") UUID userId); + + @EntityGraph(attributePaths = { "members" }) + @Query("SELECT DISTINCT e FROM ExaminationBoard e JOIN e.members m WHERE m.userId = :userId") + List findByMemberUserId(@Param("userId") UUID userId); +} diff --git a/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java b/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java index 4f0fb6b8..76a5b712 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/AdminUserService.java @@ -19,11 +19,11 @@ public class AdminUserService { private final UserRepository userRepository; - private final ResponsibleUserRoleService responsibleUserRoleService; + private final UserRolesSyncService userRolesSyncService; - public AdminUserService(UserRepository userRepository, ResponsibleUserRoleService responsibleUserRoleService) { + public AdminUserService(UserRepository userRepository, UserRolesSyncService userRolesSyncService) { this.userRepository = userRepository; - this.responsibleUserRoleService = responsibleUserRoleService; + this.userRolesSyncService = userRolesSyncService; } public PageResponseDTO getUsersPage(Pageable pageable, String search) { @@ -40,11 +40,14 @@ public UserDTO updateUserRole(UUID userId, UpdateUserRoleDTO dto) { List currentRoles = user.getRoles() != null ? user.getRoles() : List.of(); List newRoles = dto.getRoles() != null ? dto.getRoles() : List.of(); if (currentRoles.contains(UserRole.PROGRAM_COORDINATOR) && !newRoles.contains(UserRole.PROGRAM_COORDINATOR)) { - responsibleUserRoleService.unassignFromAllPrograms(userId); + userRolesSyncService.unassignFromAllPrograms(userId); } if (currentRoles.contains(UserRole.SPECIALIZATION_AREA_COORDINATOR) && !newRoles.contains(UserRole.SPECIALIZATION_AREA_COORDINATOR)) { - responsibleUserRoleService.unassignFromAllSpecializations(userId); + userRolesSyncService.unassignFromAllSpecializations(userId); + } + if (currentRoles.contains(UserRole.EXAMINATION_BOARD) && !newRoles.contains(UserRole.EXAMINATION_BOARD)) { + userRolesSyncService.unassignFromAllExaminationBoards(userId); } user.setRoles(dto.getRoles()); user = userRepository.save(user); diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java index b2cff8fb..f09d2024 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramService.java @@ -5,6 +5,7 @@ import modulemanagement.ls1.models.DegreeProgram; import modulemanagement.ls1.repositories.DegreeProgramRepository; import modulemanagement.ls1.repositories.DegreeProgramSpecializationRepository; +import modulemanagement.ls1.repositories.ExaminationBoardRepository; import modulemanagement.ls1.repositories.UserRepository; import modulemanagement.ls1.shared.ResourceNotFoundException; import org.springframework.stereotype.Service; @@ -19,16 +20,19 @@ public class DegreeProgramService { private final DegreeProgramRepository degreeProgramRepository; private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; private final UserRepository userRepository; - private final ResponsibleUserRoleService responsibleUserRoleService; + private final ExaminationBoardRepository examinationBoardRepository; + private final UserRolesSyncService userRolesSyncService; public DegreeProgramService(DegreeProgramRepository degreeProgramRepository, DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, UserRepository userRepository, - ResponsibleUserRoleService responsibleUserRoleService) { + ExaminationBoardRepository examinationBoardRepository, + UserRolesSyncService userRolesSyncService) { this.degreeProgramRepository = degreeProgramRepository; this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; this.userRepository = userRepository; - this.responsibleUserRoleService = responsibleUserRoleService; + this.examinationBoardRepository = examinationBoardRepository; + this.userRolesSyncService = userRolesSyncService; } public List getAllDegreePrograms() { @@ -54,7 +58,7 @@ public DegreeProgramDTO createDegreeProgram(CreateDegreeProgramDTO dto) { program.setName(dto.getName()); program.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); program = degreeProgramRepository.save(program); - responsibleUserRoleService.ensureProgramCoordinatorRole(dto.getResponsibleUserId()); + userRolesSyncService.ensureProgramCoordinatorRole(dto.getResponsibleUserId()); program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(program.getDegreeProgramId()) .orElse(program); return DegreeProgramDTO.fromDegreeProgram(program); @@ -69,15 +73,21 @@ public DegreeProgramDTO updateDegreeProgram(Long id, UpdateDegreeProgramDTO dto) UUID previousUserId = program.getResponsibleUser() != null ? program.getResponsibleUser().getUserId() : null; program.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); - program = degreeProgramRepository.save(program); - responsibleUserRoleService.ensureProgramCoordinatorRole(dto.getResponsibleUserId()); + userRolesSyncService.ensureProgramCoordinatorRole(dto.getResponsibleUserId()); if (previousUserId != null && !previousUserId.equals(dto.getResponsibleUserId())) - responsibleUserRoleService.removeProgramCoordinatorRoleIfNotResponsible(previousUserId); + userRolesSyncService.removeProgramCoordinatorRoleIfNotResponsible(previousUserId); + } + Long examinationBoardId = dto.getExaminationBoardId(); + if (examinationBoardId != null) { + program.setExaminationBoard(examinationBoardRepository.getReferenceById(examinationBoardId)); } else { - program = degreeProgramRepository.save(program); + program.setExaminationBoard(null); + } + program = degreeProgramRepository.save(program); program = degreeProgramRepository.findWithSpecializationsByDegreeProgramId(id).orElse(program); return DegreeProgramDTO.fromDegreeProgram(program); + } public void deleteDegreeProgram(Long id) { @@ -88,7 +98,7 @@ public void deleteDegreeProgram(Long id) { UUID responsibleUserId = program.getResponsibleUser() != null ? program.getResponsibleUser().getUserId() : null; degreeProgramRepository.deleteById(id); if (responsibleUserId != null) - responsibleUserRoleService.removeProgramCoordinatorRoleIfNotResponsible(responsibleUserId); + userRolesSyncService.removeProgramCoordinatorRoleIfNotResponsible(responsibleUserId); } public DegreeProgramDTO addSpecializationsToDegreeProgram(Long degreeProgramId, diff --git a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java index 9fbd3b3a..979c378d 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/DegreeProgramSpecializationService.java @@ -16,15 +16,15 @@ public class DegreeProgramSpecializationService { private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; private final UserRepository userRepository; - private final ResponsibleUserRoleService responsibleUserRoleService; + private final UserRolesSyncService userRolesSyncService; public DegreeProgramSpecializationService( DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, UserRepository userRepository, - ResponsibleUserRoleService responsibleUserRoleService) { + UserRolesSyncService userRolesSyncService) { this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; this.userRepository = userRepository; - this.responsibleUserRoleService = responsibleUserRoleService; + this.userRolesSyncService = userRolesSyncService; } public List getAllDegreeProgramSpecializations() { @@ -38,7 +38,7 @@ public DegreeProgramSpecializationDTO createDegreeProgramSpecialization(CreateDe entity.setName(dto.getName()); entity.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); entity = degreeProgramSpecializationRepository.save(entity); - responsibleUserRoleService.ensureSpecializationAreaCoordinatorRole(dto.getResponsibleUserId()); + userRolesSyncService.ensureSpecializationAreaCoordinatorRole(dto.getResponsibleUserId()); return DegreeProgramSpecializationDTO.fromEntity( degreeProgramSpecializationRepository .findByIdWithResponsibleUser(entity.getDegreeProgramSpecializationId()).orElse(entity)); @@ -54,9 +54,9 @@ public DegreeProgramSpecializationDTO updateDegreeProgramSpecialization(Long id, UUID previousUserId = entity.getResponsibleUser() != null ? entity.getResponsibleUser().getUserId() : null; entity.setResponsibleUser(userRepository.getReferenceById(dto.getResponsibleUserId())); entity = degreeProgramSpecializationRepository.save(entity); - responsibleUserRoleService.ensureSpecializationAreaCoordinatorRole(dto.getResponsibleUserId()); + userRolesSyncService.ensureSpecializationAreaCoordinatorRole(dto.getResponsibleUserId()); if (previousUserId != null && !previousUserId.equals(dto.getResponsibleUserId())) - responsibleUserRoleService.removeSpecializationAreaCoordinatorRoleIfNotResponsible(previousUserId); + userRolesSyncService.removeSpecializationAreaCoordinatorRoleIfNotResponsible(previousUserId); } else { entity = degreeProgramSpecializationRepository.save(entity); } @@ -72,6 +72,6 @@ public void deleteDegreeProgramSpecialization(Long id) { UUID responsibleUserId = entity.getResponsibleUser() != null ? entity.getResponsibleUser().getUserId() : null; degreeProgramSpecializationRepository.deleteById(id); if (responsibleUserId != null) - responsibleUserRoleService.removeSpecializationAreaCoordinatorRoleIfNotResponsible(responsibleUserId); + userRolesSyncService.removeSpecializationAreaCoordinatorRoleIfNotResponsible(responsibleUserId); } } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java b/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java new file mode 100644 index 00000000..03b8b668 --- /dev/null +++ b/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java @@ -0,0 +1,98 @@ +package modulemanagement.ls1.services; + +import modulemanagement.ls1.dtos.*; +import modulemanagement.ls1.models.ExaminationBoard; +import modulemanagement.ls1.models.User; +import modulemanagement.ls1.repositories.DegreeProgramRepository; +import modulemanagement.ls1.repositories.ExaminationBoardRepository; +import modulemanagement.ls1.repositories.UserRepository; +import modulemanagement.ls1.shared.ResourceNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class ExaminationBoardService { + + private final ExaminationBoardRepository examinationBoardRepository; + private final DegreeProgramRepository degreeProgramRepository; + private final UserRepository userRepository; + private final UserRolesSyncService userRolesSyncService; + + public ExaminationBoardService(ExaminationBoardRepository examinationBoardRepository, + DegreeProgramRepository degreeProgramRepository, + UserRepository userRepository, + UserRolesSyncService userRolesSyncService) { + this.examinationBoardRepository = examinationBoardRepository; + this.degreeProgramRepository = degreeProgramRepository; + this.userRepository = userRepository; + this.userRolesSyncService = userRolesSyncService; + } + + public List getAllExaminationBoards() { + return examinationBoardRepository.findAllWithMembers().stream() + .map(ExaminationBoardDTO::fromEntity) + .collect(Collectors.toList()); + } + + public ExaminationBoardDTO getExaminationBoard(Long id) { + ExaminationBoard board = examinationBoardRepository.findByIdWithMembers(id) + .orElseThrow(() -> new ResourceNotFoundException("Examination board not found: " + id)); + return ExaminationBoardDTO.fromEntity(board); + } + + @Transactional + public ExaminationBoardDTO createExaminationBoard(CreateExaminationBoardDTO dto) { + ExaminationBoard board = new ExaminationBoard(); + board.setName(dto.getName().trim()); + board = examinationBoardRepository.save(board); + return ExaminationBoardDTO.fromEntity( + examinationBoardRepository.findByIdWithMembers(board.getExaminationBoardId()).orElse(board)); + } + + @Transactional + public ExaminationBoardDTO updateExaminationBoard(Long id, UpdateExaminationBoardDTO dto) { + ExaminationBoard board = examinationBoardRepository.findByIdWithMembers(id) + .orElseThrow(() -> new ResourceNotFoundException("Examination board not found: " + id)); + + board.setName(dto.getName().trim()); + + Set previousMemberIds = board.getMembers().stream().map(User::getUserId).collect(Collectors.toSet()); + Set newMemberIds = new HashSet<>(dto.getUserIds()); + + board.getMembers().clear(); + for (UUID userId : newMemberIds) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found: " + userId)); + board.getMembers().add(user); + userRolesSyncService.ensureExaminationBoardRole(userId); + } + examinationBoardRepository.save(board); + + for (UUID oldId : previousMemberIds) { + if (!newMemberIds.contains(oldId)) { + userRolesSyncService.removeExaminationBoardRoleIfNotMember(oldId); + } + } + + return ExaminationBoardDTO.fromEntity( + examinationBoardRepository.findByIdWithMembers(id).orElse(board)); + } + + @Transactional + public void deleteExaminationBoard(Long id) { + ExaminationBoard board = examinationBoardRepository.findByIdWithMembers(id) + .orElseThrow(() -> new ResourceNotFoundException("Examination board not found: " + id)); + java.util.Set memberIds = board.getMembers().stream().map(User::getUserId).collect(Collectors.toSet()); + degreeProgramRepository.clearExaminationBoardIdByExaminationBoardId(id); + examinationBoardRepository.delete(board); + for (UUID uid : memberIds) { + userRolesSyncService.removeExaminationBoardRoleIfNotMember(uid); + } + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java b/Server/src/main/java/modulemanagement/ls1/services/UserRolesSyncService.java similarity index 58% rename from Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java rename to Server/src/main/java/modulemanagement/ls1/services/UserRolesSyncService.java index 578e9a8f..44e8e4d0 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ResponsibleUserRoleService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/UserRolesSyncService.java @@ -3,9 +3,11 @@ import modulemanagement.ls1.enums.UserRole; import modulemanagement.ls1.models.DegreeProgram; import modulemanagement.ls1.models.DegreeProgramSpecialization; +import modulemanagement.ls1.models.ExaminationBoard; import modulemanagement.ls1.models.User; import modulemanagement.ls1.repositories.DegreeProgramRepository; import modulemanagement.ls1.repositories.DegreeProgramSpecializationRepository; +import modulemanagement.ls1.repositories.ExaminationBoardRepository; import modulemanagement.ls1.repositories.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,31 +18,37 @@ /** * Keeps PROGRAM_COORDINATOR and SPECIALIZATION_AREA_COORDINATOR roles in sync: - * - Program coordinators (responsible for a degree program) have PROGRAM_COORDINATOR. + * - Program coordinators (responsible for a degree program) have + * PROGRAM_COORDINATOR. * - Specialization area coordinators have SPECIALIZATION_AREA_COORDINATOR. */ @Service -public class ResponsibleUserRoleService { +public class UserRolesSyncService { private final UserRepository userRepository; private final DegreeProgramRepository degreeProgramRepository; private final DegreeProgramSpecializationRepository degreeProgramSpecializationRepository; + private final ExaminationBoardRepository examinationBoardRepository; - public ResponsibleUserRoleService(UserRepository userRepository, + public UserRolesSyncService(UserRepository userRepository, DegreeProgramRepository degreeProgramRepository, - DegreeProgramSpecializationRepository degreeProgramSpecializationRepository) { + DegreeProgramSpecializationRepository degreeProgramSpecializationRepository, + ExaminationBoardRepository examinationBoardRepository) { this.userRepository = userRepository; this.degreeProgramRepository = degreeProgramRepository; this.degreeProgramSpecializationRepository = degreeProgramSpecializationRepository; + this.examinationBoardRepository = examinationBoardRepository; } @Transactional public void ensureProgramCoordinatorRole(UUID userId) { User user = userRepository.findById(userId).orElse(null); - if (user == null) return; + if (user == null) + return; if (user.getRoles() != null && user.getRoles().contains(UserRole.PROGRAM_COORDINATOR)) return; - if (user.getRoles() == null) user.setRoles(new ArrayList<>()); + if (user.getRoles() == null) + user.setRoles(new ArrayList<>()); user.getRoles().add(UserRole.PROGRAM_COORDINATOR); userRepository.save(user); } @@ -50,7 +58,8 @@ public void removeProgramCoordinatorRoleIfNotResponsible(UUID userId) { if (degreeProgramRepository.existsByResponsibleUser_UserId(userId)) return; User user = userRepository.findById(userId).orElse(null); - if (user == null || user.getRoles() == null) return; + if (user == null || user.getRoles() == null) + return; if (user.getRoles().remove(UserRole.PROGRAM_COORDINATOR)) userRepository.save(user); } @@ -58,10 +67,12 @@ public void removeProgramCoordinatorRoleIfNotResponsible(UUID userId) { @Transactional public void ensureSpecializationAreaCoordinatorRole(UUID userId) { User user = userRepository.findById(userId).orElse(null); - if (user == null) return; + if (user == null) + return; if (user.getRoles() != null && user.getRoles().contains(UserRole.SPECIALIZATION_AREA_COORDINATOR)) return; - if (user.getRoles() == null) user.setRoles(new ArrayList<>()); + if (user.getRoles() == null) + user.setRoles(new ArrayList<>()); user.getRoles().add(UserRole.SPECIALIZATION_AREA_COORDINATOR); userRepository.save(user); } @@ -71,7 +82,8 @@ public void removeSpecializationAreaCoordinatorRoleIfNotResponsible(UUID userId) if (degreeProgramSpecializationRepository.existsByResponsibleUser_UserId(userId)) return; User user = userRepository.findById(userId).orElse(null); - if (user == null || user.getRoles() == null) return; + if (user == null || user.getRoles() == null) + return; if (user.getRoles().remove(UserRole.SPECIALIZATION_AREA_COORDINATOR)) userRepository.save(user); } @@ -82,15 +94,52 @@ public void unassignFromAllPrograms(UUID userId) { for (DegreeProgram p : programs) { p.setResponsibleUser(null); } - if (!programs.isEmpty()) degreeProgramRepository.saveAll(programs); + if (!programs.isEmpty()) + degreeProgramRepository.saveAll(programs); } @Transactional public void unassignFromAllSpecializations(UUID userId) { - List specs = degreeProgramSpecializationRepository.findByResponsibleUser_UserId(userId); + List specs = degreeProgramSpecializationRepository + .findByResponsibleUser_UserId(userId); for (DegreeProgramSpecialization s : specs) { s.setResponsibleUser(null); } - if (!specs.isEmpty()) degreeProgramSpecializationRepository.saveAll(specs); + if (!specs.isEmpty()) + degreeProgramSpecializationRepository.saveAll(specs); + } + + @Transactional + public void ensureExaminationBoardRole(UUID userId) { + User user = userRepository.findById(userId).orElse(null); + if (user == null) + return; + if (user.getRoles() != null && user.getRoles().contains(UserRole.EXAMINATION_BOARD)) + return; + if (user.getRoles() == null) + user.setRoles(new ArrayList<>()); + user.getRoles().add(UserRole.EXAMINATION_BOARD); + userRepository.save(user); + } + + @Transactional + public void removeExaminationBoardRoleIfNotMember(UUID userId) { + if (examinationBoardRepository.existsByMemberUserId(userId)) + return; + User user = userRepository.findById(userId).orElse(null); + if (user == null || user.getRoles() == null) + return; + if (user.getRoles().remove(UserRole.EXAMINATION_BOARD)) + userRepository.save(user); + } + + @Transactional + public void unassignFromAllExaminationBoards(UUID userId) { + List boards = examinationBoardRepository.findByMemberUserId(userId); + for (ExaminationBoard b : boards) { + b.getMembers().removeIf(u -> u.getUserId().equals(userId)); + } + if (!boards.isEmpty()) + examinationBoardRepository.saveAll(boards); } } From 94ae526a7fa61ecad446e6743e73b5f10cb1ef71 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 20:22:22 +0100 Subject: [PATCH 38/42] generate openapi --- .../modules/openapi/.openapi-generator/FILES | 6 + .../src/app/core/modules/openapi/api/api.ts | 5 +- .../examination-board-controller.service.ts | 434 ++++++++++++++++++ ...ation-board-controller.serviceInterface.ts | 62 +++ .../model/create-examination-board-dto.ts | 15 + .../openapi/model/degree-program-dto.ts | 2 + .../openapi/model/examination-board-dto.ts | 18 + .../model/examination-board-summary-dto.ts | 16 + .../app/core/modules/openapi/model/models.ts | 4 + .../model/update-degree-program-dto.ts | 1 + .../model/update-examination-board-dto.ts | 16 + 11 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts create mode 100644 Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts create mode 100644 Client/src/app/core/modules/openapi/model/create-examination-board-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/examination-board-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/examination-board-summary-dto.ts create mode 100644 Client/src/app/core/modules/openapi/model/update-examination-board-dto.ts diff --git a/Client/src/app/core/modules/openapi/.openapi-generator/FILES b/Client/src/app/core/modules/openapi/.openapi-generator/FILES index ddd04dbb..2e877374 100644 --- a/Client/src/app/core/modules/openapi/.openapi-generator/FILES +++ b/Client/src/app/core/modules/openapi/.openapi-generator/FILES @@ -9,6 +9,8 @@ api/degree-program-specializations-controller.service.ts api/degree-program-specializations-controller.serviceInterface.ts api/degree-programs-controller.service.ts api/degree-programs-controller.serviceInterface.ts +api/examination-board-controller.service.ts +api/examination-board-controller.serviceInterface.ts api/feedback-controller.service.ts api/feedback-controller.serviceInterface.ts api/module-version-controller.service.ts @@ -26,8 +28,11 @@ model/completion-service-request-dto.ts model/completion-service-response-dto.ts model/create-degree-program-dto.ts model/create-degree-program-specialization-dto.ts +model/create-examination-board-dto.ts model/degree-program-dto.ts model/degree-program-specialization-dto.ts +model/examination-board-dto.ts +model/examination-board-summary-dto.ts model/feedback-compact-dto.ts model/feedback-dto.ts model/feedback-list-item-dto.ts @@ -47,6 +52,7 @@ model/responsible-user-dto.ts model/similar-module-dto.ts model/update-degree-program-dto.ts model/update-degree-program-specialization-dto.ts +model/update-examination-board-dto.ts model/update-user-role-dto.ts model/user-dto.ts model/user.ts diff --git a/Client/src/app/core/modules/openapi/api/api.ts b/Client/src/app/core/modules/openapi/api/api.ts index 80159f7c..1f2dad9c 100644 --- a/Client/src/app/core/modules/openapi/api/api.ts +++ b/Client/src/app/core/modules/openapi/api/api.ts @@ -7,6 +7,9 @@ export * from './degree-program-specializations-controller.serviceInterface'; export * from './degree-programs-controller.service'; import { DegreeProgramsControllerService } from './degree-programs-controller.service'; export * from './degree-programs-controller.serviceInterface'; +export * from './examination-board-controller.service'; +import { ExaminationBoardControllerService } from './examination-board-controller.service'; +export * from './examination-board-controller.serviceInterface'; export * from './feedback-controller.service'; import { FeedbackControllerService } from './feedback-controller.service'; export * from './feedback-controller.serviceInterface'; @@ -19,4 +22,4 @@ export * from './proposal-controller.serviceInterface'; export * from './user-controller.service'; import { UserControllerService } from './user-controller.service'; export * from './user-controller.serviceInterface'; -export const APIS = [AdminUserControllerService, DegreeProgramSpecializationsControllerService, DegreeProgramsControllerService, FeedbackControllerService, ModuleVersionControllerService, ProposalControllerService, UserControllerService]; +export const APIS = [AdminUserControllerService, DegreeProgramSpecializationsControllerService, DegreeProgramsControllerService, ExaminationBoardControllerService, FeedbackControllerService, ModuleVersionControllerService, ProposalControllerService, UserControllerService]; diff --git a/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts b/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts new file mode 100644 index 00000000..f0798b51 --- /dev/null +++ b/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts @@ -0,0 +1,434 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { CreateExaminationBoardDTO } from '../model/create-examination-board-dto'; +// @ts-ignore +import { ExaminationBoardDTO } from '../model/examination-board-dto'; +// @ts-ignore +import { UpdateExaminationBoardDTO } from '../model/update-examination-board-dto'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + ExaminationBoardControllerServiceInterface +} from './examination-board-controller.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ExaminationBoardControllerService implements ExaminationBoardControllerServiceInterface { + + protected basePath = 'http://localhost:8080'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param createExaminationBoardDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public createExaminationBoard(createExaminationBoardDTO: CreateExaminationBoardDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createExaminationBoard(createExaminationBoardDTO: CreateExaminationBoardDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createExaminationBoard(createExaminationBoardDTO: CreateExaminationBoardDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createExaminationBoard(createExaminationBoardDTO: CreateExaminationBoardDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (createExaminationBoardDTO === null || createExaminationBoardDTO === undefined) { + throw new Error('Required parameter createExaminationBoardDTO was null or undefined when calling createExaminationBoard.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/examination-boards`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: createExaminationBoardDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteExaminationBoard(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public deleteExaminationBoard(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteExaminationBoard(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteExaminationBoard(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling deleteExaminationBoard.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/examination-boards/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getAllExaminationBoards(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAllExaminationBoards(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllExaminationBoards(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllExaminationBoards(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/examination-boards`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getExaminationBoard(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getExaminationBoard(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getExaminationBoard(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getExaminationBoard(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getExaminationBoard.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/examination-boards/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param id + * @param updateExaminationBoardDTO + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public updateExaminationBoard(id: number, updateExaminationBoardDTO: UpdateExaminationBoardDTO, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateExaminationBoard(id: number, updateExaminationBoardDTO: UpdateExaminationBoardDTO, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateExaminationBoard(id: number, updateExaminationBoardDTO: UpdateExaminationBoardDTO, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateExaminationBoard(id: number, updateExaminationBoardDTO: UpdateExaminationBoardDTO, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling updateExaminationBoard.'); + } + if (updateExaminationBoardDTO === null || updateExaminationBoardDTO === undefined) { + throw new Error('Required parameter updateExaminationBoardDTO was null or undefined when calling updateExaminationBoard.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/admin/examination-boards/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: updateExaminationBoardDTO, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts new file mode 100644 index 00000000..faebf7b6 --- /dev/null +++ b/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts @@ -0,0 +1,62 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { CreateExaminationBoardDTO } from '../model/models'; +import { ExaminationBoardDTO } from '../model/models'; +import { UpdateExaminationBoardDTO } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface ExaminationBoardControllerServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + * @param createExaminationBoardDTO + */ + createExaminationBoard(createExaminationBoardDTO: CreateExaminationBoardDTO, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param id + */ + deleteExaminationBoard(id: number, extraHttpRequestParams?: any): Observable<{}>; + + /** + * + * + */ + getAllExaminationBoards(extraHttpRequestParams?: any): Observable>; + + /** + * + * + * @param id + */ + getExaminationBoard(id: number, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param id + * @param updateExaminationBoardDTO + */ + updateExaminationBoard(id: number, updateExaminationBoardDTO: UpdateExaminationBoardDTO, extraHttpRequestParams?: any): Observable; + +} diff --git a/Client/src/app/core/modules/openapi/model/create-examination-board-dto.ts b/Client/src/app/core/modules/openapi/model/create-examination-board-dto.ts new file mode 100644 index 00000000..b008bfe4 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/create-examination-board-dto.ts @@ -0,0 +1,15 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface CreateExaminationBoardDTO { + name: string; +} + diff --git a/Client/src/app/core/modules/openapi/model/degree-program-dto.ts b/Client/src/app/core/modules/openapi/model/degree-program-dto.ts index e05d448f..972e097f 100644 --- a/Client/src/app/core/modules/openapi/model/degree-program-dto.ts +++ b/Client/src/app/core/modules/openapi/model/degree-program-dto.ts @@ -9,6 +9,7 @@ */ import { DegreeProgramSpecializationDTO } from './degree-program-specialization-dto'; import { ResponsibleUserDTO } from './responsible-user-dto'; +import { ExaminationBoardSummaryDTO } from './examination-board-summary-dto'; export interface DegreeProgramDTO { @@ -16,5 +17,6 @@ export interface DegreeProgramDTO { name: string; responsibleUser: ResponsibleUserDTO; degreeProgramSpecializations?: Array; + examinationBoard?: ExaminationBoardSummaryDTO; } diff --git a/Client/src/app/core/modules/openapi/model/examination-board-dto.ts b/Client/src/app/core/modules/openapi/model/examination-board-dto.ts new file mode 100644 index 00000000..c7583a8a --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/examination-board-dto.ts @@ -0,0 +1,18 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { ResponsibleUserDTO } from './responsible-user-dto'; + + +export interface ExaminationBoardDTO { + examinationBoardId?: number; + name?: string; + members?: Array; +} + diff --git a/Client/src/app/core/modules/openapi/model/examination-board-summary-dto.ts b/Client/src/app/core/modules/openapi/model/examination-board-summary-dto.ts new file mode 100644 index 00000000..27859737 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/examination-board-summary-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ExaminationBoardSummaryDTO { + examinationBoardId?: number; + name?: string; +} + diff --git a/Client/src/app/core/modules/openapi/model/models.ts b/Client/src/app/core/modules/openapi/model/models.ts index ea08504e..23bd5f0d 100644 --- a/Client/src/app/core/modules/openapi/model/models.ts +++ b/Client/src/app/core/modules/openapi/model/models.ts @@ -3,8 +3,11 @@ export * from './completion-service-request-dto'; export * from './completion-service-response-dto'; export * from './create-degree-program-dto'; export * from './create-degree-program-specialization-dto'; +export * from './create-examination-board-dto'; export * from './degree-program-dto'; export * from './degree-program-specialization-dto'; +export * from './examination-board-dto'; +export * from './examination-board-summary-dto'; export * from './feedback'; export * from './feedback-compact-dto'; export * from './feedback-dto'; @@ -23,6 +26,7 @@ export * from './responsible-user-dto'; export * from './similar-module-dto'; export * from './update-degree-program-dto'; export * from './update-degree-program-specialization-dto'; +export * from './update-examination-board-dto'; export * from './update-user-role-dto'; export * from './user'; export * from './user-dto'; diff --git a/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts b/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts index 34094e90..a95b3f1b 100644 --- a/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts +++ b/Client/src/app/core/modules/openapi/model/update-degree-program-dto.ts @@ -12,5 +12,6 @@ export interface UpdateDegreeProgramDTO { name?: string; responsibleUserId?: string; + examinationBoardId?: number; } diff --git a/Client/src/app/core/modules/openapi/model/update-examination-board-dto.ts b/Client/src/app/core/modules/openapi/model/update-examination-board-dto.ts new file mode 100644 index 00000000..380b4572 --- /dev/null +++ b/Client/src/app/core/modules/openapi/model/update-examination-board-dto.ts @@ -0,0 +1,16 @@ +/** + * OpenAPI definition + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface UpdateExaminationBoardDTO { + name: string; + userIds: Array; +} + From a06677096ce27f05f76a55d24800e84208169dd0 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 20:33:54 +0100 Subject: [PATCH 39/42] frontend admin routes --- Client/src/app/app.routes.ts | 2 ++ .../src/app/components/side-bar/side-bar.component.html | 8 ++++++++ Client/src/app/pages/index/index.component.html | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/Client/src/app/app.routes.ts b/Client/src/app/app.routes.ts index 7111c8b8..76117573 100644 --- a/Client/src/app/app.routes.ts +++ b/Client/src/app/app.routes.ts @@ -17,6 +17,7 @@ import { UsersPageComponent } from './pages/admin/users/users-page.component'; import { AllDegreeProgramsPageComponent } from './pages/admin/degree-programs/all-degree-programs-page.component'; import { DegreeProgramDetailsPageComponent } from './pages/admin/degree-programs/degree-program-details-page.component'; import { AllSpecializationsPageComponent } from './pages/admin/degree-program-specializations/all-specializations-page.component'; +import { ExaminationBoardsPageComponent } from './pages/admin/examination-boards/examination-boards-page.component'; export const routes: Routes = [ { path: '', component: IndexComponent }, { @@ -55,6 +56,7 @@ export const routes: Routes = [ canActivate: [AuthGuard, AdminGuard], children: [ { path: 'users', component: UsersPageComponent }, + { path: 'examination-boards', component: ExaminationBoardsPageComponent }, { path: 'degree-programs/specializations', component: AllSpecializationsPageComponent }, { path: 'degree-programs/:id', component: DegreeProgramDetailsPageComponent }, { path: 'degree-programs', component: AllDegreeProgramsPageComponent }, diff --git a/Client/src/app/components/side-bar/side-bar.component.html b/Client/src/app/components/side-bar/side-bar.component.html index f7007f4f..4aba109a 100644 --- a/Client/src/app/components/side-bar/side-bar.component.html +++ b/Client/src/app/components/side-bar/side-bar.component.html @@ -17,6 +17,14 @@ [style]="{ width: '100%' }" styleClass="sidebar-btn justify-start" /> + } @if (isProfessor()) { Welcome, {{ user()?.firstName }} {{ user

Manage degree programs and their areas of specialization.

+ +

Create examination boards, assign members, and link them to degree programs.

+ +
} @if (isProfessor()) { From 01ca8b8ff311265347d68d970b6212fbd53541e8 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 21:10:26 +0100 Subject: [PATCH 40/42] frontend manage exmination boards --- Client/src/app/app.routes.ts | 2 + .../breadcrumb/breadcrumb-labels.service.ts | 2 + .../breadcrumb/breadcrumb.component.ts | 14 ++ .../users-select/users-select.component.ts | 15 +- .../examination-board-controller.service.ts | 10 +- ...ation-board-controller.serviceInterface.ts | 3 +- ...degree-program-details-page.component.html | 14 ++ .../degree-program-details-page.component.ts | 30 ++- ...amination-board-detail-page.component.html | 74 ++++++++ ...examination-board-detail-page.component.ts | 172 ++++++++++++++++++ .../examination-boards-page.component.html | 72 ++++++++ .../examination-boards-page.component.ts | 95 ++++++++++ .../ExaminationBoardController.java | 2 +- .../ls1/services/ExaminationBoardService.java | 6 +- 14 files changed, 493 insertions(+), 18 deletions(-) create mode 100644 Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.html create mode 100644 Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.ts create mode 100644 Client/src/app/pages/admin/examination-boards/examination-boards-page.component.html create mode 100644 Client/src/app/pages/admin/examination-boards/examination-boards-page.component.ts diff --git a/Client/src/app/app.routes.ts b/Client/src/app/app.routes.ts index 76117573..e37bfa2f 100644 --- a/Client/src/app/app.routes.ts +++ b/Client/src/app/app.routes.ts @@ -17,6 +17,7 @@ import { UsersPageComponent } from './pages/admin/users/users-page.component'; import { AllDegreeProgramsPageComponent } from './pages/admin/degree-programs/all-degree-programs-page.component'; import { DegreeProgramDetailsPageComponent } from './pages/admin/degree-programs/degree-program-details-page.component'; import { AllSpecializationsPageComponent } from './pages/admin/degree-program-specializations/all-specializations-page.component'; +import { ExaminationBoardDetailPageComponent } from './pages/admin/examination-boards/examination-board-detail-page.component'; import { ExaminationBoardsPageComponent } from './pages/admin/examination-boards/examination-boards-page.component'; export const routes: Routes = [ { path: '', component: IndexComponent }, @@ -56,6 +57,7 @@ export const routes: Routes = [ canActivate: [AuthGuard, AdminGuard], children: [ { path: 'users', component: UsersPageComponent }, + { path: 'examination-boards/:id', component: ExaminationBoardDetailPageComponent }, { path: 'examination-boards', component: ExaminationBoardsPageComponent }, { path: 'degree-programs/specializations', component: AllSpecializationsPageComponent }, { path: 'degree-programs/:id', component: DegreeProgramDetailsPageComponent }, diff --git a/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts b/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts index 071fb279..5bd80f93 100644 --- a/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts +++ b/Client/src/app/components/breadcrumb/breadcrumb-labels.service.ts @@ -5,6 +5,8 @@ import { Injectable, signal } from '@angular/core'; export class BreadcrumbLabelsService { /** Degree program details page: program name. */ readonly degreeProgramName = signal(null); + /** Examination board detail page: board name. */ + readonly examinationBoardName = signal(null); /** Proposal/view segment: module title (e.g. from latestModuleVersion.titleEng). */ readonly proposalTitle = signal(null); /** Version segment: e.g. "Version 2" from moduleVersion.version. */ diff --git a/Client/src/app/components/breadcrumb/breadcrumb.component.ts b/Client/src/app/components/breadcrumb/breadcrumb.component.ts index eadb0f58..f9d11fd1 100644 --- a/Client/src/app/components/breadcrumb/breadcrumb.component.ts +++ b/Client/src/app/components/breadcrumb/breadcrumb.component.ts @@ -48,6 +48,20 @@ export class BreadcrumbComponent { return items; } + if (segments[1] === 'examination-boards') { + items.push({ label: 'Examination boards', routerLink: ['/admin/examination-boards'] }); + if (segments.length > 2 && segments[2] && segments[2] !== 'specializations') { + const boardId = segments[2]; + const label = + (this.breadcrumbLabels.examinationBoardName() ?? '').trim() || `Board ${boardId}`; + items.push({ + label, + routerLink: ['/admin/examination-boards', boardId] + }); + } + return items; + } + if (segments[1] === 'degree-programs') { items.push({ label: 'Degree Programs', routerLink: ['/admin/degree-programs'] }); if (segments.length <= 2) return items; diff --git a/Client/src/app/components/users-select/users-select.component.ts b/Client/src/app/components/users-select/users-select.component.ts index 3630113f..db50e8a4 100644 --- a/Client/src/app/components/users-select/users-select.component.ts +++ b/Client/src/app/components/users-select/users-select.component.ts @@ -25,6 +25,8 @@ export class UsersSelectComponent implements ControlValueAccessor { styleClass = input(''); /** When the selected user is not yet in the loaded list, pass this so they are displayed like other options. */ selectedUser = input(null); + /** User IDs to omit from the list (e.g. users already assigned elsewhere). */ + excludeUserIds = input([]); constructor() { this.loadUsers(); @@ -48,12 +50,15 @@ export class UsersSelectComponent implements ControlValueAccessor { } userOptions = computed(() => { - const list = this.users().map((u) => ({ - label: this.formatUserLabel(u), - value: u.userId - })); + const exclude = new Set(this.excludeUserIds().filter((id): id is string => !!id)); + const list = this.users() + .filter((u) => u.userId && !exclude.has(u.userId)) + .map((u) => ({ + label: this.formatUserLabel(u), + value: u.userId + })); const current = this.value(); - if (current && !list.some((o) => o.value === current)) { + if (current && !exclude.has(current) && !list.some((o) => o.value === current)) { const u = this.selectedUser(); const label = u ? this.formatUserLabel(u) : current; return [{ label, value: current }, ...list]; diff --git a/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts b/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts index f0798b51..54c2db60 100644 --- a/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts +++ b/Client/src/app/core/modules/openapi/api/examination-board-controller.service.ts @@ -21,6 +21,8 @@ import { CreateExaminationBoardDTO } from '../model/create-examination-board-dto // @ts-ignore import { ExaminationBoardDTO } from '../model/examination-board-dto'; // @ts-ignore +import { ExaminationBoardSummaryDTO } from '../model/examination-board-summary-dto'; +// @ts-ignore import { UpdateExaminationBoardDTO } from '../model/update-examination-board-dto'; // @ts-ignore @@ -236,9 +238,9 @@ export class ExaminationBoardControllerService implements ExaminationBoardContro * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getAllExaminationBoards(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getAllExaminationBoards(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getAllExaminationBoards(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllExaminationBoards(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAllExaminationBoards(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAllExaminationBoards(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; public getAllExaminationBoards(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { let localVarHeaders = this.defaultHeaders; @@ -278,7 +280,7 @@ export class ExaminationBoardControllerService implements ExaminationBoardContro } let localVarPath = `/api/admin/examination-boards`; - return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, diff --git a/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts b/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts index faebf7b6..8d3e2f35 100644 --- a/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts +++ b/Client/src/app/core/modules/openapi/api/examination-board-controller.serviceInterface.ts @@ -13,6 +13,7 @@ import { Observable } from 'rxjs'; import { CreateExaminationBoardDTO } from '../model/models'; import { ExaminationBoardDTO } from '../model/models'; +import { ExaminationBoardSummaryDTO } from '../model/models'; import { UpdateExaminationBoardDTO } from '../model/models'; @@ -42,7 +43,7 @@ export interface ExaminationBoardControllerServiceInterface { * * */ - getAllExaminationBoards(extraHttpRequestParams?: any): Observable>; + getAllExaminationBoards(extraHttpRequestParams?: any): Observable>; /** * diff --git a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html index dd096001..604f5b4f 100644 --- a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html +++ b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.html @@ -19,6 +19,20 @@

Program details

styleClass="w-full" />
+
+ + +
diff --git a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts index afe422a9..216d520d 100644 --- a/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts +++ b/Client/src/app/pages/admin/degree-programs/degree-program-details-page.component.ts @@ -6,6 +6,7 @@ import { ButtonModule } from 'primeng/button'; import { InputTextModule } from 'primeng/inputtext'; import { DialogModule } from 'primeng/dialog'; import { MultiSelectModule } from 'primeng/multiselect'; +import { SelectModule } from 'primeng/select'; import { TooltipModule } from 'primeng/tooltip'; import { MessageService } from 'primeng/api'; import { ToastModule } from 'primeng/toast'; @@ -13,9 +14,11 @@ import { firstValueFrom } from 'rxjs'; import { DegreeProgramSpecializationsControllerService, DegreeProgramsControllerService, + ExaminationBoardControllerService, type CreateDegreeProgramSpecializationDTO, type DegreeProgramDTO, - type DegreeProgramSpecializationDTO + type DegreeProgramSpecializationDTO, + type ExaminationBoardSummaryDTO } from '../../../core/modules/openapi'; import { UsersSelectComponent } from '../../../components/users-select/users-select.component'; import { BreadcrumbLabelsService } from '../../../components/breadcrumb/breadcrumb-labels.service'; @@ -23,13 +26,14 @@ import { BreadcrumbLabelsService } from '../../../components/breadcrumb/breadcru @Component({ selector: 'app-degree-program-details-page', standalone: true, - imports: [RouterLink, FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, MultiSelectModule, TooltipModule, ToastModule, UsersSelectComponent], + imports: [RouterLink, FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, MultiSelectModule, SelectModule, TooltipModule, ToastModule, UsersSelectComponent], templateUrl: './degree-program-details-page.component.html' }) export class DegreeProgramDetailsPageComponent { private readonly route = inject(ActivatedRoute); private readonly degreeProgramsService = inject(DegreeProgramsControllerService); private readonly specializationsService = inject(DegreeProgramSpecializationsControllerService); + private readonly examinationBoardsService = inject(ExaminationBoardControllerService); private readonly messageService = inject(MessageService); private readonly breadcrumbLabels = inject(BreadcrumbLabelsService); @@ -43,6 +47,13 @@ export class DegreeProgramDetailsPageComponent { programName = signal(''); programResponsibleUserId = signal(null); + allExaminationBoards = signal([]); + programExaminationBoardId = signal(null); + examinationBoardOptions = computed(() => { + const boards = this.allExaminationBoards(); + return [{ label: '— No examination board —', value: null as number | null }, ...boards.map((b) => ({ label: b.name ?? '', value: b.examinationBoardId }))]; + }); + newSpecName = signal(''); newSpecResponsibleUserId = signal(null); @@ -65,10 +76,15 @@ export class DegreeProgramDetailsPageComponent { async loadProgram(id: number) { this.loading.set(true); try { - const program = await firstValueFrom(this.degreeProgramsService.getDegreeProgram(id)); + const [program, boards] = await Promise.all([ + firstValueFrom(this.degreeProgramsService.getDegreeProgram(id)), + firstValueFrom(this.examinationBoardsService.getAllExaminationBoards()) + ]); this.program.set(program); + this.allExaminationBoards.set(boards ?? []); this.programName.set(program.name ?? ''); this.programResponsibleUserId.set(program.responsibleUser.userId ?? null); + this.programExaminationBoardId.set(program.examinationBoard?.examinationBoardId ?? null); this.breadcrumbLabels.degreeProgramName.set(program.name ?? null); await this.loadAllSpecializations(); } catch (e) { @@ -90,7 +106,13 @@ export class DegreeProgramDetailsPageComponent { } this.savingProgram.set(true); try { - await firstValueFrom(this.degreeProgramsService.updateDegreeProgram(prog.degreeProgramId, { name: nameVal, responsibleUserId: userIdVal })); + await firstValueFrom( + this.degreeProgramsService.updateDegreeProgram(prog.degreeProgramId, { + name: nameVal, + responsibleUserId: userIdVal, + examinationBoardId: this.programExaminationBoardId() ?? undefined + }) + ); this.messageService.add({ severity: 'success', summary: 'Updated', detail: 'Program details saved.' }); await this.loadProgram(prog.degreeProgramId); } catch (e) { diff --git a/Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.html b/Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.html new file mode 100644 index 00000000..26cffa5b --- /dev/null +++ b/Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.html @@ -0,0 +1,74 @@ + + +@if (board(); as b) { +

Examination board – {{ b.name }}

+ +
+

Board details

+
+
+ + +
+ +
+
+ +

Manage users who belong to this examination board. Add members below or remove them from the list.

+ +
+
+ + +
+
+ +

Members

+ @if (b.members && b.members.length > 0) { + + + + Name + Email + Actions + + + + + {{ memberDisplayName(m) }} + {{ m.email ?? '—' }} + + + + + + + } @else { +

No members on this board yet. Use Add member above to assign users.

+ } +} @else if (!loading()) { +

Examination board not found.

+} @else { +

Loading…

+} + + +
+
+ + +
+
+ + + + +
diff --git a/Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.ts b/Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.ts new file mode 100644 index 00000000..f9b9bb01 --- /dev/null +++ b/Client/src/app/pages/admin/examination-boards/examination-board-detail-page.component.ts @@ -0,0 +1,172 @@ +import { Component, OnDestroy, inject, signal } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { TableModule } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { MessageService } from 'primeng/api'; +import { ToastModule } from 'primeng/toast'; +import { DialogModule } from 'primeng/dialog'; +import { TooltipModule } from 'primeng/tooltip'; +import { firstValueFrom } from 'rxjs'; +import { ExaminationBoardControllerService, type ExaminationBoardDTO, type ResponsibleUserDTO } from '../../../core/modules/openapi'; +import { BreadcrumbLabelsService } from '../../../components/breadcrumb/breadcrumb-labels.service'; +import { UsersSelectComponent } from '../../../components/users-select/users-select.component'; + +@Component({ + selector: 'app-examination-board-detail-page', + standalone: true, + imports: [FormsModule, TableModule, ButtonModule, InputTextModule, ToastModule, DialogModule, TooltipModule, UsersSelectComponent], + templateUrl: './examination-board-detail-page.component.html' +}) +export class ExaminationBoardDetailPageComponent implements OnDestroy { + private readonly route = inject(ActivatedRoute); + private readonly examinationBoardsService = inject(ExaminationBoardControllerService); + private readonly messageService = inject(MessageService); + private readonly breadcrumbLabels = inject(BreadcrumbLabelsService); + + board = signal(null); + loading = signal(true); + saving = signal(false); + + editName = ''; + userIdToAdd: string | null = null; + addMemberDialogVisible = signal(false); + + constructor() { + this.route.paramMap.subscribe((params) => { + const id = params.get('id'); + if (id != null) { + void this.loadBoard(Number(id)); + } else { + this.board.set(null); + } + }); + } + + ngOnDestroy(): void { + this.breadcrumbLabels.examinationBoardName.set(null); + } + + async loadBoard(id: number) { + this.loading.set(true); + this.breadcrumbLabels.examinationBoardName.set(null); + try { + const b = await firstValueFrom(this.examinationBoardsService.getExaminationBoard(id)); + this.board.set(b); + this.editName = b.name ?? ''; + this.breadcrumbLabels.examinationBoardName.set(b.name ?? null); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Examination board not found.' }); + this.board.set(null); + this.breadcrumbLabels.examinationBoardName.set(null); + } finally { + this.loading.set(false); + } + } + + memberDisplayName(m: ResponsibleUserDTO): string { + const name = [m.firstName, m.lastName].filter(Boolean).join(' ').trim(); + if (name) return name; + return (m.email ?? m.userId ?? '—').trim(); + } + + currentMemberUserIds(): string[] { + return (this.board()?.members ?? []).map((m) => m.userId!).filter(Boolean); + } + + openAddMemberDialog() { + this.userIdToAdd = null; + this.addMemberDialogVisible.set(true); + } + + onAddMemberDialogVisibleChange(visible: boolean) { + this.addMemberDialogVisible.set(visible); + if (!visible) { + this.userIdToAdd = null; + } + } + + closeAddMemberDialog() { + this.onAddMemberDialogVisibleChange(false); + } + + async saveName() { + const b = this.board(); + const id = b?.examinationBoardId; + const name = this.editName.trim(); + if (!b || id == null || !name) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Name is required.' }); + return; + } + this.saving.set(true); + try { + await firstValueFrom( + this.examinationBoardsService.updateExaminationBoard(id, { + name, + userIds: this.currentMemberUserIds() + }) + ); + this.messageService.add({ severity: 'success', summary: 'Saved', detail: 'Board details updated.' }); + await this.loadBoard(id); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save name.' }); + } finally { + this.saving.set(false); + } + } + + async removeMember(userId: string) { + const b = this.board(); + const id = b?.examinationBoardId; + if (!b || id == null) return; + const next = this.currentMemberUserIds().filter((uid) => uid !== userId); + this.saving.set(true); + try { + await firstValueFrom( + this.examinationBoardsService.updateExaminationBoard(id, { + name: b.name ?? '', + userIds: next + }) + ); + this.messageService.add({ severity: 'success', summary: 'Updated', detail: 'Member removed.' }); + await this.loadBoard(id); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to remove member.' }); + } finally { + this.saving.set(false); + } + } + + async addMember() { + const b = this.board(); + const id = b?.examinationBoardId; + const uid = this.userIdToAdd; + if (!b || id == null || !uid) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Select a user to add.' }); + return; + } + if (this.currentMemberUserIds().includes(uid)) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'That user is already on this board.' }); + return; + } + const merged = [...new Set([...this.currentMemberUserIds(), uid])]; + this.saving.set(true); + try { + await firstValueFrom( + this.examinationBoardsService.updateExaminationBoard(id, { + name: b.name ?? '', + userIds: merged + }) + ); + this.messageService.add({ severity: 'success', summary: 'Updated', detail: 'Member added.' }); + this.userIdToAdd = null; + this.closeAddMemberDialog(); + await this.loadBoard(id); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to add member.' }); + } finally { + this.saving.set(false); + } + } +} diff --git a/Client/src/app/pages/admin/examination-boards/examination-boards-page.component.html b/Client/src/app/pages/admin/examination-boards/examination-boards-page.component.html new file mode 100644 index 00000000..c4f5d702 --- /dev/null +++ b/Client/src/app/pages/admin/examination-boards/examination-boards-page.component.html @@ -0,0 +1,72 @@ +

Examination boards

+

Create examination boards and edit a board to manage members. Link boards to degree programs on each program’s detail page.

+ +
+

All boards

+ +
+ +@if (!loading()) { + + + + Name + Actions + + + + + + @if (b.examinationBoardId != null) { +
+ {{ b.name }} + + } @else { + {{ b.name }} + } + + + @if (b.examinationBoardId != null) { + + + + + } + + + + + + No examination boards yet. + + + +} @else { +

Loading…

+} + + +
+ + +
+ + + + +
diff --git a/Client/src/app/pages/admin/examination-boards/examination-boards-page.component.ts b/Client/src/app/pages/admin/examination-boards/examination-boards-page.component.ts new file mode 100644 index 00000000..c4058858 --- /dev/null +++ b/Client/src/app/pages/admin/examination-boards/examination-boards-page.component.ts @@ -0,0 +1,95 @@ +import { Component, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { TableModule } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { DialogModule } from 'primeng/dialog'; +import { MessageService } from 'primeng/api'; +import { ToastModule } from 'primeng/toast'; +import { TooltipModule } from 'primeng/tooltip'; +import { firstValueFrom } from 'rxjs'; +import { ExaminationBoardControllerService, type ExaminationBoardSummaryDTO } from '../../../core/modules/openapi'; + +@Component({ + selector: 'app-examination-boards-page', + standalone: true, + imports: [RouterLink, FormsModule, TableModule, ButtonModule, InputTextModule, DialogModule, ToastModule, TooltipModule], + templateUrl: './examination-boards-page.component.html' +}) +export class ExaminationBoardsPageComponent { + private readonly examinationBoardsService = inject(ExaminationBoardControllerService); + private readonly messageService = inject(MessageService); + + boards = signal([]); + loading = signal(true); + saving = signal(false); + + createDialogVisible = signal(false); + createBoardName = ''; + + constructor() { + this.loadBoards(); + } + + async loadBoards() { + this.loading.set(true); + try { + const list = await firstValueFrom(this.examinationBoardsService.getAllExaminationBoards()); + this.boards.set(list ?? []); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to load examination boards.' }); + this.boards.set([]); + } finally { + this.loading.set(false); + } + } + + openCreateDialog() { + this.createBoardName = ''; + this.createDialogVisible.set(true); + } + + onCreateDialogVisibleChange(visible: boolean) { + this.createDialogVisible.set(visible); + if (!visible) { + this.createBoardName = ''; + } + } + + closeCreateDialog() { + this.onCreateDialogVisibleChange(false); + } + + async createBoard() { + const name = this.createBoardName.trim(); + if (!name) { + this.messageService.add({ severity: 'warn', summary: 'Validation', detail: 'Enter a name.' }); + return; + } + this.saving.set(true); + try { + await firstValueFrom(this.examinationBoardsService.createExaminationBoard({ name })); + this.messageService.add({ severity: 'success', summary: 'Created', detail: 'Examination board created.' }); + this.closeCreateDialog(); + await this.loadBoards(); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to create examination board.' }); + } finally { + this.saving.set(false); + } + } + + async deleteBoard(board: ExaminationBoardSummaryDTO) { + const id = board.examinationBoardId; + if (id == null) return; + if (!confirm(`Delete examination board "${board.name}"?`)) return; + try { + await firstValueFrom(this.examinationBoardsService.deleteExaminationBoard(id)); + this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Examination board deleted.' }); + await this.loadBoards(); + } catch { + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to delete examination board.' }); + } + } +} diff --git a/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java b/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java index 304da11c..6889d453 100644 --- a/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java +++ b/Server/src/main/java/modulemanagement/ls1/controllers/ExaminationBoardController.java @@ -21,7 +21,7 @@ public ExaminationBoardController(ExaminationBoardService examinationBoardServic } @GetMapping - public ResponseEntity> getAllExaminationBoards() { + public ResponseEntity> getAllExaminationBoards() { return ResponseEntity.ok(examinationBoardService.getAllExaminationBoards()); } diff --git a/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java b/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java index 03b8b668..5e9b564b 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ExaminationBoardService.java @@ -34,9 +34,9 @@ public ExaminationBoardService(ExaminationBoardRepository examinationBoardReposi this.userRolesSyncService = userRolesSyncService; } - public List getAllExaminationBoards() { - return examinationBoardRepository.findAllWithMembers().stream() - .map(ExaminationBoardDTO::fromEntity) + public List getAllExaminationBoards() { + return examinationBoardRepository.findAll().stream() + .map(ExaminationBoardSummaryDTO::fromEntity) .collect(Collectors.toList()); } From 4e1edd0f17070169f733f8299aa7055664319922 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Sun, 22 Mar 2026 22:05:07 +0100 Subject: [PATCH 41/42] add separate examination board submission --- .../create-edit-base.component.html | 46 ++++------ .../create-edit-base.component.ts | 41 ++++----- .../module-edit-steps.config.ts | 5 ++ .../module-version-stepper-status.util.ts | 83 +++++++++++++++++++ .../model/module-version-compact-dto.ts | 15 ++-- .../module-version-update-request-dto.ts | 15 ++-- .../openapi/model/module-version-view-dto.ts | 15 ++-- .../openapi/model/proposal-view-dto.ts | 15 ++-- .../openapi/model/proposals-compact-dto.ts | 15 ++-- .../module-version-view.component.html | 27 ++++-- .../module-version-view.component.ts | 24 +++--- .../src/app/pipes/moduleVersionStatus.pipe.ts | 41 ++++++--- Client/src/app/pipes/proposalStatus.pipe.ts | 52 +++++++----- .../ls1/enums/ModuleVersionStatus.java | 23 +++-- .../ls1/enums/ProposalStatus.java | 29 +++++-- .../ls1/services/ModuleVersionService.java | 27 +++--- .../ls1/services/ProposalService.java | 22 ++--- 17 files changed, 329 insertions(+), 166 deletions(-) create mode 100644 Client/src/app/components/module-edit-stepper/module-version-stepper-status.util.ts diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index 14c3be04..0118ba02 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -765,41 +765,31 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for '
@if (isCreateMode()) { - Complete all steps and get coordinator feedback accepted first. Then you can submit for full feedback (quality management, program advisor, examination board). + Complete all steps and get coordinator feedback accepted first. You will then submit for examination board feedback, and after that for quality management feedback. } @else { -

- Submit for full feedback from quality management, academic program advisor, and examination board. This requires all steps to be completed and all program/area - coordinator feedback to be accepted. -

- - @if (canRequestFullFeedback()) { + } +
+ } + + @if (currentStepIndex() === 7) { +
+ @if (isCreateMode()) { + + Complete all steps and get coordinator feedback accepted first, then submit for examination board feedback. After that you can submit for quality management feedback. + + } @else { +

Submit for feedback from quality management. This requires all previous steps to be completed.

+ @if (canRequestQualityAdvisorFeedback()) { diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.ts b/Client/src/app/components/create-edit-base/create-edit-base.component.ts index 17fbf382..0bf417c5 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.ts +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.ts @@ -16,7 +16,9 @@ import { Location } from '@angular/common'; import { Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { MODULE_EDIT_STEPS, StepperStatus } from '../module-edit-stepper/module-edit-steps.config'; +import { coordinatorFeedbackStepStatus, examinationBoardFeedbackStepStatus, qualityManagementFeedbackStepStatus } from '../module-edit-stepper/module-version-stepper-status.util'; import { BreadcrumbLabelsService } from '../breadcrumb/breadcrumb-labels.service'; +import { Observable } from 'rxjs'; @Component({ template: '' @@ -53,6 +55,7 @@ export abstract class ProposalBaseComponent { this.formValueVersion(); const form = this.proposalForm; const assignmentsList = this.assignments(); + const mvStatus = this.moduleVersionStatus(); return MODULE_EDIT_STEPS.map((step) => { if (step.id === 'basic') { const allFieldsFilled = step.controlNames.every((name) => this.controlHasValue(form.get(name))); @@ -64,19 +67,13 @@ export abstract class ProposalBaseComponent { } } if (step.id === 'submit-coordinator-feedback') { - const feedbacks = this.coordinatorFeedbacksForStep1().feedbacks; - if (feedbacks.length === 0) return StepperStatus.Default; - const pending = ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.PendingFeedback; - const approved = ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Approved; - const rejected = ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Rejected; - if (feedbacks.some((fb) => fb.feedbackStatus === rejected)) return StepperStatus.Rejected; - if (feedbacks.some((fb) => (fb.feedbackStatus ?? pending) === pending)) return StepperStatus.Pending; - if (feedbacks.every((fb) => fb.feedbackStatus === approved)) return StepperStatus.Completed; - return StepperStatus.FeedbackGiven; + return coordinatorFeedbackStepStatus(mvStatus); + } + if (step.id === 'submit-examination-board-feedback') { + return examinationBoardFeedbackStepStatus(mvStatus); } - if (step.id === 'submit-full-feedback') { - return StepperStatus.Default; + return qualityManagementFeedbackStepStatus(mvStatus); } return step.controlNames.every((name) => this.controlHasValue(form.get(name))) ? StepperStatus.Completed : StepperStatus.Default; }); @@ -102,17 +99,21 @@ export abstract class ProposalBaseComponent { /** Can submit for feedback (never submitted yet). */ canRequestCoordinatorsFeedback = computed(() => { const status = this.moduleVersionStatus(); - return status === 'PENDING_FIRST_SUBMISSION' && this.isFirstStepComplete(); + return status === 'WAITING_FOR_COORDINATORS_SUBMISSION' && this.isFirstStepComplete(); + }); + + /** Steps 0–5 must be complete to request examination board feedback. */ + canRequestExaminationBoardFeedback = computed(() => { + const statuses = this.stepsStatuses(); + const throughContentSteps = statuses.slice(0, 6); + return this.moduleVersionStatus() === 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' && throughContentSteps.every((s) => s === StepperStatus.Completed); }); - /** Can submit for full feedback (second submission): PENDING_FULL_SUBMISSION, all steps done, all coordinator feedback accepted. */ - canRequestFullFeedback = computed(() => { - return ( - this.moduleVersionStatus() === 'PENDING_FULL_SUBMISSION' && - this.stepsStatuses() - .slice(0, 6) - .every((s) => s === StepperStatus.Completed) - ); + /** All steps before quality submission (indices 0–6) complete, including examination board step. */ + canRequestQualityAdvisorFeedback = computed(() => { + const statuses = this.stepsStatuses(); + const beforeQualityAdvisor = statuses.slice(0, MODULE_EDIT_STEPS.length - 1); + return this.moduleVersionStatus() === 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' && beforeQualityAdvisor.every((s) => s === StepperStatus.Completed); }); /** Coordinator feedbacks for this version (for current assignments). From moduleVersionDto().feedbacks. */ diff --git a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts index 5ce82ac2..dfc95669 100644 --- a/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts +++ b/Client/src/app/components/module-edit-stepper/module-edit-steps.config.ts @@ -63,6 +63,11 @@ export const MODULE_EDIT_STEPS: ModuleEditStepConfig[] = [ title: 'Media, literature & responsibles', controlNames: ['mediaEng', 'literatureEng', 'responsiblesEng', 'lvSwsLecturerEng'] }, + { + id: 'submit-examination-board-feedback', + title: 'Submit for examination board feedback', + controlNames: [] + }, { id: 'submit-full-feedback', title: 'Submit for full feedback', diff --git a/Client/src/app/components/module-edit-stepper/module-version-stepper-status.util.ts b/Client/src/app/components/module-edit-stepper/module-version-stepper-status.util.ts new file mode 100644 index 00000000..8d9fbf8e --- /dev/null +++ b/Client/src/app/components/module-edit-stepper/module-version-stepper-status.util.ts @@ -0,0 +1,83 @@ +import { ModuleVersionViewDTO } from '../../core/modules/openapi'; +import { StepperStatus } from './module-edit-steps.config'; + +type Status = ModuleVersionViewDTO.StatusEnum; + +export function coordinatorFeedbackStepStatus(status: Status | undefined): StepperStatus { + if (!status) return StepperStatus.Default; + switch (status) { + case 'WAITING_FOR_COORDINATORS_SUBMISSION': + return StepperStatus.Default; + case 'PENDING_COORDINATORS_FEEDBACK': + return StepperStatus.Pending; + case 'COORDINATORS_FEEDBACK_GIVEN': + return StepperStatus.FeedbackGiven; + case 'REQUIRES_REVIEW': + return StepperStatus.FeedbackGiven; + case 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION': + case 'PENDING_EXAMINATION_BOARD_FEEDBACK': + case 'EXAMINATION_BOARD_FEEDBACK_GIVEN': + case 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION': + case 'PENDING_QUALITY_MANAGEMENT_FEEDBACK': + case 'ACCEPTED': + return StepperStatus.Completed; + case 'REJECTED': + return StepperStatus.Rejected; + case 'CANCELLED': + return StepperStatus.Default; + default: + return StepperStatus.Default; + } +} +export function examinationBoardFeedbackStepStatus(status: Status | undefined): StepperStatus { + if (!status) return StepperStatus.Default; + switch (status) { + case 'WAITING_FOR_COORDINATORS_SUBMISSION': + case 'PENDING_COORDINATORS_FEEDBACK': + case 'COORDINATORS_FEEDBACK_GIVEN': + case 'REQUIRES_REVIEW': + return StepperStatus.Default; + case 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION': + return StepperStatus.Default; + case 'PENDING_EXAMINATION_BOARD_FEEDBACK': + return StepperStatus.Pending; + case 'EXAMINATION_BOARD_FEEDBACK_GIVEN': + return StepperStatus.FeedbackGiven; + case 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION': + case 'PENDING_QUALITY_MANAGEMENT_FEEDBACK': + case 'ACCEPTED': + return StepperStatus.Completed; + case 'REJECTED': + return StepperStatus.Rejected; + case 'CANCELLED': + return StepperStatus.Default; + default: + return StepperStatus.Default; + } +} + +export function qualityManagementFeedbackStepStatus(status: Status | undefined): StepperStatus { + if (!status) return StepperStatus.Default; + switch (status) { + case 'WAITING_FOR_COORDINATORS_SUBMISSION': + case 'PENDING_COORDINATORS_FEEDBACK': + case 'COORDINATORS_FEEDBACK_GIVEN': + case 'REQUIRES_REVIEW': + case 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION': + case 'PENDING_EXAMINATION_BOARD_FEEDBACK': + case 'EXAMINATION_BOARD_FEEDBACK_GIVEN': + return StepperStatus.Default; + case 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION': + return StepperStatus.Default; + case 'PENDING_QUALITY_MANAGEMENT_FEEDBACK': + return StepperStatus.Pending; + case 'ACCEPTED': + return StepperStatus.Completed; + case 'REJECTED': + return StepperStatus.Rejected; + case 'CANCELLED': + return StepperStatus.Default; + default: + return StepperStatus.Default; + } +} diff --git a/Client/src/app/core/modules/openapi/model/module-version-compact-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-compact-dto.ts index ab14aad0..fbc011e2 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-compact-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-compact-dto.ts @@ -19,13 +19,16 @@ export interface ModuleVersionCompactDTO { feedbackList?: Array; } export namespace ModuleVersionCompactDTO { - export type StatusEnum = 'PENDING_FIRST_SUBMISSION' | 'PENDING_COORDINATOR_FEEDBACK' | 'COORDINATOR_FEEDBACK_GIVEN' | 'PENDING_FULL_SUBMISSION' | 'PENDING_FULL_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; + export type StatusEnum = 'WAITING_FOR_COORDINATORS_SUBMISSION' | 'PENDING_COORDINATORS_FEEDBACK' | 'COORDINATORS_FEEDBACK_GIVEN' | 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' | 'PENDING_EXAMINATION_BOARD_FEEDBACK' | 'EXAMINATION_BOARD_FEEDBACK_GIVEN' | 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' | 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; export const StatusEnum = { - PendingFirstSubmission: 'PENDING_FIRST_SUBMISSION' as StatusEnum, - PendingCoordinatorFeedback: 'PENDING_COORDINATOR_FEEDBACK' as StatusEnum, - CoordinatorFeedbackGiven: 'COORDINATOR_FEEDBACK_GIVEN' as StatusEnum, - PendingFullSubmission: 'PENDING_FULL_SUBMISSION' as StatusEnum, - PendingFullFeedback: 'PENDING_FULL_FEEDBACK' as StatusEnum, + WaitingForCoordinatorsSubmission: 'WAITING_FOR_COORDINATORS_SUBMISSION' as StatusEnum, + PendingCoordinatorsFeedback: 'PENDING_COORDINATORS_FEEDBACK' as StatusEnum, + CoordinatorsFeedbackGiven: 'COORDINATORS_FEEDBACK_GIVEN' as StatusEnum, + WaitingForExaminationBoardSubmission: 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' as StatusEnum, + PendingExaminationBoardFeedback: 'PENDING_EXAMINATION_BOARD_FEEDBACK' as StatusEnum, + ExaminationBoardFeedbackGiven: 'EXAMINATION_BOARD_FEEDBACK_GIVEN' as StatusEnum, + WaitingForQualityManagementSubmission: 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' as StatusEnum, + PendingQualityManagementFeedback: 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' as StatusEnum, Accepted: 'ACCEPTED' as StatusEnum, RequiresReview: 'REQUIRES_REVIEW' as StatusEnum, Rejected: 'REJECTED' as StatusEnum, diff --git a/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts index db5dff10..fad91dd0 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-update-request-dto.ts @@ -50,13 +50,16 @@ export interface ModuleVersionUpdateRequestDTO { lvSwsLecturerEng?: string; } export namespace ModuleVersionUpdateRequestDTO { - export type StatusEnum = 'PENDING_FIRST_SUBMISSION' | 'PENDING_COORDINATOR_FEEDBACK' | 'COORDINATOR_FEEDBACK_GIVEN' | 'PENDING_FULL_SUBMISSION' | 'PENDING_FULL_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; + export type StatusEnum = 'WAITING_FOR_COORDINATORS_SUBMISSION' | 'PENDING_COORDINATORS_FEEDBACK' | 'COORDINATORS_FEEDBACK_GIVEN' | 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' | 'PENDING_EXAMINATION_BOARD_FEEDBACK' | 'EXAMINATION_BOARD_FEEDBACK_GIVEN' | 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' | 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; export const StatusEnum = { - PendingFirstSubmission: 'PENDING_FIRST_SUBMISSION' as StatusEnum, - PendingCoordinatorFeedback: 'PENDING_COORDINATOR_FEEDBACK' as StatusEnum, - CoordinatorFeedbackGiven: 'COORDINATOR_FEEDBACK_GIVEN' as StatusEnum, - PendingFullSubmission: 'PENDING_FULL_SUBMISSION' as StatusEnum, - PendingFullFeedback: 'PENDING_FULL_FEEDBACK' as StatusEnum, + WaitingForCoordinatorsSubmission: 'WAITING_FOR_COORDINATORS_SUBMISSION' as StatusEnum, + PendingCoordinatorsFeedback: 'PENDING_COORDINATORS_FEEDBACK' as StatusEnum, + CoordinatorsFeedbackGiven: 'COORDINATORS_FEEDBACK_GIVEN' as StatusEnum, + WaitingForExaminationBoardSubmission: 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' as StatusEnum, + PendingExaminationBoardFeedback: 'PENDING_EXAMINATION_BOARD_FEEDBACK' as StatusEnum, + ExaminationBoardFeedbackGiven: 'EXAMINATION_BOARD_FEEDBACK_GIVEN' as StatusEnum, + WaitingForQualityManagementSubmission: 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' as StatusEnum, + PendingQualityManagementFeedback: 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' as StatusEnum, Accepted: 'ACCEPTED' as StatusEnum, RequiresReview: 'REQUIRES_REVIEW' as StatusEnum, Rejected: 'REJECTED' as StatusEnum, diff --git a/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts b/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts index f194668c..069c428d 100644 --- a/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts +++ b/Client/src/app/core/modules/openapi/model/module-version-view-dto.ts @@ -53,13 +53,16 @@ export interface ModuleVersionViewDTO { feedbacks?: Array; } export namespace ModuleVersionViewDTO { - export type StatusEnum = 'PENDING_FIRST_SUBMISSION' | 'PENDING_COORDINATOR_FEEDBACK' | 'COORDINATOR_FEEDBACK_GIVEN' | 'PENDING_FULL_SUBMISSION' | 'PENDING_FULL_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; + export type StatusEnum = 'WAITING_FOR_COORDINATORS_SUBMISSION' | 'PENDING_COORDINATORS_FEEDBACK' | 'COORDINATORS_FEEDBACK_GIVEN' | 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' | 'PENDING_EXAMINATION_BOARD_FEEDBACK' | 'EXAMINATION_BOARD_FEEDBACK_GIVEN' | 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' | 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; export const StatusEnum = { - PendingFirstSubmission: 'PENDING_FIRST_SUBMISSION' as StatusEnum, - PendingCoordinatorFeedback: 'PENDING_COORDINATOR_FEEDBACK' as StatusEnum, - CoordinatorFeedbackGiven: 'COORDINATOR_FEEDBACK_GIVEN' as StatusEnum, - PendingFullSubmission: 'PENDING_FULL_SUBMISSION' as StatusEnum, - PendingFullFeedback: 'PENDING_FULL_FEEDBACK' as StatusEnum, + WaitingForCoordinatorsSubmission: 'WAITING_FOR_COORDINATORS_SUBMISSION' as StatusEnum, + PendingCoordinatorsFeedback: 'PENDING_COORDINATORS_FEEDBACK' as StatusEnum, + CoordinatorsFeedbackGiven: 'COORDINATORS_FEEDBACK_GIVEN' as StatusEnum, + WaitingForExaminationBoardSubmission: 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' as StatusEnum, + PendingExaminationBoardFeedback: 'PENDING_EXAMINATION_BOARD_FEEDBACK' as StatusEnum, + ExaminationBoardFeedbackGiven: 'EXAMINATION_BOARD_FEEDBACK_GIVEN' as StatusEnum, + WaitingForQualityManagementSubmission: 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' as StatusEnum, + PendingQualityManagementFeedback: 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' as StatusEnum, Accepted: 'ACCEPTED' as StatusEnum, RequiresReview: 'REQUIRES_REVIEW' as StatusEnum, Rejected: 'REJECTED' as StatusEnum, diff --git a/Client/src/app/core/modules/openapi/model/proposal-view-dto.ts b/Client/src/app/core/modules/openapi/model/proposal-view-dto.ts index f1eda8fd..8b565f86 100644 --- a/Client/src/app/core/modules/openapi/model/proposal-view-dto.ts +++ b/Client/src/app/core/modules/openapi/model/proposal-view-dto.ts @@ -19,13 +19,16 @@ export interface ProposalViewDTO { oldModuleVersions?: Array; } export namespace ProposalViewDTO { - export type StatusEnum = 'PENDING_FIRST_SUBMISSION' | 'PENDING_COORDINATOR_FEEDBACK' | 'COORDINATOR_FEEDBACK_GIVEN' | 'PENDING_FULL_SUBMISSION' | 'PENDING_FULL_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; + export type StatusEnum = 'WAITING_FOR_COORDINATORS_SUBMISSION' | 'PENDING_COORDINATORS_FEEDBACK' | 'COORDINATORS_FEEDBACK_GIVEN' | 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' | 'PENDING_EXAMINATION_BOARD_FEEDBACK' | 'EXAMINATION_BOARD_FEEDBACK_GIVEN' | 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' | 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; export const StatusEnum = { - PendingFirstSubmission: 'PENDING_FIRST_SUBMISSION' as StatusEnum, - PendingCoordinatorFeedback: 'PENDING_COORDINATOR_FEEDBACK' as StatusEnum, - CoordinatorFeedbackGiven: 'COORDINATOR_FEEDBACK_GIVEN' as StatusEnum, - PendingFullSubmission: 'PENDING_FULL_SUBMISSION' as StatusEnum, - PendingFullFeedback: 'PENDING_FULL_FEEDBACK' as StatusEnum, + WaitingForCoordinatorsSubmission: 'WAITING_FOR_COORDINATORS_SUBMISSION' as StatusEnum, + PendingCoordinatorsFeedback: 'PENDING_COORDINATORS_FEEDBACK' as StatusEnum, + CoordinatorsFeedbackGiven: 'COORDINATORS_FEEDBACK_GIVEN' as StatusEnum, + WaitingForExaminationBoardSubmission: 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' as StatusEnum, + PendingExaminationBoardFeedback: 'PENDING_EXAMINATION_BOARD_FEEDBACK' as StatusEnum, + ExaminationBoardFeedbackGiven: 'EXAMINATION_BOARD_FEEDBACK_GIVEN' as StatusEnum, + WaitingForQualityManagementSubmission: 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' as StatusEnum, + PendingQualityManagementFeedback: 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' as StatusEnum, Accepted: 'ACCEPTED' as StatusEnum, RequiresReview: 'REQUIRES_REVIEW' as StatusEnum, Rejected: 'REJECTED' as StatusEnum, diff --git a/Client/src/app/core/modules/openapi/model/proposals-compact-dto.ts b/Client/src/app/core/modules/openapi/model/proposals-compact-dto.ts index b5b63747..69f8414b 100644 --- a/Client/src/app/core/modules/openapi/model/proposals-compact-dto.ts +++ b/Client/src/app/core/modules/openapi/model/proposals-compact-dto.ts @@ -17,13 +17,16 @@ export interface ProposalsCompactDTO { latestTitle?: string; } export namespace ProposalsCompactDTO { - export type StatusEnum = 'PENDING_FIRST_SUBMISSION' | 'PENDING_COORDINATOR_FEEDBACK' | 'COORDINATOR_FEEDBACK_GIVEN' | 'PENDING_FULL_SUBMISSION' | 'PENDING_FULL_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; + export type StatusEnum = 'WAITING_FOR_COORDINATORS_SUBMISSION' | 'PENDING_COORDINATORS_FEEDBACK' | 'COORDINATORS_FEEDBACK_GIVEN' | 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' | 'PENDING_EXAMINATION_BOARD_FEEDBACK' | 'EXAMINATION_BOARD_FEEDBACK_GIVEN' | 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' | 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' | 'ACCEPTED' | 'REQUIRES_REVIEW' | 'REJECTED' | 'CANCELLED'; export const StatusEnum = { - PendingFirstSubmission: 'PENDING_FIRST_SUBMISSION' as StatusEnum, - PendingCoordinatorFeedback: 'PENDING_COORDINATOR_FEEDBACK' as StatusEnum, - CoordinatorFeedbackGiven: 'COORDINATOR_FEEDBACK_GIVEN' as StatusEnum, - PendingFullSubmission: 'PENDING_FULL_SUBMISSION' as StatusEnum, - PendingFullFeedback: 'PENDING_FULL_FEEDBACK' as StatusEnum, + WaitingForCoordinatorsSubmission: 'WAITING_FOR_COORDINATORS_SUBMISSION' as StatusEnum, + PendingCoordinatorsFeedback: 'PENDING_COORDINATORS_FEEDBACK' as StatusEnum, + CoordinatorsFeedbackGiven: 'COORDINATORS_FEEDBACK_GIVEN' as StatusEnum, + WaitingForExaminationBoardSubmission: 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION' as StatusEnum, + PendingExaminationBoardFeedback: 'PENDING_EXAMINATION_BOARD_FEEDBACK' as StatusEnum, + ExaminationBoardFeedbackGiven: 'EXAMINATION_BOARD_FEEDBACK_GIVEN' as StatusEnum, + WaitingForQualityManagementSubmission: 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION' as StatusEnum, + PendingQualityManagementFeedback: 'PENDING_QUALITY_MANAGEMENT_FEEDBACK' as StatusEnum, Accepted: 'ACCEPTED' as StatusEnum, RequiresReview: 'REQUIRES_REVIEW' as StatusEnum, Rejected: 'REJECTED' as StatusEnum, diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.html b/Client/src/app/pages/module-version-view/module-version-view.component.html index 180e88b7..69a477eb 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.html +++ b/Client/src/app/pages/module-version-view/module-version-view.component.html @@ -87,7 +87,7 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ @if (currentStepIndex() === 1) { -

First submission: request feedback from program coordinators and area coordinators (one per chosen degree program/area).

+

Request feedback from program coordinators and area coordinators (one per chosen degree program/area).

@if (coordinatorFeedbacksForStep1().feedbacks.length > 0) {

Coordinator feedback status {{ coordinatorFeedbacksForStep1().fromPrevious ? '(from previous version)' : '(this version)' }}

@@ -102,7 +102,12 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ />

@if (fb.feedbackStatus === 'REJECTED' && fb.rejectionComment) { - + } } @@ -229,11 +234,14 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ } @if (currentStepIndex() === 6) { + +

Request feedback from the examination boards of the chosen degree programs.

+
+ } + + @if (currentStepIndex() === 7) { -

- Second submission: after all steps are complete and coordinator feedback is accepted, you can submit for feedback from quality management, academic program advisor, and - examination board. -

+

Request feedback from quality management feedback.

@if (moduleVersionDto.feedbacks?.length) {

Feedback status

@@ -248,7 +256,12 @@

Overview of '{{ moduleVersionDto.titleEng }}' - Version {{ />

@if (fb.feedbackStatus === 'REJECTED' && fb.rejectionComment) { - + } } diff --git a/Client/src/app/pages/module-version-view/module-version-view.component.ts b/Client/src/app/pages/module-version-view/module-version-view.component.ts index ba614c5b..011e9e79 100644 --- a/Client/src/app/pages/module-version-view/module-version-view.component.ts +++ b/Client/src/app/pages/module-version-view/module-version-view.component.ts @@ -16,6 +16,11 @@ import { MessageModule } from 'primeng/message'; import { CardModule } from 'primeng/card'; import { FeedbackMessageComponent } from '../../components/feedback-message/feedback-message.component'; import { PanelModule } from 'primeng/panel'; +import { + coordinatorFeedbackStepStatus, + examinationBoardFeedbackStepStatus, + qualityManagementFeedbackStepStatus +} from '../../components/module-edit-stepper/module-version-stepper-status.util'; export interface ModuleField { key: keyof ModuleVersionViewDTO; @@ -135,20 +140,15 @@ export class ModuleVersionViewComponent { if (!dto) return MODULE_EDIT_STEPS.map(() => StepperStatus.Default); return MODULE_EDIT_STEPS.map((step, index) => { - if (step.id === 'feedbacks') return StepperStatus.Default; if (step.id === 'submit-coordinator-feedback') { - const feedbacks = this.coordinatorFeedbacksForStep1().feedbacks; - if (feedbacks.length === 0) return StepperStatus.Default; - const pending = ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.PendingFeedback; - const approved = ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Approved; - const rejected = ModuleVersionViewFeedbackDTO.FeedbackStatusEnum.Rejected; - if (feedbacks.some((fb) => fb.feedbackStatus === rejected)) return StepperStatus.Rejected; - if (feedbacks.some((fb) => (fb.feedbackStatus ?? pending) === pending)) return StepperStatus.Pending; - if (feedbacks.every((fb) => fb.feedbackStatus === approved)) return StepperStatus.Completed; - return StepperStatus.FeedbackGiven; + return coordinatorFeedbackStepStatus(dto.status); + } + if (step.id === 'submit-examination-board-feedback') { + return examinationBoardFeedbackStepStatus(dto.status); + } + if (step.id === 'submit-full-feedback') { + return qualityManagementFeedbackStepStatus(dto.status); } - - if (step.id === 'submit-full-feedback') return StepperStatus.Default; const keys = MODULE_EDIT_STEPS[index].controlNames as (keyof ModuleVersionViewDTO)[]; if (!keys?.length) return StepperStatus.Default; diff --git a/Client/src/app/pipes/moduleVersionStatus.pipe.ts b/Client/src/app/pipes/moduleVersionStatus.pipe.ts index f8b06bad..23c2b005 100644 --- a/Client/src/app/pipes/moduleVersionStatus.pipe.ts +++ b/Client/src/app/pipes/moduleVersionStatus.pipe.ts @@ -14,37 +14,58 @@ export class ModuleVersionStatusPipe implements PipeTransform { severity: Tag['severity']; } { switch (status) { - case ModuleVersionCompactDTO.StatusEnum.PendingFirstSubmission: + case ModuleVersionCompactDTO.StatusEnum.WaitingForCoordinatorsSubmission: return { - text: 'Pending first submission', + text: 'Waiting for coordinator submission', normalColor: 'bg-gray-500', fadedColor: 'bg-gray-300', severity: 'secondary' }; - case ModuleVersionCompactDTO.StatusEnum.PendingCoordinatorFeedback: + case ModuleVersionCompactDTO.StatusEnum.PendingCoordinatorsFeedback: return { - text: 'Pending coordinator feedback', + text: 'Pending coordinators feedback', normalColor: 'bg-yellow-500', fadedColor: 'bg-yellow-300', severity: 'warn' }; - case ModuleVersionCompactDTO.StatusEnum.CoordinatorFeedbackGiven: + case ModuleVersionCompactDTO.StatusEnum.CoordinatorsFeedbackGiven: return { - text: 'Coordinator feedback given', + text: 'Coordinators feedback given', normalColor: 'bg-blue-500', fadedColor: 'bg-blue-300', severity: 'info' }; - case ModuleVersionCompactDTO.StatusEnum.PendingFullSubmission: + case ModuleVersionCompactDTO.StatusEnum.WaitingForExaminationBoardSubmission: return { - text: 'Pending full submission', + text: 'Waiting for examination board submission', normalColor: 'bg-gray-500', fadedColor: 'bg-gray-300', severity: 'secondary' }; - case ModuleVersionCompactDTO.StatusEnum.PendingFullFeedback: + case ModuleVersionCompactDTO.StatusEnum.PendingExaminationBoardFeedback: return { - text: 'Pending full feedback', + text: 'Pending examination board feedback', + normalColor: 'bg-yellow-500', + fadedColor: 'bg-yellow-300', + severity: 'warn' + }; + case ModuleVersionCompactDTO.StatusEnum.ExaminationBoardFeedbackGiven: + return { + text: 'Examination board feedback given', + normalColor: 'bg-blue-500', + fadedColor: 'bg-blue-300', + severity: 'info' + }; + case ModuleVersionCompactDTO.StatusEnum.WaitingForQualityManagementSubmission: + return { + text: 'Waiting for quality management submission', + normalColor: 'bg-gray-500', + fadedColor: 'bg-gray-300', + severity: 'secondary' + }; + case ModuleVersionCompactDTO.StatusEnum.PendingQualityManagementFeedback: + return { + text: 'Pending quality management feedback', normalColor: 'bg-yellow-500', fadedColor: 'bg-yellow-300', severity: 'warn' diff --git a/Client/src/app/pipes/proposalStatus.pipe.ts b/Client/src/app/pipes/proposalStatus.pipe.ts index f809b476..5e92527e 100644 --- a/Client/src/app/pipes/proposalStatus.pipe.ts +++ b/Client/src/app/pipes/proposalStatus.pipe.ts @@ -5,16 +5,22 @@ import { Tag } from 'primeng/tag'; export class StatusDisplayPipe implements PipeTransform { transform(status: ProposalViewDTO.StatusEnum): { text: string; severity: Tag['severity'] } { switch (status) { - case 'PENDING_FIRST_SUBMISSION': - return { text: 'Pending first submission', severity: 'secondary' }; - case 'PENDING_COORDINATOR_FEEDBACK': - return { text: 'Pending coordinator feedback', severity: 'warn' }; - case 'COORDINATOR_FEEDBACK_GIVEN': - return { text: 'Coordinator feedback given', severity: 'info' }; - case 'PENDING_FULL_SUBMISSION': - return { text: 'Pending full submission', severity: 'secondary' }; - case 'PENDING_FULL_FEEDBACK': - return { text: 'Pending full feedback', severity: 'warn' }; + case 'WAITING_FOR_COORDINATORS_SUBMISSION': + return { text: 'Waiting for coordinator submission', severity: 'secondary' }; + case 'PENDING_COORDINATORS_FEEDBACK': + return { text: 'Pending coordinators feedback', severity: 'warn' }; + case 'COORDINATORS_FEEDBACK_GIVEN': + return { text: 'Coordinators feedback given', severity: 'info' }; + case 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION': + return { text: 'Waiting for examination board submission', severity: 'secondary' }; + case 'PENDING_EXAMINATION_BOARD_FEEDBACK': + return { text: 'Pending examination board feedback', severity: 'warn' }; + case 'EXAMINATION_BOARD_FEEDBACK_GIVEN': + return { text: 'Examination board feedback given', severity: 'info' }; + case 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION': + return { text: 'Waiting for quality management submission', severity: 'secondary' }; + case 'PENDING_QUALITY_MANAGEMENT_FEEDBACK': + return { text: 'Pending quality management feedback', severity: 'warn' }; case 'ACCEPTED': return { text: 'Accepted', severity: 'success' }; case 'REQUIRES_REVIEW': @@ -31,16 +37,22 @@ export class StatusDisplayPipe implements PipeTransform { export class StatusInfoPipeline implements PipeTransform { transform(status: ProposalViewDTO.StatusEnum): string { switch (status) { - case 'PENDING_FIRST_SUBMISSION': - return 'This module proposal is pending submission. Please complete step 1 (basic information and degree program assignments) and submit for coordinator feedback.'; - case 'PENDING_COORDINATOR_FEEDBACK': - return 'Submitted for coordinator feedback. Program and area coordinators are reviewing. You can cancel and resubmit if needed.'; - case 'COORDINATOR_FEEDBACK_GIVEN': - return 'All coordinators have responded. None gave approval, but at least one gave feedback without approval; review their comments and update your module if needed.'; - case 'PENDING_FULL_SUBMISSION': - return 'Coordinator feedback was accepted. Complete all steps and submit for full feedback (quality management, program advisor, examination board) when ready.'; - case 'PENDING_FULL_FEEDBACK': - return 'This module proposal is submitted and waiting for review. You can cancel the submission if needed.'; + case 'WAITING_FOR_COORDINATORS_SUBMISSION': + return 'Complete step 1 and submit for coordinator feedback when ready.'; + case 'PENDING_COORDINATORS_FEEDBACK': + return 'Submitted for coordinator feedback. Program and area coordinators are reviewing.'; + case 'COORDINATORS_FEEDBACK_GIVEN': + return 'All coordinators have responded; at least one response needs your attention before approval.'; + case 'WAITING_FOR_EXAMINATION_BOARD_SUBMISSION': + return 'Coordinator feedback was accepted. Complete all steps, then submit for examination board feedback.'; + case 'PENDING_EXAMINATION_BOARD_FEEDBACK': + return 'Waiting for examination board feedback.'; + case 'EXAMINATION_BOARD_FEEDBACK_GIVEN': + return 'Examination board has responded; review feedback before requesting quality management feedback.'; + case 'WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION': + return 'Examination board feedback is complete. Submit for quality management feedback.'; + case 'PENDING_QUALITY_MANAGEMENT_FEEDBACK': + return 'Waiting for quality management feedback.'; case 'ACCEPTED': return 'This module is approved.'; case 'REQUIRES_REVIEW': diff --git a/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java b/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java index e42c86e0..20e57a86 100644 --- a/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java +++ b/Server/src/main/java/modulemanagement/ls1/enums/ModuleVersionStatus.java @@ -2,23 +2,32 @@ public enum ModuleVersionStatus { /** Not yet submitted for coordinator feedback (first submission). */ - PENDING_FIRST_SUBMISSION, + WAITING_FOR_COORDINATORS_SUBMISSION, /** * Submitted for coordinator feedback; waiting for program/area coordinators. */ - PENDING_COORDINATOR_FEEDBACK, + PENDING_COORDINATORS_FEEDBACK, /** * All coordinator responses are in; at least one gave non-approval feedback and * none approved yet. */ - COORDINATOR_FEEDBACK_GIVEN, + COORDINATORS_FEEDBACK_GIVEN, /** - * Coordinator feedback accepted; professor has not yet submitted for full + * Coordinator feedback accepted; not yet submitted for examination board * feedback. */ - PENDING_FULL_SUBMISSION, - /** Submitted for full feedback; waiting for QM, advisor, examination board. */ - PENDING_FULL_FEEDBACK, + WAITING_FOR_EXAMINATION_BOARD_SUBMISSION, + /** Submitted for examination board feedback; waiting for examination board. */ + PENDING_EXAMINATION_BOARD_FEEDBACK, + /** Examination board responses in; not all approved. */ + EXAMINATION_BOARD_FEEDBACK_GIVEN, + /** + * All examination board feedbacks approved; not yet submitted for quality + * management feedback. + */ + WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION, + /** Submitted for quality management feedback. */ + PENDING_QUALITY_MANAGEMENT_FEEDBACK, ACCEPTED, REQUIRES_REVIEW, REJECTED, diff --git a/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java b/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java index e4e7624f..6bf254bb 100644 --- a/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java +++ b/Server/src/main/java/modulemanagement/ls1/enums/ProposalStatus.java @@ -4,23 +4,36 @@ public enum ProposalStatus { /** * Professor has not yet submitted for coordinator feedback (first submission). */ - PENDING_FIRST_SUBMISSION, + WAITING_FOR_COORDINATORS_SUBMISSION, /** * Submitted for coordinator feedback; waiting for program/area coordinators. */ - PENDING_COORDINATOR_FEEDBACK, + PENDING_COORDINATORS_FEEDBACK, /** * All coordinator responses are in; at least one gave non-approval feedback and * none approved yet. */ - COORDINATOR_FEEDBACK_GIVEN, + COORDINATORS_FEEDBACK_GIVEN, /** - * Coordinator feedback accepted; professor has not yet submitted for full - * feedback. + * Coordinator feedback accepted; professor has not yet submitted for + * examination + * board feedback. */ - PENDING_FULL_SUBMISSION, - /** Submitted for full feedback; waiting for QM, advisor, examination board. */ - PENDING_FULL_FEEDBACK, + WAITING_FOR_EXAMINATION_BOARD_SUBMISSION, + /** Submitted for examination board feedback; waiting for examination board. */ + PENDING_EXAMINATION_BOARD_FEEDBACK, + /** + * Examination board responses are in; at least one is not approval (e.g. + * FEEDBACK_GIVEN), or mixed. + */ + EXAMINATION_BOARD_FEEDBACK_GIVEN, + /** + * All examination board feedbacks approved; professor has not yet submitted for + * quality management feedback. + */ + WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION, + /** Submitted for quality management feedback. */ + PENDING_QUALITY_MANAGEMENT_FEEDBACK, ACCEPTED, REQUIRES_REVIEW, REJECTED, diff --git a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java index 7d20f860..3555c2bc 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ModuleVersionService.java @@ -100,8 +100,8 @@ private void invalidateActiveFeedbacksAndResetStatuses(ModuleVersion mv) { } feedbackRepository.saveAll(activeFeedbacks); - proposal.setStatus(ProposalStatus.PENDING_FIRST_SUBMISSION); - mv.setStatus(ModuleVersionStatus.PENDING_FIRST_SUBMISSION); + proposal.setStatus(ProposalStatus.WAITING_FOR_COORDINATORS_SUBMISSION); + mv.setStatus(ModuleVersionStatus.WAITING_FOR_COORDINATORS_SUBMISSION); proposalRepository.save(proposal); } @@ -215,9 +215,10 @@ public void updateStatus(Long moduleVersionId) { /** * Coordinator phase only (no role-based feedbacks on this module version yet). * 1) At least one rejected → REJECTED - * 2) Else at least one pending → PENDING_COORDINATOR_FEEDBACK - * 3) Else all accepted → PENDING_FULL_SUBMISSION - * 4) Else → COORDINATOR_FEEDBACK_GIVEN (e.g. all FEEDBACK_GIVEN, no approval/rejection) + * 2) Else at least one pending → PENDING_COORDINATORS_FEEDBACK + * 3) Else all accepted → WAITING_FOR_EXAMINATION_BOARD_SUBMISSION + * 4) Else → COORDINATORS_FEEDBACK_GIVEN (e.g. all FEEDBACK_GIVEN, no + * approval/rejection) */ private void applyCoordinatorFeedbackStatus(List coordinatorFeedbacks, ModuleVersion mv, Proposal p) { boolean hasRejected = coordinatorFeedbacks.stream() @@ -230,19 +231,19 @@ private void applyCoordinatorFeedbackStatus(List coordinatorFeedbacks, boolean hasPending = coordinatorFeedbacks.stream() .anyMatch(f -> f.getStatus() == FeedbackStatus.PENDING_FEEDBACK); if (hasPending) { - mv.setStatus(ModuleVersionStatus.PENDING_COORDINATOR_FEEDBACK); - p.setStatus(ProposalStatus.PENDING_COORDINATOR_FEEDBACK); + mv.setStatus(ModuleVersionStatus.PENDING_COORDINATORS_FEEDBACK); + p.setStatus(ProposalStatus.PENDING_COORDINATORS_FEEDBACK); return; } boolean allApproved = coordinatorFeedbacks.stream() .allMatch(f -> f.getStatus() == FeedbackStatus.APPROVED); if (allApproved) { - mv.setStatus(ModuleVersionStatus.PENDING_FULL_SUBMISSION); - p.setStatus(ProposalStatus.PENDING_FULL_SUBMISSION); + mv.setStatus(ModuleVersionStatus.WAITING_FOR_EXAMINATION_BOARD_SUBMISSION); + p.setStatus(ProposalStatus.WAITING_FOR_EXAMINATION_BOARD_SUBMISSION); return; } - mv.setStatus(ModuleVersionStatus.COORDINATOR_FEEDBACK_GIVEN); - p.setStatus(ProposalStatus.COORDINATOR_FEEDBACK_GIVEN); + mv.setStatus(ModuleVersionStatus.COORDINATORS_FEEDBACK_GIVEN); + p.setStatus(ProposalStatus.COORDINATORS_FEEDBACK_GIVEN); } /** @@ -260,8 +261,8 @@ private void applyFullFeedbackRoleStatus(List roleBased, ModuleVersion boolean hasPending = roleBased.stream() .anyMatch(f -> f.getStatus() == FeedbackStatus.PENDING_FEEDBACK); if (hasPending) { - mv.setStatus(ModuleVersionStatus.PENDING_FULL_FEEDBACK); - p.setStatus(ProposalStatus.PENDING_FULL_FEEDBACK); + mv.setStatus(ModuleVersionStatus.WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION); + p.setStatus(ProposalStatus.WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION); return; } boolean allApproved = roleBased.stream() diff --git a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java index 3386b702..3e09a37f 100644 --- a/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java +++ b/Server/src/main/java/modulemanagement/ls1/services/ProposalService.java @@ -53,7 +53,7 @@ public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO r Proposal p = new Proposal(); p.setCreatedBy(user); p.setCreationDate(LocalDateTime.now()); - p.setStatus(ProposalStatus.PENDING_FIRST_SUBMISSION); + p.setStatus(ProposalStatus.WAITING_FOR_COORDINATORS_SUBMISSION); p = proposalRepository.save(p); ModuleVersion mv = new ModuleVersion(); @@ -61,7 +61,7 @@ public ProposalViewDTO createProposalFromRequest(User user, ProposalRequestDTO r mv.setModuleId(null); mv.setCreationDate(LocalDateTime.now()); mv.setProposal(p); - mv.setStatus(ModuleVersionStatus.PENDING_FIRST_SUBMISSION); + mv.setStatus(ModuleVersionStatus.WAITING_FOR_COORDINATORS_SUBMISSION); mv.setBulletPoints(request.getBulletPoints()); mv.setTitleEng(request.getTitleEng()); mv.setTitleDe(request.getTitleDe()); @@ -168,8 +168,9 @@ public ProposalViewDTO requestCoordinatorsFeedback(Long proposalId, UUID userId) ModuleVersion mv = proposal.getLatestModuleVersionWithContent(); - if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_FIRST_SUBMISSION)) { - throw new IllegalStateException("Proposal is not pending first submission. It is " + mv.getStatus() + "."); + if (!mv.getStatus().equals(ModuleVersionStatus.WAITING_FOR_COORDINATORS_SUBMISSION)) { + throw new IllegalStateException( + "Proposal is not waiting for coordinator submission. It is " + mv.getStatus() + "."); } if (!mv.isFirstStepComplete()) { @@ -190,8 +191,8 @@ public ProposalViewDTO requestCoordinatorsFeedback(Long proposalId, UUID userId) requiredFeedbacks.add(feedbackRepository.save(feedback)); } - mv.setStatus(ModuleVersionStatus.PENDING_COORDINATOR_FEEDBACK); - proposal.setStatus(ProposalStatus.PENDING_COORDINATOR_FEEDBACK); + mv.setStatus(ModuleVersionStatus.PENDING_COORDINATORS_FEEDBACK); + proposal.setStatus(ProposalStatus.PENDING_COORDINATORS_FEEDBACK); moduleVersionRepository.save(mv); // to keep the version with requested feedback immutable @@ -225,7 +226,7 @@ public ProposalViewDTO requestFullFeedback(Long proposalId, UUID userId) { } ModuleVersion mv = proposal.getLatestModuleVersionWithContent(); - if (!mv.getStatus().equals(ModuleVersionStatus.PENDING_FULL_SUBMISSION)) { + if (!mv.getStatus().equals(ModuleVersionStatus.WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION)) { throw new IllegalStateException( "Proposal must be in PENDING_FULL_SUBMISSION (coordinator feedback accepted). It is " + mv.getStatus() + "."); @@ -264,11 +265,10 @@ public ProposalViewDTO requestFullFeedback(Long proposalId, UUID userId) { newFullFeedbackRequests.add(saved); } - mv.setStatus(ModuleVersionStatus.PENDING_FULL_FEEDBACK); - proposal.setStatus(ProposalStatus.PENDING_FULL_FEEDBACK); + mv.setStatus(ModuleVersionStatus.WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION); + proposal.setStatus(ProposalStatus.WAITING_FOR_QUALITY_MANAGEMENT_SUBMISSION); moduleVersionRepository.save(mv); - // to keep the version with requested feedback immutable proposal.addNewModuleVersion(); proposalRepository.save(proposal); mailingService.sendReviewerRequestNotification( @@ -284,7 +284,7 @@ public void deleteProposalById(long proposalId, UUID userId) { if (!p.getCreatedBy().getUserId().equals(userId)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized access"); } - if (!p.getStatus().equals(ProposalStatus.PENDING_FIRST_SUBMISSION)) { + if (!p.getStatus().equals(ProposalStatus.WAITING_FOR_COORDINATORS_SUBMISSION)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "You can only delete a proposal that is not already submitted. This module proposal is " + p.getStatus() + "."); From 896cae9604a411779410360731944faf8be59c53 Mon Sep 17 00:00:00 2001 From: Mohamed Alaaser Date: Fri, 1 May 2026 19:53:23 +0200 Subject: [PATCH 42/42] examination board feedback step done --- .../create-edit-base.component.html | 62 ++++--- .../create-edit-base.component.ts | 149 +++++++++++------ .../coordinator-feedback.util.ts | 45 +++++ .../module-edit-steps.config.ts | 9 +- .../module-version-stepper-status.util.ts | 41 ++--- .../api/proposal-controller.service.ts | 12 +- .../proposal-controller.serviceInterface.ts | 2 +- .../openapi/model/feedback-compact-dto.ts | 1 + .../core/modules/openapi/model/feedback.ts | 1 + .../model/module-version-compact-dto.ts | 5 +- .../module-version-update-request-dto.ts | 5 +- .../openapi/model/module-version-view-dto.ts | 5 +- .../model/module-version-view-feedback-dto.ts | 22 +-- .../openapi/model/proposal-view-dto.ts | 5 +- .../openapi/model/proposals-compact-dto.ts | 5 +- .../module-version-edit.component.ts | 7 +- .../module-version-view.component.html | 16 +- .../module-version-view.component.ts | 55 +++--- .../proposal-create.component.ts | 8 +- .../app/pipes/feedbackAuthorDisplay.pipe.ts | 30 +++- .../src/app/pipes/moduleVersionStatus.pipe.ts | 3 +- Client/src/app/pipes/proposalStatus.pipe.ts | 6 +- .../ls1/controllers/ProposalController.java | 6 +- .../ls1/dtos/FeedbackCompactDTO.java | 9 +- .../dtos/ModuleVersionViewFeedbackDTO.java | 20 ++- .../ls1/enums/ModuleVersionStatus.java | 5 +- .../ls1/enums/ProposalStatus.java | 5 +- .../modulemanagement/ls1/models/Feedback.java | 18 +- .../ls1/models/ModuleVersion.java | 1 + .../modulemanagement/ls1/models/Proposal.java | 6 + .../ls1/repositories/FeedbackRepository.java | 8 +- .../ls1/services/FeedbackService.java | 20 +-- .../ls1/services/MailingService.java | 18 +- .../ls1/services/ModuleVersionService.java | 156 +++++++++++------- .../ls1/services/ProposalService.java | 155 +++++++++++++---- .../ModuleVersionStepsChangeDetector.java | 74 +++++++++ .../0016_feedback_assigned_reviewer.yaml | 30 ++++ .../main/resources/db/changelog/master.yaml | 3 + docker/keycloak/Dockerfile | 10 +- 39 files changed, 704 insertions(+), 334 deletions(-) create mode 100644 Client/src/app/components/module-edit-stepper/coordinator-feedback.util.ts create mode 100644 Server/src/main/resources/db/changelog/changes/0016_feedback_assigned_reviewer.yaml diff --git a/Client/src/app/components/create-edit-base/create-edit-base.component.html b/Client/src/app/components/create-edit-base/create-edit-base.component.html index 0118ba02..e63fe5a3 100644 --- a/Client/src/app/components/create-edit-base/create-edit-base.component.html +++ b/Client/src/app/components/create-edit-base/create-edit-base.component.html @@ -220,7 +220,7 @@

{{ moduleVersionDto()?.titleEng ? 'Edit Proposal for ' }

- +