diff --git a/uniplanWeb/public/i18n/bg.json b/uniplanWeb/public/i18n/bg.json index 5d3fe91..4b1cd65 100644 --- a/uniplanWeb/public/i18n/bg.json +++ b/uniplanWeb/public/i18n/bg.json @@ -44,8 +44,27 @@ "faculty-number-label": "Факултетен номер", "course-label": "Курс", "qualification-level-label": "Квалификационна степен", + "first-name-label": "Първо име", + "last-name-label": "Фамилия", + "major-label": "Специалност", + "study-mode-label": "Форма на обучение", "add": { "title": "Добави студент" + }, + "edit": { + "title": "Редактирай студент" + }, + "delete": { + "title": "Изтрий студент" + }, + "warnings": { + "first-name": "Моля въведете първо име на студента.", + "last-name": "Моля въведете фамилия на студента.", + "faculty-number": "Моля въведете факултетен номер.", + "major": "Моля изберете специалност.", + "study-mode": "Моля изберете форма на обучение.", + "qualification": "Моля изберете квалификационна степен.", + "course": "Моля изберете курс." } }, "settings": { diff --git a/uniplanWeb/public/i18n/en.json b/uniplanWeb/public/i18n/en.json index bd15631..eaff0df 100644 --- a/uniplanWeb/public/i18n/en.json +++ b/uniplanWeb/public/i18n/en.json @@ -44,8 +44,27 @@ "faculty-number-label": "Student ID", "course-label": "Course", "qualification-level-label": "Qualification level", + "first-name-label": "First name", + "last-name-label": "Last name", + "major-label": "Major", + "study-mode-label": "Study mode", "add": { "title": "Add student" + }, + "edit": { + "title": "Edit student" + }, + "delete":{ + "title": "Delete student" + }, + "warnings": { + "first-name": "Please enter the student's first name.", + "last-name": "Please enter the student's last name.", + "faculty-number": "Please enter the faculty number.", + "major": "Please select a major.", + "study-mode": "Please select a study mode.", + "qualification": "Please select a qualification level.", + "course": "Please select a course." } }, "settings": { diff --git a/uniplanWeb/src/app/config/endpoints.ts b/uniplanWeb/src/app/config/endpoints.ts index 53a9589..8896ff7 100644 --- a/uniplanWeb/src/app/config/endpoints.ts +++ b/uniplanWeb/src/app/config/endpoints.ts @@ -5,4 +5,5 @@ export const API_ENDPOINTS = { courses: `${environment.baseUrl}/courses`, faculties: `${environment.baseUrl}/faculties`, majors: `${environment.baseUrl}/majors`, + students: `${environment.baseUrl}/students`, }; diff --git a/uniplanWeb/src/app/core/interfaces/course-elm.ts b/uniplanWeb/src/app/core/interfaces/course-elm.ts new file mode 100644 index 0000000..017004f --- /dev/null +++ b/uniplanWeb/src/app/core/interfaces/course-elm.ts @@ -0,0 +1,7 @@ +export interface CourseElm { + id: string; + majorId: string; + courseYear: number; + courseType: string; + courseSubtype: string; +} \ No newline at end of file diff --git a/uniplanWeb/src/app/core/interfaces/student-elm.ts b/uniplanWeb/src/app/core/interfaces/student-elm.ts index f826a9b..63ad7ba 100644 --- a/uniplanWeb/src/app/core/interfaces/student-elm.ts +++ b/uniplanWeb/src/app/core/interfaces/student-elm.ts @@ -1,9 +1,13 @@ +import { CourseSubtype } from "../../features/course/course-subtype"; + export interface StudentElm { + id: string; position: number; name: string; facultyNumber: string; - major: string; - majorType: string; - course: string; - subtype: 'редовно' | 'задочно'; + majorId: string; + majorName: string; + courseType: string; + courseSubtype: CourseSubtype; + courseYear: number; } diff --git a/uniplanWeb/src/app/features/course/course-service.ts b/uniplanWeb/src/app/features/course/course-service.ts new file mode 100644 index 0000000..a35fb88 --- /dev/null +++ b/uniplanWeb/src/app/features/course/course-service.ts @@ -0,0 +1,18 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { CourseElm } from '../../core/interfaces/course-elm'; +import { API_ENDPOINTS } from '../../config/endpoints'; + +@Injectable({ + providedIn: 'root' +}) +export class CourseService { + private http = inject(HttpClient); + + getCoursesByMajorId(majorId: string): Observable { + return this.http.get(`${API_ENDPOINTS.courses}/major/${majorId}`); + } +} +//placeholder course service to make student form dropdowns work, +//when replacing this please provide a getCoursesByMajorId-like method \ No newline at end of file diff --git a/uniplanWeb/src/app/features/course/course-subtype.ts b/uniplanWeb/src/app/features/course/course-subtype.ts new file mode 100644 index 0000000..01e81a6 --- /dev/null +++ b/uniplanWeb/src/app/features/course/course-subtype.ts @@ -0,0 +1,6 @@ +export const CourseSubtypeLabels = { + FullTime: 'редовно', + PartTime: 'задочно', +} as const; + +export type CourseSubtype = typeof CourseSubtypeLabels[keyof typeof CourseSubtypeLabels]; \ No newline at end of file diff --git a/uniplanWeb/src/app/features/student/student-panel/student-panel.html b/uniplanWeb/src/app/features/student/student-panel/student-panel.html index 7a2811e..a6d22fe 100644 --- a/uniplanWeb/src/app/features/student/student-panel/student-panel.html +++ b/uniplanWeb/src/app/features/student/student-panel/student-panel.html @@ -18,6 +18,7 @@ [searchFacNum]="searchFacNum" [searchMajor]="searchMajor" [subtype]="selectedStudentSubtype" - [subtypes]="studentSubtypes"> + [subtypes]="studentSubtypes" + (subtypesLoaded)="studentSubtypes=$event"> diff --git a/uniplanWeb/src/app/features/student/student-panel/student-panel.ts b/uniplanWeb/src/app/features/student/student-panel/student-panel.ts index 28fd5fe..ec745ef 100644 --- a/uniplanWeb/src/app/features/student/student-panel/student-panel.ts +++ b/uniplanWeb/src/app/features/student/student-panel/student-panel.ts @@ -1,9 +1,8 @@ import { Component, OnInit } from '@angular/core'; -import { StudentElm } from '../../../core/interfaces/student-elm'; import { StudentOptions } from '../student-options/student-options'; import { StudentFilters } from '../student-filters/student-filters'; -import { StudentTable, ELEMENT_STUDENT_DATA } from '../student-table/student-table'; +import { StudentTable } from '../student-table/student-table'; @Component({ selector: 'app-student-panel', @@ -19,7 +18,6 @@ export class StudentPanel implements OnInit { studentSubtypes: string[] = []; ngOnInit(): void { - const students: StudentElm[] = ELEMENT_STUDENT_DATA; - this.studentSubtypes = StudentTable.getFilterOptions(students).subtypes; + } } diff --git a/uniplanWeb/src/app/features/student/student-service.ts b/uniplanWeb/src/app/features/student/student-service.ts new file mode 100644 index 0000000..d6a451c --- /dev/null +++ b/uniplanWeb/src/app/features/student/student-service.ts @@ -0,0 +1,61 @@ +import { inject, Injectable } from '@angular/core'; +import { map, Observable, Subject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { StudentElm } from '../../core/interfaces/student-elm'; +import { API_ENDPOINTS } from '../../config/endpoints'; + +@Injectable({ + providedIn: 'root' +}) +export class StudentService { + private apiUrl = API_ENDPOINTS.students; + + refreshNeeded = new Subject(); + + private http = inject(HttpClient); + + getStudents(): Observable { + return this.http.get(this.apiUrl).pipe( + map((students) => + students.map((student, index) => ({ + id: student.id, + position: index + 1, + name: student.name, + facultyNumber: student.facultyNumber, + majorId: student.majorId, + majorName: student.majorName, + courseType: student.courseType, + courseSubtype: student.courseSubtype, + courseYear: student.courseYear, + })) + ) + ); + } + createStudent(student: Omit): Observable { + return this.http.post(`${this.apiUrl}`, student).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } + + editStudent(id: string, + updatedStudent: Omit): Observable { + return this.http.put(`${this.apiUrl}/${id}`, updatedStudent).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } + + deleteStudent(id: string): Observable { + return this.http.delete(`${this.apiUrl}/${id}`).pipe( + map((res) => { + this.refreshNeeded.next(); + return res; + }) + ); + } +} \ No newline at end of file diff --git a/uniplanWeb/src/app/features/student/student-table/student-table.html b/uniplanWeb/src/app/features/student/student-table/student-table.html index da9786a..f6116c4 100644 --- a/uniplanWeb/src/app/features/student/student-table/student-table.html +++ b/uniplanWeb/src/app/features/student/student-table/student-table.html @@ -1,104 +1,66 @@
- +
- - + - - + - - - + + - - - + + - - - + + - - - + + - - + -
{{ 'features.number-label' | translate }} {{ 'features.name-label' | translate }} - {{ 'features.name-label' | translate }} - {{ 'student.faculty-number-label' | translate }} - {{ 'student.faculty-number-label' | translate }} - {{ 'features.major-label' | translate }} - {{ 'features.major-label' | translate }} - {{'student.qualification-level-label' | translate}} - {{ 'student.qualification-level-label' | translate }} {{ 'student.course-label' | translate }} - {{ 'features.study-mode-label' | translate }} {{ 'features.study-mode-label' | translate }} - {{ 'student.course-label' | translate }} {{ 'features.actions-label' | translate }} - {{ 'features.actions-label' | translate }}
-
- +
- + - - + - - + - - - + + - - - + + - - - + + - - - + + - -
- {{element.position}} {{element.position}} - {{element.name}} {{element.name}} - {{element.facultyNumber}} {{element.facultyNumber}} - {{element.major}} {{element.majorName}} - {{element.majorType}} {{element.courseType}} - {{element.course}} {{element.courseSubtype}} - {{element.subtype}} {{element.courseYear}} - -
-
+ \ No newline at end of file diff --git a/uniplanWeb/src/app/features/student/student-table/student-table.scss b/uniplanWeb/src/app/features/student/student-table/student-table.scss index 68efa23..06fe230 100644 --- a/uniplanWeb/src/app/features/student/student-table/student-table.scss +++ b/uniplanWeb/src/app/features/student/student-table/student-table.scss @@ -4,91 +4,74 @@ overflow: hidden; background-color: #fff; } - .table-header { width: 100%; table-layout: fixed; } - .scrollable-body { max-height: 510px; overflow-y: auto; overflow-x: hidden; } - @media (min-width: 900px) { .scrollable-body { max-height: 510px; } } - @media (min-width: 1800px) { .scrollable-body { max-height: 680px; } } - .table-body { width: 100%; table-layout: fixed; border-collapse: collapse; } - .table-body tr:hover td { background-color: #f8f8f8; cursor: pointer; } - th, td { text-align: center; padding: 8px 10px; word-wrap: break-word; } - th { background-color: #f7f7f7 !important; } - td { background-color: #fff; } - .col-position { width: 80px; border-right: 1px solid #e0e0e0; } - .col-name { width: 130px; border-right: 1px solid #e0e0e0; } - .col-fac-number { width: 120px; border-right: 1px solid #e0e0e0; } - -.col-major-type { - width: 140px; - border-right: 1px solid #e0e0e0; -} - .col-major { width: 160px; border-right: 1px solid #e0e0e0; } - -.col-course { - width: 70px; +.col-course-type { + width: 140px; border-right: 1px solid #e0e0e0; } - .col-subtype { width: 100px; border-right: 1px solid #e0e0e0; } - +.col-year { + width: 70px; + border-right: 1px solid #e0e0e0; +} .col-actions { width: 109px; -} +} \ No newline at end of file diff --git a/uniplanWeb/src/app/features/student/student-table/student-table.ts b/uniplanWeb/src/app/features/student/student-table/student-table.ts index 3bc330c..69158cd 100644 --- a/uniplanWeb/src/app/features/student/student-table/student-table.ts +++ b/uniplanWeb/src/app/features/student/student-table/student-table.ts @@ -4,7 +4,10 @@ import { SimpleChanges, OnChanges, OnInit, - ChangeDetectionStrategy + ChangeDetectionStrategy, + inject, + output, + DestroyRef } from '@angular/core'; import { StudentElm } from '../../../core/interfaces/student-elm'; import { TranslatePipe } from '@ngx-translate/core'; @@ -12,99 +15,9 @@ import { TranslatePipe } from '@ngx-translate/core'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; - -export const ELEMENT_STUDENT_DATA: StudentElm[] = [ - { - position: 1, - name: 'Иван Иванов', - facultyNumber: '123456', - majorType: 'бакалавър', - major: 'Компютърни науки', - course: '3', - subtype: 'редовно', - }, - { - position: 2, - name: 'Мария Петрова', - facultyNumber: '234567', - majorType: 'бакалавър', - major: 'История', - course: '2', - subtype: 'задочно', - }, - { - position: 3, - name: 'Георги Георгиев', - facultyNumber: '345678', - majorType: 'бакалавър', - major: 'Английска филология', - course: '1', - subtype: 'редовно', - }, - { - position: 4, - name: 'Анна Димитрова', - facultyNumber: '456789', - majorType: 'магистър', - major: 'Молекулярна биология', - course: '4', - subtype: 'редовно', - }, - { - position: 5, - name: 'Петър Петров', - facultyNumber: '567890', - majorType: 'магистър', - major: 'Органична химия', - course: '2', - subtype: 'редовно', - }, - { - position: 6, - name: 'Елена Стоянова', - facultyNumber: '678901', - majorType: 'магистър', - major: 'Астрофизика', - course: '3', - subtype: 'задочно', - }, - { - position: 7, - name: 'Димитър Иванов', - facultyNumber: '789012', - majorType: 'бакалавър', - major: 'Начална педагогика', - course: '1', - subtype: 'редовно', - }, - { - position: 8, - name: 'Виктория Николова', - facultyNumber: '890123', - majorType: 'бакалавър', - major: 'Специална педагогика', - course: '4', - subtype: 'редовно', - }, - { - position: 9, - name: 'Красимир Тодоров', - facultyNumber: '901234', - majorType: 'бакалавър', - major: 'Екология', - course: '3', - subtype: 'задочно', - }, - { - position: 10, - name: 'Милена Георгиева', - facultyNumber: '012345', - majorType: 'магистър', - major: 'Прикладна математика', - course: '2', - subtype: 'редовно', - }, -]; +import { StudentService } from '../student-service'; +import { MatDialog } from '@angular/material/dialog'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-student-table', @@ -114,6 +27,9 @@ export const ELEMENT_STUDENT_DATA: StudentElm[] = [ imports: [MatTableModule, MatIconModule, MatButtonModule, TranslatePipe], }) export class StudentTable implements OnInit, OnChanges { + private dialog = inject(MatDialog) + private studentService = inject(StudentService) + private destroyRef = inject(DestroyRef); @Input() searchText = ''; @Input() searchFacNum = ''; @Input() searchMajor = ''; @@ -121,29 +37,51 @@ export class StudentTable implements OnInit, OnChanges { @Input() subtypes: string[] = []; + subtypesLoaded = output(); + displayedColumns: string[] = [ 'position', 'name', 'facultyNumber', - 'major', - 'majorType', - 'subtype', - 'course', + 'majorName', + 'courseSubtype', + 'courseType', + 'courseYear', 'actions', - ]; +]; - originalData: StudentElm[] = ELEMENT_STUDENT_DATA; - dataSourceFilter: StudentElm[] = ELEMENT_STUDENT_DATA; + originalData: StudentElm[] = []; + dataSourceFilter: StudentElm[] = []; ngOnInit(): void { - this.subtypes = StudentTable.getFilterOptions(this.originalData).subtypes; - this.applyFilters(); + this.loadStudents(); + this.studentService.refreshNeeded + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.loadStudents(); + }); } ngOnChanges(changes: SimpleChanges): void { this.applyFilters(); } + loadStudents(): void { + this.studentService.getStudents() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (students) => { + this.originalData = students; + this.subtypes = StudentTable.getFilterOptions(students).subtypes; + this.subtypesLoaded.emit(this.subtypes); + this.applyFilters(); + }, + error: () => { + console.error('Failed to load students'); + } + }); + } + applyFilters(): void { const name = this.searchText.toLowerCase(); const major = this.searchMajor.toLowerCase(); @@ -151,9 +89,9 @@ export class StudentTable implements OnInit, OnChanges { this.dataSourceFilter = this.originalData.filter((student) => { const matchName = !name || student.name.toLowerCase().includes(name); - const matchMajor = !major || student.major.toLowerCase().includes(major); + const matchMajor = !major || student.majorName.toLowerCase().includes(major); const matchFacNum = !facNum || student.facultyNumber.includes(facNum); - const matchSubtype = !this.subtype || student.subtype === this.subtype; + const matchSubtype = !this.subtype || student.courseSubtype === this.subtype; return matchName && matchMajor && matchFacNum && matchSubtype; }); @@ -161,7 +99,7 @@ export class StudentTable implements OnInit, OnChanges { static getFilterOptions(data: StudentElm[]) { return { - subtypes: [...new Set(data.map((e) => e.subtype))], + subtypes: [...new Set(data.map((e) => e.courseSubtype))], }; }