소스 검색

Subject details list

Marcin Jaborski 3 년 전
부모
커밋
570c48d38a
33개의 변경된 파일709개의 추가작업 그리고 130개의 파일을 삭제
  1. 63 3
      project-back/src/main/java/com/example/projectback/controllers/StudentController.java
  2. 48 2
      project-back/src/main/java/com/example/projectback/controllers/SubjectController.java
  3. 56 0
      project-back/src/main/java/com/example/projectback/model/Grade.java
  4. 18 10
      project-back/src/main/java/com/example/projectback/model/Student.java
  5. 0 4
      project-back/src/main/java/com/example/projectback/model/Subject.java
  6. 4 0
      project-back/src/main/java/com/example/projectback/model/SubjectStudent.java
  7. 8 7
      project-back/src/main/java/com/example/projectback/model/SubjectStudentId.java
  8. 7 0
      project-back/src/main/java/com/example/projectback/repository/GradeRepository.java
  9. 10 0
      project-back/src/main/java/com/example/projectback/repository/SubjectStudentRepository.java
  10. 10 5
      project-front/src/app/app.module.ts
  11. 10 0
      project-front/src/app/material/material.module.ts
  12. 23 0
      project-front/src/app/students/add-student-dialog.component.html
  13. 14 0
      project-front/src/app/students/grade.model.ts
  14. 5 1
      project-front/src/app/students/student.model.ts
  15. 16 5
      project-front/src/app/students/student.service.ts
  16. 11 0
      project-front/src/app/students/students.component.css
  17. 60 33
      project-front/src/app/students/students.component.html
  18. 87 30
      project-front/src/app/students/students.component.ts
  19. 10 0
      project-front/src/app/subject/studentAssoc.ts
  20. 12 0
      project-front/src/app/subject/subject-details/add-student-to-subject-dialog.component.html
  21. 11 0
      project-front/src/app/subject/subject-details/subject-details.component.css
  22. 39 0
      project-front/src/app/subject/subject-details/subject-details.component.html
  23. 25 0
      project-front/src/app/subject/subject-details/subject-details.component.spec.ts
  24. 96 0
      project-front/src/app/subject/subject-details/subject-details.component.ts
  25. 0 0
      project-front/src/app/subject/subject-list/add-subject-dialog.component.html
  26. 15 0
      project-front/src/app/subject/subject-list/subject-list.component.css
  27. 9 3
      project-front/src/app/subject/subject-list/subject-list.component.html
  28. 5 5
      project-front/src/app/subject/subject-list/subject-list.component.spec.ts
  29. 16 8
      project-front/src/app/subject/subject-list/subject-list.component.ts
  30. 2 2
      project-front/src/app/subject/subject.model.ts
  31. 3 3
      project-front/src/app/subject/subject.service.spec.ts
  32. 16 2
      project-front/src/app/subject/subject.service.ts
  33. 0 7
      project-front/src/app/subjects/subjects.component.css

+ 63 - 3
project-back/src/main/java/com/example/projectback/controllers/StudentController.java

@@ -1,12 +1,16 @@
 package com.example.projectback.controllers;
 
-import com.example.projectback.model.Student;
+import com.example.projectback.model.*;
+import com.example.projectback.repository.GradeRepository;
 import com.example.projectback.repository.StudentRepository;
+import com.example.projectback.repository.SubjectRepository;
+import com.example.projectback.repository.SubjectStudentRepository;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.LinkedHashMap;
 import java.util.List;
 
 @RestController
@@ -14,16 +18,72 @@ import java.util.List;
 @RequestMapping("/students")
 public class StudentController {
     private final StudentRepository studentRepository;
+    private final SubjectRepository subjectRepository;
+    private final GradeRepository gradeRepository;
+    private final SubjectStudentRepository subjectStudentRepository;
 
     @Autowired
-    public StudentController(StudentRepository studentRepository) {this.studentRepository = studentRepository;}
+    public StudentController(StudentRepository studentRepository, SubjectRepository subjectRepository, GradeRepository gradeRepository, SubjectStudentRepository subjectStudentRepository) {
+        this.studentRepository = studentRepository;
+        this.subjectRepository = subjectRepository;
+        this.gradeRepository = gradeRepository;
+        this.subjectStudentRepository = subjectStudentRepository;
+    }
 
     @GetMapping
-    public List<Student> getAllStudents() {return studentRepository.findAll();}
+    public List<Student> getAllStudents() {
+        return studentRepository.findAll();
+    }
 
     @PostMapping
     public ResponseEntity<Student> addStudent(@RequestBody Student student) {
         studentRepository.save(student);
         return new ResponseEntity<>(student, HttpStatus.CREATED);
     }
+
+    @PostMapping("/delete")
+    public ResponseEntity<Changeset> deleteStudents(@RequestBody Changeset changeset) {
+        for (Long recId : changeset.getRecords()) {
+            studentRepository.deleteById(recId);
+        }
+        return new ResponseEntity<>(changeset, HttpStatus.OK);
+    }
+
+    @PostMapping("/{id}/grades")
+    public ResponseEntity<Grade> addGrade(@PathVariable("id") Long id, @RequestBody LinkedHashMap<String, Object> grade) {
+        Student student = this.studentRepository.getById(id);
+        Grade newGrade = new Grade();
+        Integer markRequest = (Integer) grade.get("mark");
+        newGrade.setMark(markRequest.longValue());
+        newGrade.setDescription((String) grade.get("description"));
+        LinkedHashMap<String, Object> subjectRequest = (LinkedHashMap<String, Object>) grade.get("subject");
+        Integer subjectIDRequest = (Integer) subjectRequest.get("id");
+        Subject subject = this.subjectRepository.getById(Long.valueOf(subjectIDRequest));
+        newGrade.setSubject(subject);
+
+        student.getGrades().add(newGrade);
+        this.gradeRepository.save(newGrade);
+        this.studentRepository.save(student);
+        return new ResponseEntity<>(newGrade, HttpStatus.OK);
+    }
+
+    @DeleteMapping("/{studentID}/grades/{gradeID}")
+    public ResponseEntity<Student> deleteGrade(@PathVariable("studentID") Long studentID, @PathVariable("gradeID") Long gradeID) {
+        Student student = this.studentRepository.getById(studentID);
+        Grade grade = this.gradeRepository.getById(gradeID);
+        student.getGrades().remove(grade);
+        this.studentRepository.save(student);
+        this.gradeRepository.delete(grade);
+        return new ResponseEntity<>(student, HttpStatus.OK);
+    }
+
+    @PatchMapping("/{studentID}/payment/{subjectID}")
+    public ResponseEntity<Student> confirmPayment(@PathVariable("studentID") Long studentID, @PathVariable("subjectID") Long subjectID, @RequestBody SubjectStudent subjectStudentReq) {
+        Student student = this.studentRepository.getById(studentID);
+        Subject subject = this.subjectRepository.getById(subjectID);
+        SubjectStudent subjectStudent = this.subjectStudentRepository.getByStudentAndSubject(student, subject);
+        subjectStudent.setPaymentConfirmed(subjectStudentReq.isPaymentConfirmed());
+        this.subjectStudentRepository.save(subjectStudent);
+        return new ResponseEntity<>(student, HttpStatus.OK);
+    }
 }

+ 48 - 2
project-back/src/main/java/com/example/projectback/controllers/SubjectController.java

@@ -1,30 +1,49 @@
 package com.example.projectback.controllers;
 
 import com.example.projectback.model.Changeset;
+import com.example.projectback.model.Student;
 import com.example.projectback.model.Subject;
+import com.example.projectback.model.SubjectStudent;
+import com.example.projectback.repository.StudentRepository;
 import com.example.projectback.repository.SubjectRepository;
+import com.example.projectback.repository.SubjectStudentRepository;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
-import javax.swing.event.ChangeEvent;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
 @RestController
 @CrossOrigin(origins = "http://localhost:4200")
 @RequestMapping("/subjects")
 public class SubjectController {
     private final SubjectRepository subjectRepository;
+    private final SubjectStudentRepository subjectStudentRepository;
+    private final StudentRepository studentRepository;
 
     @Autowired
-    public SubjectController(SubjectRepository subjectRepository) {
+    public SubjectController(SubjectRepository subjectRepository, SubjectStudentRepository subjectStudentRepository, StudentRepository studentRepository) {
         this.subjectRepository = subjectRepository;
+        this.subjectStudentRepository = subjectStudentRepository;
+        this.studentRepository = studentRepository;
     }
 
     @GetMapping
     public List<Subject> getAllSubjects() {return subjectRepository.findAll();}
 
+    @GetMapping("/{id}")
+    public ResponseEntity<Subject> getSubject(@PathVariable("id") Long id) {
+        Optional<Subject> subject = subjectRepository.findById(id);
+        if (subject.isEmpty()) {
+            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+        }
+        return new ResponseEntity<>(subject.get(), HttpStatus.OK);
+    }
+
     @PostMapping
     public ResponseEntity<Subject> addSubject(@RequestBody Subject subject) {
         subjectRepository.save(subject);
@@ -38,4 +57,31 @@ public class SubjectController {
         }
         return new ResponseEntity<>(changeset, HttpStatus.OK);
     }
+
+    @PatchMapping("/{id}")
+    public ResponseEntity<Subject> updatePartOfSubject(@RequestBody Map<String, Object> updates, @PathVariable("id") Long id) {
+        Subject subject = subjectRepository.getById(id);
+        if (subject == null) {
+            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+        }
+        partialUpdates(subject, updates);
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+
+    private void partialUpdates(Subject subject, Map<String, Object> updates) {
+        if (updates.containsKey("name")) {
+            subject.setName((String) updates.get("nane"));
+        }
+        if (updates.containsKey("studentAssoc")) {
+            ArrayList<SubjectStudent> newStudents = new ArrayList<>();
+            for (Integer studentId : (ArrayList<Integer>)updates.get("studentAssoc")) {
+                Student student = this.studentRepository.getById(studentId.longValue());
+                SubjectStudent newAssoc = new SubjectStudent(subject, student);
+                this.subjectStudentRepository.save(newAssoc);
+                newStudents.add(newAssoc);
+            }
+            subject.setStudentsAssoc(newStudents);
+        }
+        subjectRepository.save(subject);
+    }
 }

+ 56 - 0
project-back/src/main/java/com/example/projectback/model/Grade.java

@@ -0,0 +1,56 @@
+package com.example.projectback.model;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import javax.persistence.*;
+
+@Entity
+@JsonIgnoreProperties({"hibernateLazyInitializer"})
+public class Grade {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private Long mark;
+    private String description;
+
+    @OneToOne
+    @JsonBackReference
+    private Subject subject;
+
+    public Grade() {
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getMark() {
+        return mark;
+    }
+
+    public void setMark(Long mark) {
+        this.mark = mark;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Subject getSubject() {
+        return subject;
+    }
+
+    public void setSubject(Subject subject) {
+        this.subject = subject;
+    }
+}

+ 18 - 10
project-back/src/main/java/com/example/projectback/model/Student.java

@@ -1,30 +1,30 @@
 package com.example.projectback.model;
 
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
 import javax.persistence.*;
 import java.util.ArrayList;
 import java.util.List;
 
 @Entity
+@JsonIgnoreProperties({"hibernateLazyInitializer"})
 public class Student {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @GeneratedValue(strategy = GenerationType.SEQUENCE)
     private Long id;
 
-    @OneToMany(mappedBy = "student")
-    private List<SubjectStudent> subjectAssoc = new ArrayList<>();
+    @OneToMany
+    private List<Grade> grades = new ArrayList<>();
 
     private String firstname;
     private String lastname;
     private String email;
     private String telephone;
 
-    public List<SubjectStudent> getSubjectAssoc() {
-        return subjectAssoc;
+    public Student() {
     }
 
-    public void setSubjectAssoc(List<SubjectStudent> subjectAssoc) {
-        this.subjectAssoc = subjectAssoc;
-    }
 
     public String getFirstname() {
         return firstname;
@@ -58,11 +58,19 @@ public class Student {
         this.telephone = telephone;
     }
 
-    public long getId() {
+    public Long getId() {
         return id;
     }
 
-    public void setId(long id) {
+    public void setId(Long id) {
         this.id = id;
     }
+
+    public List<Grade> getGrades() {
+        return grades;
+    }
+
+    public void setGrades(List<Grade> grades) {
+        this.grades = grades;
+    }
 }

+ 0 - 4
project-back/src/main/java/com/example/projectback/model/Subject.java

@@ -18,10 +18,6 @@ public class Subject {
     public Subject() {
     }
 
-    public Subject(String name) {
-        this.name = name;
-    }
-
     public Long getId() {
         return id;
     }

+ 4 - 0
project-back/src/main/java/com/example/projectback/model/SubjectStudent.java

@@ -1,5 +1,8 @@
 package com.example.projectback.model;
 
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 import javax.persistence.*;
 
 @Entity
@@ -8,6 +11,7 @@ import javax.persistence.*;
 public class SubjectStudent {
     @Id
     @ManyToOne
+    @JsonIgnore
     @JoinColumn(name = "subject_id", referencedColumnName = "id")
     private Subject subject;
 

+ 8 - 7
project-back/src/main/java/com/example/projectback/model/SubjectStudentId.java

@@ -1,25 +1,26 @@
 package com.example.projectback.model;
 
+import javax.persistence.Embeddable;
 import java.io.Serializable;
 import java.util.Objects;
 
 public class SubjectStudentId implements Serializable {
-    private int subject;
-    private int student;
+    private Long subject;
+    private Long student;
 
-    public int getSubject() {
+    public Long getSubject() {
         return subject;
     }
 
-    public void setSubject(int subject) {
+    public void setSubject(Long subject) {
         this.subject = subject;
     }
 
-    public int getStudent() {
+    public Long getStudent() {
         return student;
     }
 
-    public void setStudent(int student) {
+    public void setStudent(Long student) {
         this.student = student;
     }
 
@@ -28,7 +29,7 @@ public class SubjectStudentId implements Serializable {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         SubjectStudentId that = (SubjectStudentId) o;
-        return subject == that.subject && student == that.student;
+        return Objects.equals(subject, that.subject) && Objects.equals(student, that.student);
     }
 
     @Override

+ 7 - 0
project-back/src/main/java/com/example/projectback/repository/GradeRepository.java

@@ -0,0 +1,7 @@
+package com.example.projectback.repository;
+
+import com.example.projectback.model.Grade;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface GradeRepository extends JpaRepository<Grade, Long> {
+}

+ 10 - 0
project-back/src/main/java/com/example/projectback/repository/SubjectStudentRepository.java

@@ -0,0 +1,10 @@
+package com.example.projectback.repository;
+
+import com.example.projectback.model.Student;
+import com.example.projectback.model.Subject;
+import com.example.projectback.model.SubjectStudent;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface SubjectStudentRepository extends JpaRepository<SubjectStudent, Long> {
+    SubjectStudent getByStudentAndSubject(Student student, Subject subject);
+}

+ 10 - 5
project-front/src/app/app.module.ts

@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { AppComponent } from './app.component';
 import { HomeComponent } from './home/home.component';
-import { StudentsComponent } from './students/students.component';
+import { AddStudentDialog, StudentsComponent } from './students/students.component';
 import { RouterModule, Routes } from "@angular/router";
 import { HttpClientModule } from "@angular/common/http";
 import { FormsModule, ReactiveFormsModule } from "@angular/forms";
@@ -14,12 +14,14 @@ import { httpInterceptorProviders } from './auth/auth-interceptor';
 import { RoleGuard } from "./guards/role.guard";
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { MaterialModule } from "./material/material.module";
-import { AddSubjectDialog, SubjectsComponent } from './subjects/subjects.component';
+import { AddSubjectDialog, SubjectListComponent } from './subject/subject-list/subject-list.component';
+import { StudentSelectionDialog, SubjectDetailsComponent } from './subject/subject-details/subject-details.component';
 
 const routes: Routes = [
   { path: 'home', component: HomeComponent },
   { path: 'students', component: StudentsComponent },
-  { path: 'subjects', component: SubjectsComponent },
+  { path: 'subjects', component: SubjectListComponent },
+  { path: 'subjects/:id', component: SubjectDetailsComponent },
   { path: 'user', component: UserComponent, canActivate: [RoleGuard], data: { roles: ['ROLE_USER', 'ROLE_ADMIN'] }, },
   { path: 'admin', component: AdminComponent, canActivate: [RoleGuard], data: { roles: ['ROLE_ADMIN'] }, },
   { path: 'auth/login', component: LoginComponent },
@@ -36,8 +38,11 @@ const routes: Routes = [
     RegisterComponent,
     UserComponent,
     AdminComponent,
-    SubjectsComponent,
-    AddSubjectDialog
+    SubjectListComponent,
+    AddSubjectDialog,
+    SubjectDetailsComponent,
+    AddStudentDialog,
+    StudentSelectionDialog,
   ],
   imports: [
     BrowserModule,

+ 10 - 0
project-front/src/app/material/material.module.ts

@@ -9,6 +9,11 @@ import { MatTableModule } from "@angular/material/table";
 import { MatDialogModule } from "@angular/material/dialog";
 import { MatPaginatorModule } from "@angular/material/paginator";
 import { MatCheckboxModule } from "@angular/material/checkbox";
+import { MatExpansionModule } from "@angular/material/expansion";
+import { MatListModule } from "@angular/material/list";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { MatMenuModule } from "@angular/material/menu";
+import { MatSortModule } from "@angular/material/sort";
 
 const MaterialComponents = [
   MatButtonModule,
@@ -21,6 +26,11 @@ const MaterialComponents = [
   MatDialogModule,
   MatPaginatorModule,
   MatCheckboxModule,
+  MatExpansionModule,
+  MatListModule,
+  MatTooltipModule,
+  MatMenuModule,
+  MatSortModule,
 ];
 
 @NgModule({

+ 23 - 0
project-front/src/app/students/add-student-dialog.component.html

@@ -0,0 +1,23 @@
+<h1 mat-dialog-title>Add new subject</h1>
+<div mat-dialog-content>
+  <mat-form-field appearance="fill">
+    <mat-label>Firstname</mat-label>
+    <input matInput [(ngModel)]="student.firstname">
+  </mat-form-field>
+  <mat-form-field appearance="fill">
+    <mat-label>Lastname</mat-label>
+    <input matInput [(ngModel)]="student.lastname">
+  </mat-form-field>
+  <mat-form-field appearance="fill">
+    <mat-label>Email</mat-label>
+    <input matInput [(ngModel)]="student.email">
+  </mat-form-field>
+  <mat-form-field appearance="fill">
+    <mat-label>Telephone</mat-label>
+    <input matInput [(ngModel)]="student.telephone">
+  </mat-form-field>
+</div>
+<div mat-dialog-actions>
+  <button mat-button (click)="onNoClick()">Cancel</button>
+  <button mat-button [mat-dialog-close]="student">Add</button>
+</div>

+ 14 - 0
project-front/src/app/students/grade.model.ts

@@ -0,0 +1,14 @@
+import { Subject } from "../subject/subject.model";
+
+export class Grade {
+  id?: number;
+  mark: number;
+  description: string;
+  subject: Subject;
+
+  constructor(mark: number, description: string, subject: Subject) {
+    this.mark = mark;
+    this.description = description;
+    this.subject = subject;
+  }
+}

+ 5 - 1
project-front/src/app/students/student.model.ts

@@ -1,15 +1,19 @@
+import { Grade } from "./grade.model";
+
 export class Student {
   id?: number;
   firstname: string;
   lastname: string;
   email: string;
   telephone: string;
+  grades: Grade[];
 
-  constructor(firstname: string, lastname: string, email: string, telephone: string) {
+  constructor(firstname: string, lastname: string, email: string, telephone: string, grades: Grade[]) {
     this.firstname = firstname;
     this.lastname = lastname;
     this.email = email;
     this.telephone = telephone;
+    this.grades = grades;
   }
 
 }

+ 16 - 5
project-front/src/app/students/student.service.ts

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
 import {HttpClient, HttpHeaders} from "@angular/common/http";
 import {BehaviorSubject, catchError, Observable, of, tap} from "rxjs";
 import {Student} from "./student.model";
+import { Grade } from "./grade.model";
 
 const httpOptions = {
   headers: new HttpHeaders({ 'Content-Type': 'application/json' })
@@ -55,11 +56,9 @@ export class StudentService {
   }
 
   /** DELETE: delete the student from the server */
-  deleteStudents(): Observable<Student> {
-    return this.http.delete<Student>(this.studentsUrl, httpOptions).pipe(
-      tap(_ => this.log(`deleted students`)),
-      catchError(this.handleError<Student>('deleteStudents'))
-    );
+  deleteStudents(students: Student[]): Observable<any> {
+    const ids = students.map(student => student.id);
+    return this.http.post(`${this.studentsUrl}/delete`, {records: ids}, httpOptions);
   }
 
   /** PUT: update the student on the server */
@@ -70,6 +69,18 @@ export class StudentService {
     );
   }
 
+  addGrade(studentID: number, grade: Grade): Observable<any> {
+    return this.http.post(`${this.studentsUrl}/${studentID}/grades`, grade);
+  }
+
+  deleteGrade(studentID: number, gradeID: number): Observable<any> {
+    return this.http.delete(`${this.studentsUrl}/${studentID}/grades/${gradeID}`);
+  }
+
+  confirmPayment(studentID: number, subjectID: number, confirmed: boolean): Observable<any> {
+    return this.http.patch(`${this.studentsUrl}/${studentID}/payment/${subjectID}`, {paymentConfirmed: confirmed}, httpOptions);
+  }
+
   /**
    * Handle Http operation that failed.
    * Let the app continue.

+ 11 - 0
project-front/src/app/students/students.component.css

@@ -0,0 +1,11 @@
+table {
+    width: 100%;
+}
+
+button, h1 {
+    margin: 20px !important;
+}
+
+mat-form-field {
+    margin-left: 20px;
+}

+ 60 - 33
project-front/src/app/students/students.component.html

@@ -1,34 +1,61 @@
-<h2>Students</h2>
-
-<div>
-  <label>Student firstname:
-    <input #studentFirstName />
-  </label>
-  <br>
-  <label>Student lastname:
-    <input #studentLastName />
-  </label>
-  <br>
-  <label>Student email:
-    <input #studentEmail />
-  </label>
-  <br>
-  <label>Student telephone:
-    <input #studentTelephone />
-  </label>
-  <br>
-  <!-- (click) passes input value to add() and then clears the input -->
-  <button (click)="add(studentFirstName.value, studentLastName.value, studentEmail.value, studentTelephone.value);
-                  studentFirstName.value=''; studentLastName.value=''; studentEmail.value=''; studentTelephone.value=''">
-    add
-  </button>
-</div>
-
-<ul>
-  <li *ngFor="let student of studentList">
-    <span class="badge">{{student.id}}</span> {{ student.firstname }} {{ student.lastname }} {{ student.email }} {{ student.telephone }}
-    <button title="delete" (click)="delete(student)">delete</button>
-  </li>
-</ul>
-<button title="delete all" (click)="deleteAll()">delete all</button>
+<h1 class="mat-display-1">Students</h1>
+<button mat-raised-button color="primary" (click)="openDialog()">Add</button>
+<button mat-raised-button color="primary" (click)="delete()">Remove</button>
+<br>
+<mat-form-field appearance="fill">
+  <mat-label>Filter</mat-label>
+  <input matInput (keyup)="applyFilter($event)" #filter>
+</mat-form-field>
+
+<table *ngIf="studentList" mat-table [dataSource]="studentList" matSort>
+  <ng-container matColumnDef="select">
+    <th mat-header-cell *matHeaderCellDef>
+      <mat-checkbox (change)="$event ? masterToggle() : null"
+                    [checked]="selection.hasValue() && isAllSelected()"
+                    [indeterminate]="selection.hasValue() && !isAllSelected()"
+                    [aria-label]="checkboxLabel()">
+      </mat-checkbox>
+    </th>
+    <td mat-cell *matCellDef="let row">
+      <mat-checkbox (click)="$event.stopPropagation()"
+                    (change)="$event ? selection.toggle(row) : null"
+                    [checked]="selection.isSelected(row)"
+                    [aria-label]="checkboxLabel(row)">
+      </mat-checkbox>
+    </td>
+  </ng-container>
+
+  <ng-container matColumnDef="firstname">
+    <th mat-header-cell *matHeaderCellDef mat-sort-header="firstname">Firstname</th>
+    <td mat-cell *matCellDef="let student"> {{student.firstname}} </td>
+  </ng-container>
+
+  <ng-container matColumnDef="lastname">
+    <th mat-header-cell *matHeaderCellDef mat-sort-header="lastname">Lastname</th>
+    <td mat-cell *matCellDef="let student"> {{student.lastname}} </td>
+  </ng-container>
+
+  <ng-container matColumnDef="email">
+    <th mat-header-cell *matHeaderCellDef mat-sort-header="email">Email</th>
+    <td mat-cell *matCellDef="let student"> {{student.email}} </td>
+  </ng-container>
+
+  <ng-container matColumnDef="telephone">
+    <th mat-header-cell *matHeaderCellDef mat-sort-header="telephone">Telephone</th>
+    <td mat-cell *matCellDef="let student"> {{student.telephone}} </td>
+  </ng-container>
+
+  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+
+  <tr class="mat-row" *matNoDataRow>
+    <td class="mat-cell" colspan="5">No data matching the filter "{{filter.value}}"</td>
+  </tr>
+</table>
+
+<mat-paginator [pageSizeOptions]="[5, 10, 20]"
+               showFirstLastButtons
+               aria-label="Select page">
+</mat-paginator>
+
 

+ 87 - 30
project-front/src/app/students/students.component.ts

@@ -1,58 +1,115 @@
-import { Component, OnInit } from '@angular/core';
-import {Student} from "./student.model";
-import {StudentService} from "./student.service";
+import { AfterViewInit, Component, Inject, OnInit, ViewChild } from '@angular/core';
+import { Student } from "./student.model";
+import { StudentService } from "./student.service";
+import { MatTable, MatTableDataSource } from "@angular/material/table";
+import { MatPaginator } from "@angular/material/paginator";
+import { SelectionModel } from "@angular/cdk/collections";
+import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
+import { MatSort } from "@angular/material/sort";
 
 @Component({
   selector: 'app-students',
   templateUrl: './students.component.html',
   styleUrls: ['./students.component.css']
 })
-export class StudentsComponent implements OnInit {
-  studentList?: Student[];
+export class StudentsComponent implements OnInit, AfterViewInit {
+  displayedColumns: string[] = ['select', 'firstname', 'lastname', 'email', 'lastname'];
+  studentList = new MatTableDataSource<Student>([]);
   student?: Student;
+  selection = new SelectionModel<Student>(true, []);
+  @ViewChild(MatTable) table!: MatTable<Student>;
+  @ViewChild(MatPaginator) paginator!: MatPaginator;
+  @ViewChild(MatSort) sort!: MatSort;
 
-  constructor(private studentService: StudentService) { }
+  constructor(private studentService: StudentService, public dialog: MatDialog) {
+  }
+
+  ngOnInit() {
+    this.getStudents();
+  }
 
-  ngOnInit() { this.getStudents();   }
+  ngAfterViewInit() {
+    this.studentList.sort = this.sort;
+  }
+
+  isAllSelected() {
+    const numSelected = this.selection.selected.length;
+    const numRows = this.studentList.data.length;
+    return numSelected === numRows;
+  }
+
+  masterToggle() {
+    if (this.isAllSelected()) {
+      this.selection.clear();
+      return;
+    }
+
+    this.selection.select(...this.studentList.data);
+  }
+
+  checkboxLabel(row?: Student): string {
+    if (!row) {
+      return `${this.isAllSelected() ? 'deselect' : 'select'} all`;
+    }
+    return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${(row.id || 0) + 1}`;
+  }
 
   getStudents(): void {
     this.studentService.getStudents()
-      .subscribe(studentList => this.studentList = studentList);
+      .subscribe(studentList => this.studentList.data = studentList);
   }
 
-  add(firstname: string, lastname: string, email: string, telephone: string): void {
+  add({firstname, lastname, email, telephone}: Student): void {
     firstname = firstname.trim();
     lastname = lastname.trim();
     email = email.trim();
     telephone = telephone.trim();
-    this.studentService.addStudent({ firstname, lastname, email, telephone } as Student)
-      .subscribe({
-        next: (student: Student) => { this.studentList?.push(student); },
-        error: () => {},
-        complete: () => {
-          if (this.studentList != undefined) {
-            this.studentService.totalItems.next(this.studentList.length);
-          }
+    this.studentService.addStudent({firstname, lastname, email, telephone} as Student).subscribe({
+      next: (student: Student) => {
+        this.studentList.data = [...this.studentList.data, student];
+      }
+    });
+  }
+
+  delete(): void {
+    const selectedStudents: Student[] = this.selection.selected;
+    this.studentService.deleteStudents(selectedStudents).subscribe({
+      next: ({ records }) => {
+        for (const rec of records) {
+          this.studentList.data = this.studentList.data.filter(student => student.id !== rec);
         }
-  });
+      },
+    });
   }
 
-  delete(student: Student): void {
-    this.studentList = this.studentList?.filter(c => c !== student);
-    this.studentService.deleteStudent(student).subscribe(() => {
-        // for automatic update of number of students in parent component
-      if(this.studentList != undefined) {
-        this.studentService.totalItems.next(this.studentList.length);
+  openDialog(): void {
+    const dialogRef = this.dialog.open(AddStudentDialog, {
+      width: '250px',
+      data: {},
+    });
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) {
+        this.add(result);
       }
-      }
-    );
+    })
   }
 
-  deleteAll(): void {
-    this.studentService.deleteStudents().subscribe(() => {
-      }
-    );
+  applyFilter(event: Event) {
+    const filterValue = (event.target as HTMLInputElement).value;
+    this.studentList.filter = filterValue.trim().toLowerCase();
   }
 
 }
 
+@Component({
+  selector: 'add-student-dialog',
+  templateUrl: 'add-student-dialog.component.html',
+})
+export class AddStudentDialog {
+  constructor(public dialogRef: MatDialogRef<AddStudentDialog>, @Inject(MAT_DIALOG_DATA) public student: Student) {
+  }
+
+  onNoClick(): void {
+    this.dialogRef.close();
+  }
+}

+ 10 - 0
project-front/src/app/subject/studentAssoc.ts

@@ -0,0 +1,10 @@
+import { Student } from "../students/student.model";
+
+export class StudentAssoc {
+  paymentConfirmed: boolean;
+  student: Student;
+  constructor(paymentConfirmed: boolean, student: Student) {
+    this.paymentConfirmed = paymentConfirmed;
+    this.student = student;
+  }
+}

+ 12 - 0
project-front/src/app/subject/subject-details/add-student-to-subject-dialog.component.html

@@ -0,0 +1,12 @@
+<h1 mat-dialog-title>Select students</h1>
+<div mat-dialog-content>
+  <mat-selection-list #selection>
+    <mat-list-option *ngFor="let student of allStudents" [value]="student.id">
+      {{student.firstname}} {{student.lastname}}
+    </mat-list-option>
+  </mat-selection-list>
+</div>
+<div mat-dialog-actions>
+  <button mat-button (click)="onNoClick()">Cancel</button>
+  <button mat-button [mat-dialog-close]="selection.selectedOptions.selected">Add</button>
+</div>

+ 11 - 0
project-front/src/app/subject/subject-details/subject-details.component.css

@@ -0,0 +1,11 @@
+mat-form-field {
+    margin-right: 10px;
+}
+
+.mat-expansion-panel-header-description {
+    justify-content: flex-end;
+}
+
+button, h1 {
+    margin: 20px !important;
+}

+ 39 - 0
project-front/src/app/subject/subject-details/subject-details.component.html

@@ -0,0 +1,39 @@
+<h1 *ngIf="subject" class="mat-display-1">{{subject.name}}</h1>
+<button mat-raised-button color="primary" (click)="addStudent()">Add student</button>
+
+<mat-accordion *ngIf="subject">
+  <mat-expansion-panel *ngFor="let studentAssoc of subject.studentsAssoc">
+    <mat-expansion-panel-header>
+      <mat-panel-title>
+        {{studentAssoc.student.firstname}} {{studentAssoc.student.lastname}}
+      </mat-panel-title>
+      <mat-panel-description>
+        <mat-checkbox #payment [checked]="studentAssoc.paymentConfirmed" (change)="confirmPayment(studentAssoc.student.id!, payment.checked)">Payment confirmed</mat-checkbox>
+      </mat-panel-description>
+    </mat-expansion-panel-header>
+    <mat-form-field appearance="fill">
+      <mat-label>Grade</mat-label>
+      <input matInput type="number" placeholder="Range 2-5" #grade>
+    </mat-form-field>
+    <mat-form-field appearance="fill">
+      <mat-label>Description</mat-label>
+      <input matInput placeholder="Description" #description>
+    </mat-form-field>
+    <button mat-raised-button color="primary" (click)="addGrade(studentAssoc.student.id!, grade, description)">
+      Add Grade
+    </button>
+    <br>
+    Email: {{studentAssoc.student.email}} <br>
+    Telephone: {{studentAssoc.student.telephone}} <br>
+    Grades:
+    <span *ngFor=" let grade of studentAssoc.student.grades" [matTooltip]="grade.description"
+          [matMenuTriggerFor]="menu">
+      {{grade.mark}},
+      <mat-menu #menu="matMenu">
+        <button (click)="deleteGrade(studentAssoc.student.id!, grade.id!)" mat-menu-item>Delete</button>
+      </mat-menu>
+    </span>
+    <br>
+    <button mat-raised-button color="primary" (click)="deleteStudent(studentAssoc.student.id!)">Delete student</button>
+  </mat-expansion-panel>
+</mat-accordion>

+ 25 - 0
project-front/src/app/subject/subject-details/subject-details.component.spec.ts

@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SubjectDetailsComponent } from './subject-details.component';
+
+describe('SubjectComponent', () => {
+  let component: SubjectDetailsComponent;
+  let fixture: ComponentFixture<SubjectDetailsComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ SubjectDetailsComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(SubjectDetailsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 96 - 0
project-front/src/app/subject/subject-details/subject-details.component.ts

@@ -0,0 +1,96 @@
+import { Component, Inject, OnInit } from '@angular/core';
+import { SubjectService } from "../subject.service";
+import { Subject } from "../subject.model";
+import { ActivatedRoute } from "@angular/router";
+import { Student } from "../../students/student.model";
+import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
+import { StudentService } from "../../students/student.service";
+import { MatListOption } from "@angular/material/list";
+import { MatSnackBar } from "@angular/material/snack-bar";
+import { StudentAssoc } from "../studentAssoc";
+
+@Component({
+  selector: 'app-subject',
+  templateUrl: './subject-details.component.html',
+  styleUrls: ['./subject-details.component.css']
+})
+export class SubjectDetailsComponent implements OnInit {
+  subject?: Subject;
+
+  constructor(private subjectService: SubjectService,
+              private studentService: StudentService,
+              private route: ActivatedRoute,
+              public dialog: MatDialog,
+              private _snackBar: MatSnackBar) {
+  }
+
+  ngOnInit(): void {
+    this.getSubject();
+  }
+
+  getSubject(): void {
+    const id = Number(this.route.snapshot.paramMap.get('id'));
+    this.subjectService.getSubject(id).subscribe(subject => this.subject = subject);
+  }
+
+  addStudent(): void {
+    const dialogRef = this.dialog.open(StudentSelectionDialog, {
+      width: '250px',
+      data: {},
+    });
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) {
+        const ids = result.map((student: MatListOption) => student.value);
+        this.subjectService.addStudentToSubject(this.subject!, ids).subscribe();
+      }
+    });
+  }
+
+  deleteStudent(studentIdToDelete: number): void {
+    let ids = this.subject!.studentsAssoc!.map((studentAssoc: StudentAssoc) => studentAssoc.student.id!);
+    ids = ids.filter((id: number) => id !== studentIdToDelete);
+    this.subjectService.addStudentToSubject(this.subject!, ids).subscribe();
+  }
+
+  addGrade(studentId: number, gradeEl: HTMLInputElement, descriptionEl: HTMLInputElement): void {
+    const grade = Number(gradeEl.value);
+    if (!Number.isInteger(grade) || grade < 2 || grade > 5) {
+      this._snackBar.open('Grade must be integer in range 2-5', 'Dismiss');
+      return;
+    }
+    this.studentService.addGrade(studentId, {
+      mark: grade,
+      description: descriptionEl.value,
+      subject: this.subject!,
+    }).subscribe(() => this.getSubject());
+  }
+
+  deleteGrade(studentId: number, gradeId: number): void {
+    this.studentService.deleteGrade(studentId, gradeId).subscribe(() => this.getSubject());
+  }
+
+  confirmPayment(studentId: number, confirmed: boolean): void {
+    this.studentService.confirmPayment(studentId, this.subject?.id!, confirmed).subscribe();
+  }
+
+}
+
+@Component({
+  selector: 'add-student-to-subject-dialog',
+  templateUrl: 'add-student-to-subject-dialog.component.html',
+})
+export class StudentSelectionDialog implements OnInit {
+  allStudents?: Student[];
+
+  constructor(public dialogRef: MatDialogRef<StudentSelectionDialog>, @Inject(MAT_DIALOG_DATA) public subject: Subject,
+              private studentService: StudentService) {
+  }
+
+  onNoClick(): void {
+    this.dialogRef.close();
+  }
+
+  ngOnInit(): void {
+    this.studentService.getStudents().subscribe(studentList => this.allStudents = studentList);
+  }
+}

+ 0 - 0
project-front/src/app/subjects/add-subject-dialog.component.html → project-front/src/app/subject/subject-list/add-subject-dialog.component.html


+ 15 - 0
project-front/src/app/subject/subject-list/subject-list.component.css

@@ -0,0 +1,15 @@
+table {
+    width: 100%;
+}
+
+button, h1 {
+    margin: 20px !important;
+}
+
+[mat-row] {
+    cursor: pointer;
+}
+
+mat-form-field {
+    margin-left: 20px;
+}

+ 9 - 3
project-front/src/app/subjects/subjects.component.html → project-front/src/app/subject/subject-list/subject-list.component.html

@@ -1,7 +1,13 @@
+<h1 class="mat-display-1">Subjects</h1>
 <button mat-raised-button color="primary" (click)="openDialog()">Add</button>
 <button mat-raised-button color="primary" (click)="delete()">Remove</button>
+<br>
+<mat-form-field appearance="fill">
+  <mat-label>Filter</mat-label>
+  <input matInput (keyup)="applyFilter($event)" #filter>
+</mat-form-field>
 
-<table *ngIf="subjectList" mat-table [dataSource]="subjectList">
+<table *ngIf="subjectList" mat-table [dataSource]="subjectList" matSort>
   <ng-container matColumnDef="select">
     <th mat-header-cell *matHeaderCellDef>
       <mat-checkbox (change)="$event ? masterToggle() : null"
@@ -25,7 +31,7 @@
   </ng-container>
 
   <ng-container matColumnDef="name">
-    <th mat-header-cell *matHeaderCellDef> Name</th>
+    <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> Name</th>
     <td mat-cell *matCellDef="let subject"> {{subject.name}} </td>
   </ng-container>
 
@@ -35,7 +41,7 @@
   </ng-container>
 
   <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
-  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+  <tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="[row.id]"></tr>
 </table>
 
 <mat-paginator [pageSizeOptions]="[5, 10, 20]"

+ 5 - 5
project-front/src/app/subjects/subjects.component.spec.ts → project-front/src/app/subject/subject-list/subject-list.component.spec.ts

@@ -1,20 +1,20 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { SubjectsComponent } from './subjects.component';
+import { SubjectListComponent } from './subject-list.component';
 
 describe('SubjectsComponent', () => {
-  let component: SubjectsComponent;
-  let fixture: ComponentFixture<SubjectsComponent>;
+  let component: SubjectListComponent;
+  let fixture: ComponentFixture<SubjectListComponent>;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      declarations: [ SubjectsComponent ]
+      declarations: [ SubjectListComponent ]
     })
     .compileComponents();
   });
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(SubjectsComponent);
+    fixture = TestBed.createComponent(SubjectListComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });

+ 16 - 8
project-front/src/app/subjects/subjects.component.ts → project-front/src/app/subject/subject-list/subject-list.component.ts

@@ -1,25 +1,27 @@
 import { AfterViewInit, Component, Inject, OnInit, ViewChild } from '@angular/core';
-import { Subject } from "./subjects.model";
-import { SubjectsService } from "./subjects.service";
+import { Subject } from "../subject.model";
+import { SubjectService } from "../subject.service";
 import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
 import { MatTable, MatTableDataSource } from "@angular/material/table";
 import { MatPaginator } from "@angular/material/paginator";
 import { SelectionModel } from "@angular/cdk/collections";
+import { MatSort } from "@angular/material/sort";
 
 @Component({
   selector: 'app-subjects',
-  templateUrl: './subjects.component.html',
-  styleUrls: ['./subjects.component.css']
+  templateUrl: './subject-list.component.html',
+  styleUrls: ['./subject-list.component.css']
 })
-export class SubjectsComponent implements OnInit, AfterViewInit {
-  displayedColumns: string[] = ['select', 'no', 'name', 'students'];
+export class SubjectListComponent implements OnInit, AfterViewInit {
+  displayedColumns: string[] = ['select', 'name', 'students'];
   subjectList = new MatTableDataSource<Subject>([]);
   selection = new SelectionModel<Subject>(true, []);
   subject?: Subject;
-  @ViewChild(MatTable) table: MatTable<Subject> | undefined;
+  @ViewChild(MatTable) table!: MatTable<Subject>;
   @ViewChild(MatPaginator) paginator!: MatPaginator;
+  @ViewChild(MatSort) sort!: MatSort;
 
-  constructor(private subjectService: SubjectsService, public dialog: MatDialog) {
+  constructor(private subjectService: SubjectService, public dialog: MatDialog) {
   }
 
   ngOnInit(): void {
@@ -28,6 +30,7 @@ export class SubjectsComponent implements OnInit, AfterViewInit {
 
   ngAfterViewInit() {
     this.subjectList.paginator = this.paginator;
+    this.subjectList.sort = this.sort;
   }
 
   isAllSelected() {
@@ -89,6 +92,11 @@ export class SubjectsComponent implements OnInit, AfterViewInit {
     });
   }
 
+  applyFilter(event: Event) {
+    const filterValue = (event.target as HTMLInputElement).value;
+    this.subjectList.filter = filterValue.trim().toLowerCase();
+  }
+
 }
 
 @Component({

+ 2 - 2
project-front/src/app/subjects/subjects.model.ts → project-front/src/app/subject/subject.model.ts

@@ -1,9 +1,9 @@
-import { Student } from "../students/student.model";
+import { StudentAssoc } from "./studentAssoc";
 
 export class Subject {
   id?: number;
   name: string;
-  studentsAssoc?: Student[];
+  studentsAssoc?: StudentAssoc[];
 
   constructor(name: string) {
     this.name = name;

+ 3 - 3
project-front/src/app/subjects/subjects.service.spec.ts → project-front/src/app/subject/subject.service.spec.ts

@@ -1,13 +1,13 @@
 import { TestBed } from '@angular/core/testing';
 
-import { SubjectsService } from './subjects.service';
+import { SubjectService } from './subject.service';
 
 describe('SubjectsService', () => {
-  let service: SubjectsService;
+  let service: SubjectService;
 
   beforeEach(() => {
     TestBed.configureTestingModule({});
-    service = TestBed.inject(SubjectsService);
+    service = TestBed.inject(SubjectService);
   });
 
   it('should be created', () => {

+ 16 - 2
project-front/src/app/subjects/subjects.service.ts → project-front/src/app/subject/subject.service.ts

@@ -1,7 +1,8 @@
 import { Injectable } from '@angular/core';
 import { HttpClient, HttpHeaders } from "@angular/common/http";
 import { catchError, Observable, of, tap } from "rxjs";
-import { Subject } from "./subjects.model";
+import { Subject } from "./subject.model";
+import { Student } from "../students/student.model";
 
 const httpOptions = {
   headers: new HttpHeaders({ 'Content-Type': 'application/json' })
@@ -14,7 +15,7 @@ export interface Changeset {
 @Injectable({
   providedIn: 'root'
 })
-export class SubjectsService {
+export class SubjectService {
 
   private subjectsUrl = 'http://localhost:8080/subjects';
 
@@ -24,6 +25,11 @@ export class SubjectsService {
     return this.http.get<Subject[]>(this.subjectsUrl);
   }
 
+  getSubject(id: number): Observable<Subject> {
+    const url = `${this.subjectsUrl}/${id}`;
+    return this.http.get<Subject>(url);
+  }
+
   addSubject(subject: Subject): Observable<Subject> {
     return this.http.post<Subject>(this.subjectsUrl, subject, httpOptions).pipe(
       tap((subjectAdded: Subject) => this.log(`added subject id=${subjectAdded.id}`)),
@@ -36,6 +42,14 @@ export class SubjectsService {
     return this.http.post(`${this.subjectsUrl}/delete`, {records: ids}, httpOptions);
   }
 
+  addStudentToSubject(subject: Subject, ids: Number[]): Observable<any> {
+    return this.http.patch(`${this.subjectsUrl}/${subject.id}`, {studentAssoc: ids}, httpOptions);
+  }
+
+  // removeStudentFromSubject(subject: Subject, student: Student): Observable<any> {
+  //
+  // }
+
   private log(message: string): void {
     console.log('SubjectService: ' + message);
   }

+ 0 - 7
project-front/src/app/subjects/subjects.component.css

@@ -1,7 +0,0 @@
-table {
-    width: 100%;
-}
-
-button {
-    margin: 20px;
-}