diff --git a/server/pom.xml b/server/pom.xml index e945b64..12901ed 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -10,15 +10,14 @@ 0.0.1 - ru.mskobaro server 0.0.1 TDMS::SERVER - 17 - 17 + 23 + 23 UTF-8 1.18.34 @@ -127,6 +126,8 @@ ${lombok.version} + 23 + 23 diff --git a/server/src/main/java/ru/mskobaro/tdms/TdmsApplication.java b/server/src/main/java/ru/mskobaro/tdms/TdmsApplication.java index a3b12d0..96357ab 100644 --- a/server/src/main/java/ru/mskobaro/tdms/TdmsApplication.java +++ b/server/src/main/java/ru/mskobaro/tdms/TdmsApplication.java @@ -15,7 +15,7 @@ public class TdmsApplication { Thread.currentThread().setName("spring-bootstrapper"); ConfigurableApplicationContext ctx = SpringApplication.run(TdmsApplication.class, args); Environment environment = ctx.getEnvironment(); - log.info("Static files location: {}", environment.getProperty("spring.web.resources.static-locations")); + log.info("Server listening on port: {}", environment.getProperty("server.port")); ctx.start(); } } \ No newline at end of file diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/AuditInfo.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/AuditInfo.java new file mode 100644 index 0000000..2e43ca9 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/AuditInfo.java @@ -0,0 +1,21 @@ +package ru.mskobaro.tdms.business.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +@Embeddable +@Getter +public class AuditInfo { + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt; + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/CommissionMemberData.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/CommissionMemberData.java new file mode 100644 index 0000000..3e283ee --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/CommissionMemberData.java @@ -0,0 +1,25 @@ +package ru.mskobaro.tdms.business.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "commission_member_data") +@Getter +@Setter +public class CommissionMemberData { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "partic_id", referencedColumnName = "id") + private Participant participant; + + private String workPlace; + private String workPosition; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/Defense.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/Defense.java new file mode 100644 index 0000000..795a1ee --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/Defense.java @@ -0,0 +1,40 @@ +package ru.mskobaro.tdms.business.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "defense") +@Getter +@Setter +public class Defense { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToMany + @JoinTable( + name = "defense_commission", + joinColumns = @JoinColumn(name = "defense_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "commission_member_data_id", referencedColumnName = "id")) + private List commissionMembers; + + private LocalDate defenseDate; + + @OneToMany(mappedBy = "defense") + private List groups; + + @ManyToMany + @JoinTable( + name = "defense_best_student_works", + joinColumns = @JoinColumn(name = "defense_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "student_data_id", referencedColumnName = "id")) + private List bestWorks; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/DiplomaTopic.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/DiplomaTopic.java new file mode 100644 index 0000000..e8652d2 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/DiplomaTopic.java @@ -0,0 +1,35 @@ +package ru.mskobaro.tdms.business.entity; + + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + + +@Getter +@Setter +@ToString +@Entity +@Table(name = "diploma_topic") +public class DiplomaTopic { + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name") + private String name; + + @ManyToOne + @JoinColumn(name = "teacher_id") + private TeacherData teacher; + + @ManyToOne + @JoinColumn(name = "direction_of_preparation_id") + private DirectionOfPreparation directionOfPreparation; + + @Embedded + private AuditInfo auditInfo; +} + diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/DirectionOfPreparation.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/DirectionOfPreparation.java new file mode 100644 index 0000000..e39483c --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/DirectionOfPreparation.java @@ -0,0 +1,27 @@ +package ru.mskobaro.tdms.business.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Entity +@Table(name = "direction_of_preparation") +@Getter +@Setter +public class DirectionOfPreparation { + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String code; + + @OneToMany(mappedBy = "directionOfPreparation") + private List diplomaTopic; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/Group.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/Group.java new file mode 100644 index 0000000..4a93a33 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/Group.java @@ -0,0 +1,40 @@ +package ru.mskobaro.tdms.business.entity; + + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + + +@Getter +@Setter +@ToString +@Entity +@Table(name = "`group`") +public class Group { + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name") + private String name; + + @OneToMany(mappedBy = "group") + private List students = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "direction_of_preparation_id") + private DirectionOfPreparation directionOfPreparation; + + @ManyToOne + @JoinColumn(name = "defense_id") + private Defense defense; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/Participant.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/Participant.java new file mode 100644 index 0000000..dd63780 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/Participant.java @@ -0,0 +1,67 @@ +package ru.mskobaro.tdms.business.entity; + +import io.micrometer.common.util.StringUtils; +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Entity +@Table(name = "participant") +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class Participant { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String firstName; + private String lastName; + private String middleName; + + private String email; + + private String numberPhone; + + @OneToOne(mappedBy = "participant") + private User user; + + private boolean deleted; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "participant_role", + joinColumns = @JoinColumn(name = "partic_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) + private List roles; + + @Embedded + private AuditInfo auditInfo; + + @Transient + public String getFullName() { + return Stream.of(lastName, firstName, middleName) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(" ")); + } + + @Transient + public String getShortName() { + StringBuilder builder = new StringBuilder(); + if (StringUtils.isNotBlank(lastName)) { + builder.append(lastName); + } + if (StringUtils.isNotBlank(firstName)) { + builder.append(" ").append(firstName.charAt(0)).append("."); + } + if (StringUtils.isNotBlank(middleName)) { + builder.append(" ").append(middleName.charAt(0)).append("."); + } + return builder.toString().trim(); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Role.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/Role.java similarity index 76% rename from server/src/main/java/ru/mskobaro/tdms/domain/entity/Role.java rename to server/src/main/java/ru/mskobaro/tdms/business/entity/Role.java index 496e771..195db27 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Role.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/Role.java @@ -1,14 +1,11 @@ -package ru.mskobaro.tdms.domain.entity; +package ru.mskobaro.tdms.business.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; +import lombok.*; import org.springframework.security.core.GrantedAuthority; @@ -18,6 +15,7 @@ import org.springframework.security.core.GrantedAuthority; @AllArgsConstructor @NoArgsConstructor @Table(name = "`role`") +@EqualsAndHashCode(of = "id") public class Role implements GrantedAuthority { @Id @Column(name = "id") diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/StudentData.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/StudentData.java new file mode 100644 index 0000000..dde5f67 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/StudentData.java @@ -0,0 +1,50 @@ +package ru.mskobaro.tdms.business.entity; + + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import ru.mskobaro.tdms.integration.database.TeacherDataRepository; + + +@Getter +@Setter +@ToString(exclude = "group") +@Entity +@Table(name = "student_data") +public class StudentData { + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "partic_id") + private Participant participant; + + @ManyToOne + @JoinColumn(name = "group_id") + private Group group; + + @ManyToOne + @JoinColumn(name = "study_form_id") + private StudyForm form; + + private Integer protectionOrder; + private Integer protectionDay; + + private Integer markComment; + private Integer markPractice; + + @ManyToOne + @JoinColumn(name = "curator_id") + private TeacherData curator; + + @ManyToOne + @JoinColumn(name = "diploma_topic_id") + private DiplomaTopic diplomaTopic; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/entity/DiplomaTopic.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/StudyForm.java similarity index 60% rename from server/src/main/java/ru/mskobaro/tdms/domain/entity/DiplomaTopic.java rename to server/src/main/java/ru/mskobaro/tdms/business/entity/StudyForm.java index 2675a87..98ff818 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/entity/DiplomaTopic.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/StudyForm.java @@ -1,23 +1,21 @@ -package ru.mskobaro.tdms.domain.entity; - +package ru.mskobaro.tdms.business.entity; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; -import lombok.ToString; - +@Entity +@Table(name = "study_form") @Getter @Setter -@ToString -@Entity -@Table(name = "diploma_topic") -public class DiplomaTopic { +public class StudyForm { @Id - @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "name") private String name; -} + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/Task.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/Task.java new file mode 100644 index 0000000..102a4f2 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/Task.java @@ -0,0 +1,41 @@ +package ru.mskobaro.tdms.business.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import ru.mskobaro.tdms.business.taskfields.TaskFields; + +@Entity +@Table(name = "task") +@NoArgsConstructor +@Getter +@Setter +public class Task { + public enum Type { + DIPLOMA_TOPIC_AGREEMENT, + } + + public enum Status { + WAIT_FOR_TOPIC_AGREEMENT, + DONE, + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private Type type; + + @Enumerated(EnumType.STRING) + private Status status; + + @JdbcTypeCode(SqlTypes.JSON) + private TaskFields fields; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/entity/TeacherData.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/TeacherData.java new file mode 100644 index 0000000..6c7f0a7 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/TeacherData.java @@ -0,0 +1,24 @@ +package ru.mskobaro.tdms.business.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "teacher_data") +@Getter +@Setter +public class TeacherData { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "participant_id", referencedColumnName = "id") + private Participant participant; + + private String degree; + + @Embedded + private AuditInfo auditInfo; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/entity/User.java b/server/src/main/java/ru/mskobaro/tdms/business/entity/User.java similarity index 55% rename from server/src/main/java/ru/mskobaro/tdms/domain/entity/User.java rename to server/src/main/java/ru/mskobaro/tdms/business/entity/User.java index 0e1f039..c98dcda 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/entity/User.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/entity/User.java @@ -1,4 +1,4 @@ -package ru.mskobaro.tdms.domain.entity; +package ru.mskobaro.tdms.business.entity; import jakarta.persistence.*; @@ -6,13 +6,10 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; @@ -32,33 +29,26 @@ public class User implements UserDetails { private String login; @Column(name = "password") private String password; - @Column(name = "full_name") - private String fullName; - @Column(name = "email") - private String email; - @Column(name = "number_phone") - private String numberPhone; - @Column(name = "created_at") - @CreationTimestamp - private ZonedDateTime createdAt; - @Column(name = "updated_at") - @UpdateTimestamp - private ZonedDateTime updatedAt; - @ManyToMany - @JoinTable( - name = "user_role", - joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) - private List roles; + @OneToOne + @JoinColumn(name = "partic_id") + private Participant participant; + + @Embedded + private AuditInfo auditInfo; @Override public Collection getAuthorities() { - return roles.stream() + return participant.getRoles().stream() .map(Role::getAuthority) .map(SimpleGrantedAuthority::new) .toList(); } + @Override + public boolean isEnabled() { + return !participant.isDeleted(); + } + @Override public String getUsername() { return login; diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/exception/AccessDeniedException.java b/server/src/main/java/ru/mskobaro/tdms/business/exception/AccessDeniedException.java similarity index 57% rename from server/src/main/java/ru/mskobaro/tdms/domain/exception/AccessDeniedException.java rename to server/src/main/java/ru/mskobaro/tdms/business/exception/AccessDeniedException.java index 606a9d1..d68250c 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/exception/AccessDeniedException.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/exception/AccessDeniedException.java @@ -1,12 +1,16 @@ -package ru.mskobaro.tdms.domain.exception; +package ru.mskobaro.tdms.business.exception; -import ru.mskobaro.tdms.presentation.payload.ErrorDTO; +import ru.mskobaro.tdms.presentation.controller.payload.ErrorDTO; public class AccessDeniedException extends BusinessException { public AccessDeniedException() { super("Access denied"); } + public AccessDeniedException(String message) { + super(message); + } + @Override public ErrorDTO.ErrorCode getErrorCode() { return ErrorDTO.ErrorCode.ACCESS_DENIED; diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/exception/BusinessException.java b/server/src/main/java/ru/mskobaro/tdms/business/exception/BusinessException.java similarity index 68% rename from server/src/main/java/ru/mskobaro/tdms/domain/exception/BusinessException.java rename to server/src/main/java/ru/mskobaro/tdms/business/exception/BusinessException.java index 1cc9574..de2b01c 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/exception/BusinessException.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/exception/BusinessException.java @@ -1,6 +1,6 @@ -package ru.mskobaro.tdms.domain.exception; +package ru.mskobaro.tdms.business.exception; -import ru.mskobaro.tdms.presentation.payload.ErrorDTO; +import ru.mskobaro.tdms.presentation.controller.payload.ErrorDTO; public class BusinessException extends RuntimeException { public BusinessException(String message) { diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/exception/NotFoundException.java b/server/src/main/java/ru/mskobaro/tdms/business/exception/NotFoundException.java similarity index 72% rename from server/src/main/java/ru/mskobaro/tdms/domain/exception/NotFoundException.java rename to server/src/main/java/ru/mskobaro/tdms/business/exception/NotFoundException.java index 278081b..afd2631 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/exception/NotFoundException.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/exception/NotFoundException.java @@ -1,18 +1,18 @@ -package ru.mskobaro.tdms.domain.exception; +package ru.mskobaro.tdms.business.exception; -import ru.mskobaro.tdms.presentation.payload.ErrorDTO; +import ru.mskobaro.tdms.presentation.controller.payload.ErrorDTO; public class NotFoundException extends BusinessException { public NotFoundException(Class entityClass, Object id) { - super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден"); + super(entityClass.getSimpleName() + " с идентификатором " + id + " не существует"); } public NotFoundException(Class entityClass) { - super(entityClass.getSimpleName() + " не найден"); + super(entityClass.getSimpleName() + " не существует"); } public NotFoundException() { - super("Не найдено"); + super("Не существует"); } public static void throwIfNull(Object object) { diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/AuthenticationService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/AuthenticationService.java similarity index 50% rename from server/src/main/java/ru/mskobaro/tdms/domain/service/AuthenticationService.java rename to server/src/main/java/ru/mskobaro/tdms/business/service/AuthenticationService.java index 4080bc4..6bdc862 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/AuthenticationService.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/AuthenticationService.java @@ -1,4 +1,4 @@ -package ru.mskobaro.tdms.domain.service; +package ru.mskobaro.tdms.business.service; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; @@ -6,10 +6,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; + @Service @Slf4j public class AuthenticationService { @@ -17,25 +21,41 @@ public class AuthenticationService { private HttpServletRequest request; @Autowired private AuthenticationManager authenticationManager; + @Autowired + private SessionRegistry sessionRegistry; public void logout() { - log.info("Logging out user: {}", SecurityContextHolder.getContext().getAuthentication().getName()); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return; + } + + String username = authentication.getName(); + HttpSession session = request.getSession(false); - if(session != null) { + if (session != null) { session.invalidate(); } + SecurityContextHolder.clearContext(); - log.info("User logged out"); + + log.info("User {} logged out", username); + } + + public void logout(String username) { + sessionRegistry.getAllSessions(username, false).forEach(session -> { + log.info("Invalidating session for user {}: {}", username, session.getSessionId()); + session.expireNow(); + sessionRegistry.removeSessionInformation(session.getSessionId()); + }); } @Transactional public void login(String username, String password) { - log.info("Logging in user: {}, ip: {}", username, request.getRemoteAddr()); - var token = new UsernamePasswordAuthenticationToken(username, password); var authenticated = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authenticated); - - log.info("User {} logged in", username); + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); + log.info("User {} logged in, ip: {}", username, request.getRemoteAddr()); } } diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/DefenceService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/DefenceService.java new file mode 100644 index 0000000..1510eca --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/DefenceService.java @@ -0,0 +1,20 @@ +package ru.mskobaro.tdms.business.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.mskobaro.tdms.integration.database.DefenceRepository; +import ru.mskobaro.tdms.presentation.controller.payload.DefenceDTO; + +import java.util.List; + +@Service +@Transactional +public class DefenceService { + @Autowired + private DefenceRepository defenceRepository; + + public List getAllDefences() { + return defenceRepository.findAll().stream().map(DefenceDTO::from).toList(); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/DiplomaTopicService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/DiplomaTopicService.java new file mode 100644 index 0000000..af341e7 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/DiplomaTopicService.java @@ -0,0 +1,55 @@ +package ru.mskobaro.tdms.business.service; + +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.mskobaro.tdms.business.entity.DiplomaTopic; +import ru.mskobaro.tdms.business.entity.DirectionOfPreparation; +import ru.mskobaro.tdms.business.entity.TeacherData; +import ru.mskobaro.tdms.integration.database.DiplomaTopicRepository; +import ru.mskobaro.tdms.integration.database.PreparationDirectionRepository; +import ru.mskobaro.tdms.integration.database.TeacherDataRepository; +import ru.mskobaro.tdms.presentation.controller.payload.DiplomaTopicDTO; + +import java.util.List; + +@Service +@Transactional +public class DiplomaTopicService { + @Autowired + private DiplomaTopicRepository diplomaTopicRepository; + @Autowired + private TeacherDataRepository teacherDataRepository; + @Autowired + private PreparationDirectionRepository preparationDirectionRepository; + + public List findAll() { + return diplomaTopicRepository.findAll(); + } + + public void save(DiplomaTopicDTO diplomaTopicDTO) { + boolean isEdit = diplomaTopicDTO.getId() != null; + DiplomaTopic diplomaTopic; + if (isEdit) { + diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicDTO.getId()); + } else { + diplomaTopic = new DiplomaTopic(); + } + + diplomaTopic.setName(diplomaTopicDTO.getName()); + if (diplomaTopicDTO.getTeacher() != null) { + TeacherData teacherData = teacherDataRepository.findByIdThrow(diplomaTopicDTO.getTeacher().getId()); + diplomaTopic.setTeacher(teacherData); + } + if (diplomaTopicDTO.getPreparationDirection() != null) { + DirectionOfPreparation directionOfPreparation = preparationDirectionRepository.findByIdThrow(diplomaTopicDTO.getPreparationDirection().getId()); + diplomaTopic.setDirectionOfPreparation(directionOfPreparation); + } + + diplomaTopicRepository.save(diplomaTopic); + } + + public List findAllForStudent(Long studentId) { + return diplomaTopicRepository.findAllForStudentId(studentId); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/GroupService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/GroupService.java new file mode 100644 index 0000000..9518b84 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/GroupService.java @@ -0,0 +1,78 @@ +package ru.mskobaro.tdms.business.service; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import ru.mskobaro.tdms.business.entity.DirectionOfPreparation; +import ru.mskobaro.tdms.business.entity.Group; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.business.exception.BusinessException; +import ru.mskobaro.tdms.integration.database.GroupRepository; +import ru.mskobaro.tdms.integration.database.PreparationDirectionRepository; +import ru.mskobaro.tdms.integration.database.StudentDataRepository; +import ru.mskobaro.tdms.presentation.controller.payload.GroupDTO; +import ru.mskobaro.tdms.presentation.controller.payload.StudentDataDTO; + +import java.util.Collection; +import java.util.List; + +@Service +@Transactional +@Slf4j +public class GroupService { + @Autowired + private GroupRepository groupRepository; + @Autowired + private RoleService roleService; + @Autowired + private StudentDataRepository studentDataRepository; + @Autowired + private PreparationDirectionRepository preparationDirectionRepository; + + public Collection getAllGroups() { + List groups = groupRepository.findAll(); + return groups.stream().map(g -> GroupDTO.from(g, true)).toList(); + } + + public void save(@Valid GroupDTO groupDTO) { + boolean editMode = groupDTO.getId() != null; + log.info("Saving group: {}. Edit?: {}", groupDTO, editMode); + if (!editMode && groupRepository.existsByName(groupDTO.getName())) { + throw new BusinessException("Группа с именем " + groupDTO.getName() + " уже существует"); + } + + Group group = editMode ? groupRepository.findByIdThrow(groupDTO.getId()) : new Group(); + group.setName(groupDTO.getName()); + + studentDataRepository.findAllByGroup_Id(group.getId()).forEach(s -> { + s.setGroup(null); + studentDataRepository.save(s); + }); + group.getStudents().clear(); + + List studentIds = groupDTO.getStudents().stream().map(StudentDataDTO::getId).toList(); + if (!CollectionUtils.isEmpty(studentIds)) { + List students = studentDataRepository.findAllById(studentIds); + students.stream() + .filter(s -> roleService.isParticInAuthority(s.getParticipant(), RoleService.Authority.STUDENT)) + .forEach(s -> { + group.getStudents().add(s); + s.setGroup(group); + }); + } + + if (groupDTO.getPreparationDirection() != null) { + DirectionOfPreparation directionOfPreparation = preparationDirectionRepository.findByIdThrow(groupDTO.getPreparationDirection().getId()); + group.setDirectionOfPreparation(directionOfPreparation); + } + + groupRepository.save(group); + } + + public void deleteGroup(Long groupId) { + groupRepository.deleteById(groupId); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/ParticipantService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/ParticipantService.java new file mode 100644 index 0000000..0fe0974 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/ParticipantService.java @@ -0,0 +1,220 @@ +package ru.mskobaro.tdms.business.service; + +import io.micrometer.common.util.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.mskobaro.tdms.business.entity.*; +import ru.mskobaro.tdms.business.exception.AccessDeniedException; +import ru.mskobaro.tdms.business.exception.BusinessException; +import ru.mskobaro.tdms.integration.database.*; +import ru.mskobaro.tdms.presentation.controller.payload.ParticipantSaveDTO; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Transactional +@Slf4j +public class ParticipantService { + @Autowired + private ParticipantRepository participantRepository; + @Autowired + private UserService userService; + @Autowired + private RoleService roleService; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private UserRepository userRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private StudentDataRepository studentDataRepository; + @Autowired + private AuthenticationService authenticationService; + @Autowired + private TeacherDataRepository teacherDataRepository; + + public ParticipantService(ParticipantRepository participantRepository) { + this.participantRepository = participantRepository; + } + + public Collection getAllParticipants() { + return participantRepository.findAll(); + } + + public void saveParticipant(ParticipantSaveDTO participantSaveDTO) { + boolean editMode = participantSaveDTO.getId() != null; + log.info("Saving participant: {}. Edit: {}", participantSaveDTO, editMode); + Participant existingParticipant = null; + if (editMode) + existingParticipant = participantRepository.findByIdThrow(participantSaveDTO.getId()); + persistParticipant(participantSaveDTO, existingParticipant, editMode); + } + + private void persistParticipant(ParticipantSaveDTO participantSaveDTO, Participant existingParticipant, boolean editMode) { + Participant participant = !editMode ? new Participant() : existingParticipant; + User callerUser = userService.getCallerUser(); + if (callerUser == null) + throw new AccessDeniedException(); + + participant.setFirstName(participantSaveDTO.getFirstName()); + participant.setLastName(participantSaveDTO.getLastName()); + participant.setMiddleName(participantSaveDTO.getMiddleName()); + participant.setNumberPhone(participantSaveDTO.getNumberPhone()); + participant.setEmail(participantSaveDTO.getEmail()); + + List roles = persistRoles(participantSaveDTO, existingParticipant, editMode, callerUser, participant); + boolean credentialsChanged = persistUserData(participantSaveDTO, existingParticipant, editMode, participant); + persistStudentData(participantSaveDTO, existingParticipant, editMode, roles, participant); + persistTeacherData(participantSaveDTO, existingParticipant, editMode, roles, participant); + + // TODO: notification task + Participant saved = participantRepository.save(participant); + log.info("Participant saved: {}", saved.getFullName()); + + if (credentialsChanged) { + log.info("User {} changed credentials, logging out", saved.getUser().getUsername()); + authenticationService.logout(saved.getUser().getUsername()); + } + } + + private List persistRoles(ParticipantSaveDTO participantSaveDTO, Participant existingParticipant, boolean editMode, User callerUser, Participant participant) { + boolean isAdmin = roleService.isParticInAuthority(callerUser.getParticipant(), RoleService.Authority.ADMIN); + boolean isSecretary = roleService.isParticInAuthority(callerUser.getParticipant(), RoleService.Authority.SECRETARY); + boolean isOwner = existingParticipant != null && existingParticipant.getUser() != null + && existingParticipant.getUser().getId().equals(callerUser.getId()); + if (!isAdmin && !isSecretary && !isOwner) + throw new AccessDeniedException(); + if (participantSaveDTO.getAuthorities() != null && participantSaveDTO.getAuthorities().contains(RoleService.Authority.ADMIN) && !isAdmin) + throw new AccessDeniedException("Недостаточно прав для назначения роли администратора"); + + List roles = participantSaveDTO.getAuthorities() != null + ? participantSaveDTO.getAuthorities() + .stream() + .map(roleService::getRoleByAuthority) + .collect(Collectors.toList()) + : null; + + if (editMode && isOwner && !isAdmin && roles != null && !ListUtils.isEqualList(roles, existingParticipant.getRoles())) { + throw new AccessDeniedException("Вы не можете изменять свои роли"); + } else if (roles != null) { + participant.setRoles(roles); + } + return roles; + } + + private boolean persistUserData(ParticipantSaveDTO participantSaveDTO, Participant existingParticipant, boolean editMode, Participant participant) { + boolean credentialsChanged = false; + if (participantSaveDTO.getUserData() == null) { + return credentialsChanged; + } + + User user; + boolean wasBefore = false; + if (editMode && existingParticipant.getUser() != null) { + user = existingParticipant.getUser(); + wasBefore = true; + } else { + user = new User(); + } + + String login = participantSaveDTO.getUserData().getLogin(); + if (StringUtils.isNotBlank(login)) { + user.setLogin(login); + credentialsChanged = true; + } else if (!wasBefore) { + throw new BusinessException("Логин не может быть пустым"); + } + + String password = participantSaveDTO.getUserData().getPassword(); + if (StringUtils.isNotBlank(password)) { + user.setPassword(passwordEncoder.encode(password)); + credentialsChanged = true; + } else if (!wasBefore) { + throw new BusinessException("Пароль не может быть пустым"); + } + + user = userRepository.save(user); + participant.setUser(user); + user.setParticipant(participant); + return credentialsChanged; + } + + private void persistTeacherData(ParticipantSaveDTO participantSaveDTO, Participant existingParticipant, boolean editMode, List roles, Participant participant) { + boolean shouldPersistTeacherData = participantSaveDTO.getTeacherData() != null && roles != null + && CollectionUtils.containsAny(roles, roleService.getRoleByAuthority(RoleService.Authority.TEACHER)); + if (!shouldPersistTeacherData) { + return; + } + + boolean alreadyExists = editMode && teacherDataRepository.existsByParticipant_IdAndParticipant_DeletedFalse(existingParticipant.getId()); + TeacherData teacherData; + if (alreadyExists) { + teacherData = teacherDataRepository.findByParticipant_Id(existingParticipant.getId()); + } else { + teacherData = new TeacherData(); + } + + teacherData.setDegree(participantSaveDTO.getTeacherData().getDegree()); + + teacherData = teacherDataRepository.save(teacherData); + teacherData.setParticipant(participant); + } + + private void persistStudentData(ParticipantSaveDTO participantSaveDTO, Participant existingParticipant, boolean editMode, List roles, Participant participant) { + boolean shouldPersistStudentData = participantSaveDTO.getStudentData() != null && roles != null + && CollectionUtils.containsAny(roles, roleService.getRoleByAuthority(RoleService.Authority.STUDENT)); + if (!shouldPersistStudentData) { + return; + } + + boolean alreadyExists = editMode && studentDataRepository.existsByParticipant_IdAndParticipant_DeletedFalse(existingParticipant.getId()); + StudentData studentData; + if (alreadyExists) { + studentData = studentDataRepository.findStudentDataByParticipant_Id(existingParticipant.getId()); + } else { + studentData = new StudentData(); + } + + if (participantSaveDTO.getStudentData().getGroupId() != null) { + Group group = groupRepository.findByIdThrow(participantSaveDTO.getStudentData().getGroupId()); + studentData.setGroup(group); + if (!group.getStudents().contains(studentData)) + group.getStudents().add(studentData); + } else { + if (editMode) { + Group group = groupRepository.findByStudentsContaining(Collections.singletonList(studentData)); + if (group != null) + group.getStudents().remove(studentData); + } + studentData.setGroup(null); + } + + if (participantSaveDTO.getStudentData().getCuratorId() != null) { + TeacherData teacherData = teacherDataRepository.findByIdThrow(participantSaveDTO.getStudentData().getCuratorId()); + studentData.setCurator(teacherData); + } else { + studentData.setCurator(null); + } + + studentData = studentDataRepository.save(studentData); + studentData.setParticipant(participant); + } + + public void deleteParticipant(Long id) { + if (id == 1) { + throw new AccessDeniedException(); + } + + Participant partic = participantRepository.findByIdThrow(id); + partic.setDeleted(true); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/PreparationDirectionService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/PreparationDirectionService.java new file mode 100644 index 0000000..16437b3 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/PreparationDirectionService.java @@ -0,0 +1,34 @@ +package ru.mskobaro.tdms.business.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.mskobaro.tdms.business.entity.DirectionOfPreparation; +import ru.mskobaro.tdms.integration.database.PreparationDirectionRepository; +import ru.mskobaro.tdms.presentation.controller.payload.PreparationDirectionDTO; + +import java.util.List; + +@Service +@Transactional +public class PreparationDirectionService { + @Autowired + private PreparationDirectionRepository preparationDirectionRepository; + + public List getAll() { + return preparationDirectionRepository.findAll(); + } + + public void save(PreparationDirectionDTO preparationDirectionDTO) { + boolean editMode = preparationDirectionDTO.getId() != null; + DirectionOfPreparation preparationDirection; + if (editMode) { + preparationDirection = preparationDirectionRepository.findByIdThrow(preparationDirectionDTO.getId()); + } else { + preparationDirection = new DirectionOfPreparation(); + } + preparationDirection.setName(preparationDirectionDTO.getName()); + preparationDirection.setCode(preparationDirectionDTO.getCode()); + preparationDirectionRepository.save(preparationDirection); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/RoleService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/RoleService.java similarity index 56% rename from server/src/main/java/ru/mskobaro/tdms/domain/service/RoleService.java rename to server/src/main/java/ru/mskobaro/tdms/business/service/RoleService.java index 7790d3d..a667413 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/RoleService.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/RoleService.java @@ -1,5 +1,7 @@ -package ru.mskobaro.tdms.domain.service; +package ru.mskobaro.tdms.business.service; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -7,7 +9,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.mskobaro.tdms.domain.entity.Role; +import ru.mskobaro.tdms.business.entity.Participant; +import ru.mskobaro.tdms.business.entity.Role; import ru.mskobaro.tdms.integration.database.RoleRepository; import java.util.Map; @@ -17,17 +20,23 @@ import java.util.concurrent.ConcurrentHashMap; @Slf4j public class RoleService { @RequiredArgsConstructor - @Getter public enum Authority { ADMIN("ROLE_ADMINISTRATOR"), COMM_MEMBER("ROLE_COMMISSION_MEMBER"), TEACHER("ROLE_TEACHER"), + PLAGIARISM_CHECKER("ROLE_PLAGIARISM_CHECKER"), SECRETARY("ROLE_SECRETARY"), STUDENT("ROLE_STUDENT"), ; private final String authority; + @JsonValue + public String getAuthority() { + return authority; + } + + @JsonCreator public static Authority from(String authority) { for (Authority value : values()) { if (value.getAuthority().equals(authority)) { @@ -37,7 +46,8 @@ public class RoleService { throw new IllegalArgumentException("No such authority: " + authority); } } - public transient Map roles; + + private final Map roles = new ConcurrentHashMap<>(); @Autowired private RoleRepository roleRepository; @@ -45,7 +55,6 @@ public class RoleService { @PostConstruct @Transactional public void bootstrapRolesCache() { - roles = new ConcurrentHashMap<>(); roleRepository.findAll().forEach(role -> roles.put(role.getAuthority(), role)); log.info("Roles initialized: {}", roles); } @@ -53,4 +62,21 @@ public class RoleService { public Role getRoleByAuthority(Authority authority) { return roles.get(authority.getAuthority()); } + + public boolean isParticInAuthority(Participant participant, Authority authority) { + return participant.getRoles().stream() + .anyMatch(role -> role.getAuthority().equals(authority.getAuthority())); + } + + public boolean isParticInAnyAuthority(Participant participant, Authority... authority) { + return participant.getRoles().stream() + .anyMatch(role -> { + for (Authority auth : authority) { + if (role.getAuthority().equals(auth.getAuthority())) { + return true; + } + } + return false; + }); + } } diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/StudentDataService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/StudentDataService.java new file mode 100644 index 0000000..a10ff24 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/StudentDataService.java @@ -0,0 +1,29 @@ +package ru.mskobaro.tdms.business.service; + +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.mskobaro.tdms.business.entity.Participant; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.integration.database.StudentDataRepository; +import ru.mskobaro.tdms.integration.database.UserRepository; + +import java.util.Collection; +import java.util.List; + +@Service +@Transactional +@Slf4j +public class StudentDataService { + @Autowired + private StudentDataRepository studentDataRepository; + + public StudentData getStudentByParticIdThrow(Long particId) { + return studentDataRepository.findStudentDataByParticipant_Id(particId); + } + + public Collection getAllStudentsWithoutGroup() { + return studentDataRepository.findByGroupIsNull(); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/SysInfoService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/SysInfoService.java similarity index 86% rename from server/src/main/java/ru/mskobaro/tdms/domain/service/SysInfoService.java rename to server/src/main/java/ru/mskobaro/tdms/business/service/SysInfoService.java index 6e31cf6..38a068e 100644 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/SysInfoService.java +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/SysInfoService.java @@ -1,4 +1,4 @@ -package ru.mskobaro.tdms.domain.service; +package ru.mskobaro.tdms.business.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/TaskService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/TaskService.java new file mode 100644 index 0000000..ba9e370 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/TaskService.java @@ -0,0 +1,63 @@ +package ru.mskobaro.tdms.business.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.business.entity.Task; +import ru.mskobaro.tdms.business.entity.User; +import ru.mskobaro.tdms.business.taskfields.DiplomaTopicAgreementTaskFields; +import ru.mskobaro.tdms.business.taskfields.TaskFields; +import ru.mskobaro.tdms.integration.database.StudentDataRepository; +import ru.mskobaro.tdms.integration.database.TaskRepository; +import ru.mskobaro.tdms.presentation.controller.TaskController; + +import java.util.List; + +@Service +@Transactional +public class TaskService { + @Autowired + private TaskRepository taskRepository; + @Autowired + private UserService userService; + @Autowired + private StudentDataRepository studentDataRepository; + + public void createTask(Task.Type type, Task.Status status, TaskFields taskFields) { + Task task = new Task(); + task.setType(type); + task.setStatus(status); + task.setFields(taskFields); + taskRepository.save(task); + } + + public Task findDiplomaTopicAgreementTaskCallerMaker() { + User user = userService.getCallerUser(); + List diplomaTopicAgreementTaskByMakerId = taskRepository.findDiplomaTopicAgreementTaskByMakerId( + user.getParticipant().getId(), Task.Type.DIPLOMA_TOPIC_AGREEMENT + ); + if (diplomaTopicAgreementTaskByMakerId.isEmpty()) { + return null; + } + + if (diplomaTopicAgreementTaskByMakerId.size() > 1) { + throw new IllegalStateException(); + } + + return diplomaTopicAgreementTaskByMakerId; + } + + public void createDiplomaAgreementTask(TaskController.DiplomaTopicAgreementDTO diplomaTopicAgreementDTO) { + DiplomaTopicAgreementTaskFields taskFields = new DiplomaTopicAgreementTaskFields(); + User user = userService.getCallerUser(); + StudentData studentData = studentDataRepository.findStudentDataByParticipant_Id(user.getParticipant().getId()); + + taskFields.setCheckerParticipantId(user.getParticipant().getId()); + taskFields.setDiplomaTopicId(diplomaTopicAgreementDTO.getDiplomaTopicId()); + taskFields.setDiplomaTopicName(diplomaTopicAgreementDTO.getDiplomaTopicName()); + taskFields.setCheckerParticipantId(studentData.getCurator().getId()); + + createTask(Task.Type.DIPLOMA_TOPIC_AGREEMENT, Task.Status.WAIT_FOR_TOPIC_AGREEMENT, taskFields); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/TeacherDataService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/TeacherDataService.java new file mode 100644 index 0000000..f873a85 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/TeacherDataService.java @@ -0,0 +1,30 @@ +package ru.mskobaro.tdms.business.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.mskobaro.tdms.business.entity.TeacherData; +import ru.mskobaro.tdms.business.exception.NotFoundException; +import ru.mskobaro.tdms.integration.database.TeacherDataRepository; + +import java.util.List; + +@Service +@Transactional +public class TeacherDataService { + @Autowired + private TeacherDataRepository teacherDataRepository; + + public List findAll() { + return teacherDataRepository.findAll(); + } + + public TeacherData getTeacherDataByParticipantId(Long participantId) { + TeacherData teacher = teacherDataRepository.findByParticipant_Id(participantId); + if (teacher == null) { + throw new NotFoundException(TeacherData.class, participantId); + } + + return teacher; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/service/UserService.java b/server/src/main/java/ru/mskobaro/tdms/business/service/UserService.java new file mode 100644 index 0000000..51fbd6d --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/service/UserService.java @@ -0,0 +1,50 @@ +package ru.mskobaro.tdms.business.service; + +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import ru.mskobaro.tdms.business.entity.User; +import ru.mskobaro.tdms.integration.database.UserRepository; +import ru.mskobaro.tdms.presentation.controller.payload.UserDTO; + +import java.util.List; + +@Service +@Transactional +@Slf4j +public class UserService implements UserDetailsService { + @Autowired + private UserRepository userRepository; + + @Override + public User loadUserByUsername(String username) throws UsernameNotFoundException { + log.debug("Loading user with username: {}", username); + User user = userRepository.findUserByLogin(username).orElseThrow( + () -> new UsernameNotFoundException("User with login " + username + " not found")); + log.debug("User with login {} loaded", username); + return user; + } + + public List getAllUsers() { + log.debug("Loading all users"); + List users = userRepository.findAll().stream() + .map(UserDTO::fromEntity) + .toList(); + log.info("{} users loaded", users.size()); + return users; + } + + public User getCallerUser() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (!(principal instanceof User)) { + return null; + } + + return (User) principal; + } +} + diff --git a/server/src/main/java/ru/mskobaro/tdms/business/taskfields/DiplomaTopicAgreementTaskFields.java b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/DiplomaTopicAgreementTaskFields.java new file mode 100644 index 0000000..07fa3c3 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/DiplomaTopicAgreementTaskFields.java @@ -0,0 +1,11 @@ +package ru.mskobaro.tdms.business.taskfields; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DiplomaTopicAgreementTaskFields extends MakerCheckerTaskFields { + private String diplomaTopicName; + private Long diplomaTopicId; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/taskfields/MakerCheckerTaskFields.java b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/MakerCheckerTaskFields.java new file mode 100644 index 0000000..dce60eb --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/MakerCheckerTaskFields.java @@ -0,0 +1,13 @@ +package ru.mskobaro.tdms.business.taskfields; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class MakerCheckerTaskFields extends MakerTaskFields { + private Long checkerParticipantId; + private LocalDateTime approvedAt; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/taskfields/MakerTaskFields.java b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/MakerTaskFields.java new file mode 100644 index 0000000..c27b1ac --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/MakerTaskFields.java @@ -0,0 +1,10 @@ +package ru.mskobaro.tdms.business.taskfields; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MakerTaskFields extends TaskFields { + private Long makerParticipantId; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/business/taskfields/TaskFields.java b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/TaskFields.java new file mode 100644 index 0000000..20b8a27 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/business/taskfields/TaskFields.java @@ -0,0 +1,10 @@ +package ru.mskobaro.tdms.business.taskfields; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class TaskFields { + private LocalDateTime createdAt; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Group.java b/server/src/main/java/ru/mskobaro/tdms/domain/entity/Group.java deleted file mode 100644 index 72547ff..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Group.java +++ /dev/null @@ -1,35 +0,0 @@ -package ru.mskobaro.tdms.domain.entity; - - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.ZonedDateTime; - - -@Getter -@Setter -@ToString -@Entity -@Table(name = "`group`") -public class Group { - @Id - @Column(name = "id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(name = "name") - private String name; - @ManyToOne - @JoinColumn(name = "curator_teacher_id") - private Teacher groupCurator; - @Column(name = "created_at") - @CreationTimestamp - private ZonedDateTime createdAt; - @Column(name = "updated_at") - @UpdateTimestamp - private ZonedDateTime updatedAt; -} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Student.java b/server/src/main/java/ru/mskobaro/tdms/domain/entity/Student.java deleted file mode 100644 index 230230d..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Student.java +++ /dev/null @@ -1,68 +0,0 @@ -package ru.mskobaro.tdms.domain.entity; - - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.ZonedDateTime; - - -@Getter -@Setter -@ToString -@Entity -@Table(name = "student") -public class Student { - @Id - @Column(name = "id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(name = "form") - private Boolean form; - @Column(name = "protection_order") - private Integer protectionOrder; - @Column(name = "magistracy") - private String magistracy; - @Column(name = "digital_format_present") - private Boolean digitalFormatPresent; - @Column(name = "mark_comment") - private Integer markComment; - @Column(name = "mark_practice") - private Integer markPractice; - @Column(name = "predefence_comment") - private String predefenceComment; - @Column(name = "normal_control") - private String normalControl; - @Column(name = "anti_plagiarism") - private Integer antiPlagiarism; - @Column(name = "note") - private String note; - @Column(name = "record_book_returned") - private Boolean recordBookReturned; - @Column(name = "work") - private String work; - @OneToOne - @JoinColumn(name = "user_id") - private User user; - @ManyToOne - @JoinColumn(name = "diploma_topic_id") - private DiplomaTopic diplomaTopic; - // Научный руководитель - @ManyToOne - @JoinColumn(name = "adviser_teacher_id") - private Teacher adviser; - @ManyToOne - @JoinColumn(name = "group_id") - private Group group; - - @Column(name = "created_at") - @CreationTimestamp - private ZonedDateTime createdAt; - @Column(name = "updated_at") - @UpdateTimestamp - private ZonedDateTime updatedAt; -} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Teacher.java b/server/src/main/java/ru/mskobaro/tdms/domain/entity/Teacher.java deleted file mode 100644 index 4bd1938..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/entity/Teacher.java +++ /dev/null @@ -1,36 +0,0 @@ -package ru.mskobaro.tdms.domain.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.ZonedDateTime; -import java.util.List; - -@Getter -@Setter -@ToString -@Entity -@Table(name = "teacher") -public class Teacher { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @OneToOne - @JoinColumn(name = "user_id") - private User user; - @OneToMany(mappedBy = "groupCurator") - private List curatingGroups; - @OneToMany(mappedBy = "adviser") - private List advisingStudents; - - @Column(name = "created_at") - @CreationTimestamp - private ZonedDateTime createdAt; - @Column(name = "updated_at") - @UpdateTimestamp - private ZonedDateTime updatedAt; -} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/DiplomaTopicService.java b/server/src/main/java/ru/mskobaro/tdms/domain/service/DiplomaTopicService.java deleted file mode 100644 index adccdae..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/DiplomaTopicService.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.mskobaro.tdms.domain.service; - -import jakarta.transaction.Transactional; -import org.springframework.stereotype.Service; - -@Service -@Transactional -public class DiplomaTopicService { -} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/GroupService.java b/server/src/main/java/ru/mskobaro/tdms/domain/service/GroupService.java deleted file mode 100644 index f67babe..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/GroupService.java +++ /dev/null @@ -1,67 +0,0 @@ -package ru.mskobaro.tdms.domain.service; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import ru.mskobaro.tdms.domain.entity.Group; -import ru.mskobaro.tdms.domain.entity.User; -import ru.mskobaro.tdms.domain.exception.BusinessException; -import ru.mskobaro.tdms.integration.database.GroupRepository; -import ru.mskobaro.tdms.presentation.payload.GroupDTO; -import ru.mskobaro.tdms.presentation.payload.GroupEditDTO; - -import java.util.Collection; -import java.util.List; - -@Service -@Transactional -@Slf4j -public class GroupService { - @Autowired - private GroupRepository groupRepository; - @Autowired - private UserService userService; - - public Collection getAllGroups() { - log.info("Getting all groups"); - List groups = groupRepository.findAll(); - User callerUser = userService.getCallerUser(); - - List result = groups.stream().map(group -> { - GroupDTO groupDTO = new GroupDTO(); - groupDTO.setName(group.getName()); - groupDTO.setId(group.getId()); - - if (group.getGroupCurator() != null) { - groupDTO.setCuratorName(group.getGroupCurator().getUser().getFullName()); - if (callerUser != null) { - groupDTO.setIAmCurator(group.getGroupCurator().getUser().equals(callerUser)); - } - } - return groupDTO; - }).toList(); - - log.info("Found {} groups", result.size()); - return result; - } - - public void createGroup(String groupName) { - log.info("Creating group with name {}", groupName); - if (groupRepository.existsByName(groupName)) { - throw new BusinessException("Группа с именем " + groupName + " уже существует"); - } - - Group group = new Group(); - group.setName(groupName); - Group saved = groupRepository.save(group); - log.info("Group saved: {}", saved); - } - - public void editGroup(GroupEditDTO groupEditDTO) { - log.info("Updating group with dto: {}", groupEditDTO); - Group group = groupRepository.findByIdThrow(groupEditDTO.getId()); - group.setName(groupEditDTO.getName()); - log.info("Group updated: {}", group); - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/StudentService.java b/server/src/main/java/ru/mskobaro/tdms/domain/service/StudentService.java deleted file mode 100644 index f64f825..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/StudentService.java +++ /dev/null @@ -1,57 +0,0 @@ -package ru.mskobaro.tdms.domain.service; - -import jakarta.transaction.Transactional; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import ru.mskobaro.tdms.domain.entity.Student; -import ru.mskobaro.tdms.domain.entity.User; -import ru.mskobaro.tdms.domain.exception.BusinessException; -import ru.mskobaro.tdms.domain.exception.NotFoundException; -import ru.mskobaro.tdms.integration.database.StudentRepository; -import ru.mskobaro.tdms.integration.database.UserRepository; -import ru.mskobaro.tdms.presentation.payload.StudentDTO; - -@Service -@Transactional -@Slf4j -public class StudentService { - @Autowired - private StudentRepository studentRepository; - @Autowired - private UserService userService; - @Autowired - private UserRepository userRepository; - - public Student getCallerStudentThrow() { - userService.sureCallerInAnyRole(RoleService.Authority.STUDENT); - return studentRepository.findByUser(userService.getCallerUser()); - } - - public StudentDTO getCallerStudentDtoThrow() { - Student callerStudent = getCallerStudentThrow(); - if (callerStudent == null) { - throw new BusinessException("Вызывающий пользователь является студентом, но ассоциированный с ним студент не найден"); - } - - return StudentDTO.from(callerStudent); - } - - public StudentDTO getStudentByUserIdThrow(Long id) { - User callerUser = userService.getCallerUser(); - if (callerUser == null) { - throw new NotFoundException(); - } - - if (callerUser.getId().equals(id)) { - return getCallerStudentDtoThrow(); - } else if (userService.isCallerInRole(RoleService.Authority.ADMIN, RoleService.Authority.TEACHER, RoleService.Authority.SECRETARY)) { - User user = userRepository.findByIdThrow(id); - Student student = studentRepository.findByUser(user); - NotFoundException.throwIfNull(student, Student.class, user.getId()); - return StudentDTO.from(student); - } else { - throw new NotFoundException(Student.class, id); - } - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/domain/service/UserService.java b/server/src/main/java/ru/mskobaro/tdms/domain/service/UserService.java deleted file mode 100644 index bf6eec2..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/domain/service/UserService.java +++ /dev/null @@ -1,181 +0,0 @@ -package ru.mskobaro.tdms.domain.service; - -import jakarta.transaction.Transactional; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import ru.mskobaro.tdms.domain.entity.*; -import ru.mskobaro.tdms.domain.exception.AccessDeniedException; -import ru.mskobaro.tdms.domain.exception.NotFoundException; -import ru.mskobaro.tdms.integration.database.GroupRepository; -import ru.mskobaro.tdms.integration.database.StudentRepository; -import ru.mskobaro.tdms.integration.database.TeacherRepository; -import ru.mskobaro.tdms.integration.database.UserRepository; -import ru.mskobaro.tdms.presentation.payload.RegistrationDTO; -import ru.mskobaro.tdms.presentation.payload.UserDTO; - -import java.util.List; -import java.util.Optional; - -@Service -@Transactional -@Slf4j -public class UserService implements UserDetailsService { - @Autowired - private UserRepository userRepository; - @Autowired - private GroupRepository groupRepository; - @Autowired - private StudentRepository studentRepository; - @Autowired - private RoleService roleService; - @Autowired - private PasswordEncoder passwordEncoder; - @Autowired - private TeacherRepository teacherRepository; - - @Override - public User loadUserByUsername(String username) throws UsernameNotFoundException { - log.debug("Loading user with username: {}", username); - User user = userRepository.findUserByLogin(username).orElseThrow( - () -> new UsernameNotFoundException("User with login " + username + " not found")); - log.debug("User with login {} loaded", username); - return user; - } - - public List getAllUsers() { - log.debug("Loading all users"); - List users = userRepository.findAll().stream() - .map(UserDTO::from) - .toList(); - log.info("{} users loaded", users.size()); - return users; - } - - public void registerUser(RegistrationDTO registrationDTO) { - log.info("Registering user: {}", registrationDTO); - - User user = transientUser(registrationDTO); - fillRoles(user, registrationDTO); - userRepository.save(user); - - if (userInAnyRole(user, RoleService.Authority.STUDENT)) { - sureCallerInAnyRole(RoleService.Authority.ADMIN, RoleService.Authority.SECRETARY); - Student student = transientStudent(registrationDTO.getStudentData()); - student.setUser(user); - student = studentRepository.save(student); - log.info("New user is student: {}", student); - } else if (userInAnyRole(user, RoleService.Authority.TEACHER)) { - sureCallerInAnyRole(RoleService.Authority.ADMIN, RoleService.Authority.SECRETARY); - Teacher teacher = transientTeacher(registrationDTO.getTeacherData()); - teacher.setUser(user); - teacher = teacherRepository.save(teacher); - log.info("New user is teacher: {}", teacher); - } else if (userInAnyRole(user, RoleService.Authority.ADMIN)) { - sureCallerInAnyRole(RoleService.Authority.ADMIN); - log.info("New user is administrator"); - } else { - throw new UnsupportedOperationException("Role not supported: " + user.getAuthorities()); - } - } - - private Teacher transientTeacher(RegistrationDTO.TeacherRegistrationDTO teacherData) { - if (teacherData == null) - throw new NullPointerException("Teacher data is null"); - if (teacherData.getCuratingGroups() == null) - teacherData.setCuratingGroups(List.of()); - if (teacherData.getAdvisingStudents() == null) - teacherData.setAdvisingStudents(List.of()); - - Teacher teacher = new Teacher(); - - List groups = groupRepository.findAllById(teacherData.getCuratingGroups()); - if (groups.size() != teacherData.getCuratingGroups().size()) { - throw new NotFoundException(Teacher.class); - } - List students = studentRepository.findAllById(teacherData.getAdvisingStudents()); - if (students.size() != teacherData.getAdvisingStudents().size()) { - throw new NotFoundException(Student.class); - } - - teacher.setCuratingGroups(groups); - teacher.setAdvisingStudents(students); - return teacher; - } - - private User transientUser(RegistrationDTO registrationDTO) { - User user = new User(); - user.setLogin(registrationDTO.getLogin()); - user.setPassword(passwordEncoder.encode(registrationDTO.getPassword())); - user.setFullName(registrationDTO.getFullName()); - user.setEmail(registrationDTO.getEmail()); - user.setNumberPhone(registrationDTO.getNumberPhone()); - return user; - } - - private Student transientStudent(RegistrationDTO.StudentRegistrationDTO studentData) { - if (studentData == null) - throw new NullPointerException("Student data is null"); - - Student student = new Student(); - if (studentData.getGroupId() != null) { - student.setGroup(groupRepository.findByIdThrow(studentData.getGroupId())); - } - - return student; - } - - private void fillRoles(User user, RegistrationDTO registrationDTO) { - RoleService.Authority accountType = registrationDTO.getAccountType(); - Role role = roleService.getRoleByAuthority(accountType); - if (role == null) { - throw new IllegalArgumentException("Role not found for authority: " + accountType); - } - user.setRoles(List.of(role)); - } - - public boolean userInAnyRole(User user, RoleService.Authority... authority) { - if (user == null || authority == null) { - return false; - } - - List toCheckAuthorities = List.of(authority); - return user.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .map(RoleService.Authority::from) - .anyMatch(toCheckAuthorities::contains); - } - - public User getCallerUser() { - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if (!(principal instanceof User)) { - return null; - } - - return (User) principal; - } - - public Optional getCallerOptional() { - return Optional.ofNullable(getCallerUser()); - } - - public UserDTO getCallerUserDTO() { - return getCallerOptional().map(UserDTO::from).orElse(UserDTO.unauthenticated()); - } - - public boolean isCallerInRole(RoleService.Authority... authorities) { - return userInAnyRole(getCallerUser(), authorities); - } - - public void sureCallerInAnyRole(RoleService.Authority... authority) { - if (!isCallerInRole(authority)) { - throw new AccessDeniedException(); - } - } -} - diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/DefenceRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/DefenceRepository.java index b7348bb..5736206 100644 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/DefenceRepository.java +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/DefenceRepository.java @@ -1,4 +1,25 @@ package ru.mskobaro.tdms.integration.database; -public interface DefenceRepository { +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.mskobaro.tdms.business.entity.Defense; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.Optional; + +@Repository +public interface DefenceRepository extends JpaRepository { + default Defense findByIdThrow(Long id) { + return this.findById(id).orElseThrow(() -> new NotFoundException(Defense.class, id)); + } + + @Override + @Query("SELECT d from Defense d " + + "left join fetch d.bestWorks bw " + + "left join fetch d.commissionMembers cm " + + "where d.id = :id " + + "and bw.participant.deleted = false " + + "and cm.participant.deleted = false") + Optional findById(Long id); } diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/DiplomaTopicRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/DiplomaTopicRepository.java index fcfa333..c9a6f20 100644 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/DiplomaTopicRepository.java +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/DiplomaTopicRepository.java @@ -1,13 +1,29 @@ package ru.mskobaro.tdms.integration.database; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import ru.mskobaro.tdms.domain.entity.DiplomaTopic; -import ru.mskobaro.tdms.domain.exception.NotFoundException; +import ru.mskobaro.tdms.business.entity.DiplomaTopic; +import ru.mskobaro.tdms.business.entity.DirectionOfPreparation; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.List; +import java.util.Optional; @Repository -public interface DiplomaTopicRepository extends JpaRepository { - default DiplomaTopic findByIdThrow(Integer id) { +public interface DiplomaTopicRepository extends JpaRepository { + default DiplomaTopic findByIdThrow(Long id) { return this.findById(id).orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id)); } + + @Override + @Query("SELECT d FROM DiplomaTopic d WHERE d.id = :id AND d.teacher.participant.deleted = false") + Optional findById(Long id); + + @Query("SELECT d FROM DiplomaTopic d " + + "inner join d.directionOfPreparation dp " + + "inner join StudentData sd on sd.id = :studentIdwhere " + + "where dp = sd.group.directionOfPreparation") + List findAllForStudentId(Long studentId); } \ No newline at end of file diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/GroupRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/GroupRepository.java index bc6d4b7..6421637 100644 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/GroupRepository.java +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/GroupRepository.java @@ -1,9 +1,14 @@ package ru.mskobaro.tdms.integration.database; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import ru.mskobaro.tdms.domain.entity.Group; -import ru.mskobaro.tdms.domain.exception.NotFoundException; +import ru.mskobaro.tdms.business.entity.Group; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.List; +import java.util.Optional; @Repository public interface GroupRepository extends JpaRepository { @@ -11,5 +16,12 @@ public interface GroupRepository extends JpaRepository { return this.findById(id).orElseThrow(() -> new NotFoundException(Group.class, id)); } + @Override + @Query("SELECT g FROM Group g left join fetch g.students sd WHERE g.id = :id") + Optional findById(Long id); + boolean existsByName(String name); + + @Query("SELECT g FROM Group g left join fetch g.students sd WHERE sd IN :students") + Group findByStudentsContaining(List students); } diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/ParticipantRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/ParticipantRepository.java new file mode 100644 index 0000000..3fde395 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/ParticipantRepository.java @@ -0,0 +1,27 @@ +package ru.mskobaro.tdms.integration.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.mskobaro.tdms.business.entity.DiplomaTopic; +import ru.mskobaro.tdms.business.entity.Participant; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ParticipantRepository extends JpaRepository { + + default Participant findByIdThrow(Long id) { + return this.findById(id).orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id)); + } + + @Override + @Query("SELECT p FROM Participant p WHERE p.id = :id AND p.deleted = false") + Optional findById(Long id); + + @Override + @Query("SELECT p from Participant p where p.deleted = false") + List findAll(); +} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/PreparationDirectionRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/PreparationDirectionRepository.java new file mode 100644 index 0000000..98bc83e --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/PreparationDirectionRepository.java @@ -0,0 +1,13 @@ +package ru.mskobaro.tdms.integration.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.mskobaro.tdms.business.entity.DirectionOfPreparation; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +@Repository +public interface PreparationDirectionRepository extends JpaRepository { + default DirectionOfPreparation findByIdThrow(Long id) { + return this.findById(id).orElseThrow(() -> new NotFoundException(DirectionOfPreparation.class, id)); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/RoleRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/RoleRepository.java index 1020358..5950a89 100644 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/RoleRepository.java +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/RoleRepository.java @@ -2,7 +2,7 @@ package ru.mskobaro.tdms.integration.database; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import ru.mskobaro.tdms.domain.entity.Role; +import ru.mskobaro.tdms.business.entity.Role; @Repository public interface RoleRepository extends JpaRepository { diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/StudentDataRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/StudentDataRepository.java new file mode 100644 index 0000000..671fe19 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/StudentDataRepository.java @@ -0,0 +1,39 @@ +package ru.mskobaro.tdms.integration.database; + +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface StudentDataRepository extends JpaRepository { + default StudentData findByIdThrow(Long id) { + return this.findById(id).orElseThrow(() -> new NotFoundException(StudentData.class, id)); + } + + @EntityGraph(type = EntityGraph.EntityGraphType.LOAD, attributePaths = {"group.students"}) + @Query("SELECT s FROM StudentData s join fetch s.participant p WHERE p.id = :id AND p.deleted = false") + StudentData findStudentDataByParticipant_Id(Long id); + + boolean existsByParticipant_IdAndParticipant_DeletedFalse(Long id); + + @Override + @EntityGraph(type = EntityGraph.EntityGraphType.LOAD, attributePaths = {"participant.roles"}) + @Query("SELECT s FROM StudentData s join fetch s.participant p WHERE s.id in :ids AND p.deleted = false") + List findAllById(Iterable ids); + + @Query("SELECT s FROM StudentData s join fetch s.participant p WHERE s.group is null and p.deleted = false") + List findByGroupIsNull(); + + @Query("SELECT s FROM StudentData s join fetch s.participant p join fetch s.group g WHERE g.id = :id AND p.deleted = false") + List findAllByGroup_Id(Long id); + + @Override + @Query("SELECT s FROM StudentData s join fetch s.participant p WHERE s.id = :id AND p.deleted = false") + Optional findById(Long id); +} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/StudentRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/StudentRepository.java deleted file mode 100644 index 03a0cc1..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/StudentRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.mskobaro.tdms.integration.database; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import ru.mskobaro.tdms.domain.entity.Student; -import ru.mskobaro.tdms.domain.entity.User; -import ru.mskobaro.tdms.domain.exception.NotFoundException; - -@Repository -public interface StudentRepository extends JpaRepository { - default Student findByIdThrow(Long id) { - return this.findById(id).orElseThrow(() -> new NotFoundException(Student.class, id)); - } - - Student findByUser(User user); -} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/TaskRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/TaskRepository.java new file mode 100644 index 0000000..7d61ab8 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/TaskRepository.java @@ -0,0 +1,21 @@ +package ru.mskobaro.tdms.integration.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.mskobaro.tdms.business.entity.Task; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.List; + +@Repository +public interface TaskRepository extends JpaRepository { + default Task findByIdThrow(Long id) { + return findById(id).orElseThrow(() -> new NotFoundException(Task.class, id)); + } + + @Query(value = "SELECT t FROM task t " + + "WHERE t.type = :type " + + "and t.fields->>'makerParticipantId' = :id", nativeQuery = true) + List findDiplomaTopicAgreementTaskByMakerId(Long id, Task.Type type); +} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/TeacherDataRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/TeacherDataRepository.java new file mode 100644 index 0000000..d4f415f --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/TeacherDataRepository.java @@ -0,0 +1,30 @@ +package ru.mskobaro.tdms.integration.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.mskobaro.tdms.business.entity.TeacherData; +import ru.mskobaro.tdms.business.exception.NotFoundException; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TeacherDataRepository extends JpaRepository { + default TeacherData findByIdThrow(Long id) { + return this.findById(id).orElseThrow(() -> new NotFoundException(TeacherData.class, id)); + } + + @Override + @Query("SELECT t FROM TeacherData t WHERE t.id = :id AND t.participant.deleted = false") + Optional findById(Long id); + + boolean existsByParticipant_IdAndParticipant_DeletedFalse(Long id); + + @Query("SELECT t FROM TeacherData t WHERE t.participant.id = :id AND t.participant.deleted = false") + TeacherData findByParticipant_Id(Long id); + + @Override + @Query("select t from TeacherData t where t.participant.deleted = false") + List findAll(); +} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/TeacherRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/TeacherRepository.java deleted file mode 100644 index 27f212d..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/TeacherRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.mskobaro.tdms.integration.database; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import ru.mskobaro.tdms.domain.entity.Teacher; -import ru.mskobaro.tdms.domain.exception.NotFoundException; - -@Repository -public interface TeacherRepository extends JpaRepository { - default Teacher findByIdThrow(Long id) { - return this.findById(id).orElseThrow(() -> new NotFoundException(Teacher.class, id)); - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/integration/database/UserRepository.java b/server/src/main/java/ru/mskobaro/tdms/integration/database/UserRepository.java index 17009c8..03f466d 100644 --- a/server/src/main/java/ru/mskobaro/tdms/integration/database/UserRepository.java +++ b/server/src/main/java/ru/mskobaro/tdms/integration/database/UserRepository.java @@ -1,9 +1,10 @@ package ru.mskobaro.tdms.integration.database; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import ru.mskobaro.tdms.domain.entity.User; -import ru.mskobaro.tdms.domain.exception.NotFoundException; +import ru.mskobaro.tdms.business.entity.User; +import ru.mskobaro.tdms.business.exception.NotFoundException; import java.util.Optional; @@ -12,5 +13,11 @@ public interface UserRepository extends JpaRepository { default User findByIdThrow(Long id) { return this.findById(id).orElseThrow(() -> new NotFoundException(User.class, id)); } + + @Override + @Query("SELECT u from User u join fetch u.participant p where u.id = :id and p.deleted = false") + Optional findById(Long id); + + @Query("SELECT u from User u join fetch u.participant p where u.login = :login and p.deleted = false") Optional findUserByLogin(String login); } diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DefenceController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DefenceController.java new file mode 100644 index 0000000..0ef7a5c --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DefenceController.java @@ -0,0 +1,22 @@ +package ru.mskobaro.tdms.presentation.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.mskobaro.tdms.business.service.DefenceService; +import ru.mskobaro.tdms.presentation.controller.payload.DefenceDTO; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/defence") +public class DefenceController { + @Autowired + private DefenceService defenceService; + + @GetMapping("/all") + public List getAllDefences() { + return defenceService.getAllDefences(); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DiplomaTopicController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DiplomaTopicController.java index efb764a..7294156 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DiplomaTopicController.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/DiplomaTopicController.java @@ -1,13 +1,34 @@ package ru.mskobaro.tdms.presentation.controller; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import ru.mskobaro.tdms.business.service.DiplomaTopicService; +import ru.mskobaro.tdms.presentation.controller.payload.DiplomaTopicDTO; + +import java.util.List; @RestController @RequestMapping("/api/v1/diploma-topic/") @Validated public class DiplomaTopicController { + @Autowired + private DiplomaTopicService diplomaTopicService; + + @GetMapping("/all") + public List getAll() { + return diplomaTopicService.findAll().stream().map(DiplomaTopicDTO::from).toList(); + } + + @PostMapping("/save") + public void save(@RequestBody DiplomaTopicDTO diplomaTopicDTO) { + diplomaTopicService.save(diplomaTopicDTO); + } + + @GetMapping("/all-for-student") + public List getAllForStudent(@RequestParam Long studentId) { + return diplomaTopicService.findAllForStudent(studentId).stream().map(DiplomaTopicDTO::from).toList(); + } } diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/GroupController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/GroupController.java index 74dbb7b..2eeb7e0 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/GroupController.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/GroupController.java @@ -3,10 +3,8 @@ package ru.mskobaro.tdms.presentation.controller; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import ru.mskobaro.tdms.domain.service.GroupService; -import ru.mskobaro.tdms.presentation.payload.GroupCreateDTO; -import ru.mskobaro.tdms.presentation.payload.GroupDTO; -import ru.mskobaro.tdms.presentation.payload.GroupEditDTO; +import ru.mskobaro.tdms.business.service.GroupService; +import ru.mskobaro.tdms.presentation.controller.payload.GroupDTO; import java.util.Collection; @@ -21,13 +19,13 @@ public class GroupController { return groupService.getAllGroups(); } - @PostMapping("/create-group") - public void createGroup(@RequestBody @Valid GroupCreateDTO groupCreateDTO) { - groupService.createGroup(groupCreateDTO.getName()); + @PostMapping("/save") + public void save(@RequestBody @Valid GroupDTO groupDTO) { + groupService.save(groupDTO); } - @PostMapping("/edit-group") - public void editGroup(@RequestBody @Valid GroupEditDTO groupEditDTO) { - groupService.editGroup(groupEditDTO); + @PostMapping("/delete") + public void delete(@RequestParam(value = "id") Long groupId) { + groupService.deleteGroup(groupId); } } diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/ParticipantController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/ParticipantController.java new file mode 100644 index 0000000..7a949c1 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/ParticipantController.java @@ -0,0 +1,35 @@ +package ru.mskobaro.tdms.presentation.controller; + +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.mskobaro.tdms.business.service.ParticipantService; +import ru.mskobaro.tdms.presentation.controller.payload.IdDto; +import ru.mskobaro.tdms.presentation.controller.payload.ParticipantDTO; +import ru.mskobaro.tdms.presentation.controller.payload.ParticipantSaveDTO; + +import java.util.Collection; + +@RestController +@RequestMapping("/api/v1/participant") +public class ParticipantController { + @Autowired + private ParticipantService participantService; + + @GetMapping("/get-all-participants") + public Collection getAllParticipants() { + return participantService.getAllParticipants().stream() + .map(ParticipantDTO::fromEntity) + .toList(); + } + + @PostMapping("/save") + public void registerParticipant(@Valid @RequestBody ParticipantSaveDTO participantSaveDTO) { + participantService.saveParticipant(participantSaveDTO); + } + + @PostMapping("/delete") + public void deleteParticipant(@RequestBody IdDto id) { + participantService.deleteParticipant(id.getId()); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/PreparationDirectionController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/PreparationDirectionController.java new file mode 100644 index 0000000..e2c06be --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/PreparationDirectionController.java @@ -0,0 +1,26 @@ +package ru.mskobaro.tdms.presentation.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.mskobaro.tdms.business.service.PreparationDirectionService; +import ru.mskobaro.tdms.presentation.controller.payload.PreparationDirectionDTO; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/prep-direction") +public class PreparationDirectionController { + @Autowired + private PreparationDirectionService preparationDirectionService; + + @GetMapping("/get-all") + public List getAll() { + return preparationDirectionService.getAll().stream().map(PreparationDirectionDTO::from).collect(Collectors.toList()); + } + + @PostMapping("save") + public void save(@RequestBody PreparationDirectionDTO preparationDirectionDTO) { + preparationDirectionService.save(preparationDirectionDTO); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/StudentController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/StudentController.java index fda517c..cd436f2 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/StudentController.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/StudentController.java @@ -5,22 +5,29 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import ru.mskobaro.tdms.domain.service.StudentService; -import ru.mskobaro.tdms.presentation.payload.StudentDTO; +import ru.mskobaro.tdms.business.entity.StudentData; +import ru.mskobaro.tdms.business.service.StudentDataService; +import ru.mskobaro.tdms.presentation.controller.payload.GroupDTO; +import ru.mskobaro.tdms.presentation.controller.payload.StudentDataDTO; + +import java.util.Collection; @RestController @RequestMapping("/api/v1/student/") public class StudentController { @Autowired - private StudentService studentService; + private StudentDataService studentDataService; - @GetMapping("/current") - public StudentDTO getCurrentStudent() { - return studentService.getCallerStudentDtoThrow(); + @GetMapping(value = "/by-partic-id") + public StudentDataDTO getCurrentStudent(@RequestParam(value = "id") Long particId) { + StudentData studentData = studentDataService.getStudentByParticIdThrow(particId); + return StudentDataDTO.from(studentData, true); } - @GetMapping("/by-user-id") - public StudentDTO getStudentByUserId(@RequestParam Long id) { - return studentService.getStudentByUserIdThrow(id); + @GetMapping(value = "all-without-group") + public Collection getAllStudentsWithoutGroup() { + return studentDataService.getAllStudentsWithoutGroup().stream() + .map(sd -> StudentDataDTO.from(sd, true)) + .toList(); } } diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/SysInfoController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/SysInfoController.java index 9e854fa..eca4bf3 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/SysInfoController.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/SysInfoController.java @@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import ru.mskobaro.tdms.domain.service.SysInfoService; +import ru.mskobaro.tdms.business.service.SysInfoService; @RestController @RequestMapping("/api/v1/sysinfo") diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/TaskController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/TaskController.java new file mode 100644 index 0000000..8266edf --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/TaskController.java @@ -0,0 +1,34 @@ +package ru.mskobaro.tdms.presentation.controller; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.mskobaro.tdms.business.service.TaskService; +import ru.mskobaro.tdms.presentation.controller.payload.TaskDto; + +@RestController +@RequestMapping("/api/v1/task") +public class TaskController { + @Autowired + private TaskService taskService; + + @GetMapping("/diploma-agreement-maker") + public TaskDto diplomaTopicAgreementMaker() { + return TaskDto.from(taskService.findDiplomaTopicAgreementTaskCallerMaker()); + } + + @PostMapping("/create-topic-agreement") + public void createDiplomaTopicAgreement(@RequestBody DiplomaTopicAgreementDTO diplomaTopicAgreementDTO) { + taskService.createDiplomaAgreementTask(diplomaTopicAgreementDTO); + } + + @NoArgsConstructor + @Getter + @Setter + public static class DiplomaTopicAgreementDTO { + private String diplomaTopicName; + private Long diplomaTopicId; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/TeacherDataController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/TeacherDataController.java new file mode 100644 index 0000000..5e712e3 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/TeacherDataController.java @@ -0,0 +1,28 @@ +package ru.mskobaro.tdms.presentation.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.mskobaro.tdms.business.service.TeacherDataService; +import ru.mskobaro.tdms.presentation.controller.payload.TeacherDataDTO; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/teacher-data") +public class TeacherDataController { + @Autowired + private TeacherDataService teacherDataService; + + @GetMapping("/get-all") + public List getAllTeacherData() { + return teacherDataService.findAll().stream().map(TeacherDataDTO::from).toList(); + } + + @GetMapping("/by-partic-id") + public TeacherDataDTO getTeacherDataByParticipantId(@RequestParam Long id) { + return TeacherDataDTO.from(teacherDataService.getTeacherDataByParticipantId(id)); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/UserController.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/UserController.java index e32b7c9..b02b0be 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/UserController.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/UserController.java @@ -3,14 +3,14 @@ package ru.mskobaro.tdms.presentation.controller; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import ru.mskobaro.tdms.domain.service.AuthenticationService; -import ru.mskobaro.tdms.domain.service.UserService; -import ru.mskobaro.tdms.presentation.payload.LoginDTO; -import ru.mskobaro.tdms.presentation.payload.RegistrationDTO; -import ru.mskobaro.tdms.presentation.payload.UserDTO; - -import java.util.List; +import ru.mskobaro.tdms.business.entity.User; +import ru.mskobaro.tdms.business.service.AuthenticationService; +import ru.mskobaro.tdms.business.service.UserService; +import ru.mskobaro.tdms.presentation.controller.payload.LoginDTO; +import ru.mskobaro.tdms.presentation.controller.payload.UserDTO; @RestController @RequestMapping("/api/v1/user") @@ -22,8 +22,11 @@ public class UserController { private UserService userService; @GetMapping("/current") - public UserDTO getCurrentUser() { - return userService.getCallerUserDTO(); + public ResponseEntity getCurrentUser() { + User user = userService.getCallerUser(); + return user == null + ? ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() + : ResponseEntity.ok(UserDTO.fromEntity(user)); } @PostMapping("/logout") @@ -35,14 +38,4 @@ public class UserController { public void login(@RequestBody @Valid LoginDTO loginDTO) { authenticationService.login(loginDTO.getLogin(), loginDTO.getPassword()); } - - @PostMapping("/register") - public void register(@RequestBody @Valid RegistrationDTO registrationDTO) { - userService.registerUser(registrationDTO); - } - - @GetMapping("/get-all") - public List getAllUsers() { - return userService.getAllUsers(); - } } diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/CommissionMemberDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/CommissionMemberDTO.java new file mode 100644 index 0000000..2f5b95a --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/CommissionMemberDTO.java @@ -0,0 +1,31 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.mskobaro.tdms.business.entity.CommissionMemberData; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +public class CommissionMemberDTO { + private Long id; + private ParticipantDTO participant; + private String workPlace; + private String workPosition; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static CommissionMemberDTO from(CommissionMemberData commissionMemberData) { + CommissionMemberDTO dto = new CommissionMemberDTO(); + dto.setId(commissionMemberData.getId()); + dto.setParticipant(ParticipantDTO.fromEntity(commissionMemberData.getParticipant())); + dto.setWorkPlace(commissionMemberData.getWorkPlace()); + dto.setWorkPosition(commissionMemberData.getWorkPosition()); + dto.setCreatedAt(commissionMemberData.getAuditInfo().getCreatedAt()); + dto.setUpdatedAt(commissionMemberData.getAuditInfo().getUpdatedAt()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/DefenceDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/DefenceDTO.java new file mode 100644 index 0000000..767267d --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/DefenceDTO.java @@ -0,0 +1,39 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.CollectionUtils; +import ru.mskobaro.tdms.business.entity.Defense; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Setter +public class DefenceDTO { + private Long id; + private List commissionMembers; + private List groups; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static DefenceDTO from(Defense defense) { + DefenceDTO dto = new DefenceDTO(); + dto.setId(defense.getId()); + + if (CollectionUtils.isNotEmpty(defense.getCommissionMembers())) { + dto.setCommissionMembers( + defense.getCommissionMembers().stream() + .map(CommissionMemberDTO::from) + .collect(Collectors.toList()) + ); + } + + dto.setGroups(defense.getGroups().stream().map(g -> GroupDTO.from(g, true)).toList()); + dto.setCreatedAt(defense.getAuditInfo().getCreatedAt()); + dto.setUpdatedAt(defense.getAuditInfo().getUpdatedAt()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/DiplomaTopicDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/DiplomaTopicDTO.java new file mode 100644 index 0000000..33f72a6 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/DiplomaTopicDTO.java @@ -0,0 +1,37 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import ru.mskobaro.tdms.business.entity.DiplomaTopic; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@ToString +public class DiplomaTopicDTO { + private Long id; + private String name; + private TeacherDataDTO teacher; + private PreparationDirectionDTO preparationDirection; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static DiplomaTopicDTO from(DiplomaTopic diplomaTopic) { + DiplomaTopicDTO dto = new DiplomaTopicDTO(); + dto.setId(diplomaTopic.getId()); + dto.setName(diplomaTopic.getName()); + if (diplomaTopic.getTeacher() != null) { + dto.setTeacher(TeacherDataDTO.from(diplomaTopic.getTeacher())); + } + if (diplomaTopic.getDirectionOfPreparation() != null) { + dto.setPreparationDirection(PreparationDirectionDTO.from(diplomaTopic.getDirectionOfPreparation())); + } + dto.setCreatedAt(diplomaTopic.getAuditInfo().getCreatedAt()); + dto.setUpdatedAt(diplomaTopic.getAuditInfo().getUpdatedAt()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/ErrorDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ErrorDTO.java similarity index 90% rename from server/src/main/java/ru/mskobaro/tdms/presentation/payload/ErrorDTO.java rename to server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ErrorDTO.java index 895fe7b..d099897 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/ErrorDTO.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ErrorDTO.java @@ -1,4 +1,4 @@ -package ru.mskobaro.tdms.presentation.payload; +package ru.mskobaro.tdms.presentation.controller.payload; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/GroupDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/GroupDTO.java new file mode 100644 index 0000000..b8fcf21 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/GroupDTO.java @@ -0,0 +1,47 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import ru.mskobaro.tdms.business.entity.Group; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +@ToString +public class GroupDTO { + private Long id; + @NotEmpty(message = "Имя группы не может быть пустым") + @Size(min = 3, max = 50, message = "Имя группы должно быть от 3 до 50 символов") + @Pattern(regexp = "^[а-яА-ЯёЁ0-9_-]*$", message = "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры") + private String name; + private List students; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private PreparationDirectionDTO preparationDirection; + + public static GroupDTO from(Group group, boolean includeStudents) { + GroupDTO dto = new GroupDTO(); + dto.setId(group.getId()); + dto.setName(group.getName()); + if (includeStudents && group.getStudents() != null) { + dto.setStudents( + group.getStudents() + .stream() + .filter(sd -> !sd.getParticipant().isDeleted()) + .map(sd -> StudentDataDTO.from(sd, false)) + .toList()); + } + if (group.getDirectionOfPreparation() != null) { + dto.setPreparationDirection(PreparationDirectionDTO.from(group.getDirectionOfPreparation())); + } + dto.setCreatedAt(group.getAuditInfo().getCreatedAt()); + dto.setUpdatedAt(group.getAuditInfo().getUpdatedAt()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/IdDto.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/IdDto.java new file mode 100644 index 0000000..98fd6cc --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/IdDto.java @@ -0,0 +1,14 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@NoArgsConstructor +@Getter +@Setter +@ToString +public class IdDto { + private Long id; +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/LoginDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/LoginDTO.java similarity index 94% rename from server/src/main/java/ru/mskobaro/tdms/presentation/payload/LoginDTO.java rename to server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/LoginDTO.java index fefbc44..1fcf3eb 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/LoginDTO.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/LoginDTO.java @@ -1,4 +1,4 @@ -package ru.mskobaro.tdms.presentation.payload; +package ru.mskobaro.tdms.presentation.controller.payload; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ParticipantDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ParticipantDTO.java new file mode 100644 index 0000000..fc7f4e9 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ParticipantDTO.java @@ -0,0 +1,48 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import ru.mskobaro.tdms.business.entity.Participant; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.List; + +@Getter +@Setter +@ToString +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class ParticipantDTO { + private Long id; + private String firstName; + private String lastName; + private String middleName; + private String email; + private String numberPhone; + private UserDTO user; + private List roles; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static ParticipantDTO fromEntity(Participant participant) { + ParticipantDTO participantDTO = new ParticipantDTO(); + participantDTO.setId(participant.getId()); + participantDTO.setFirstName(participant.getFirstName()); + participantDTO.setLastName(participant.getLastName()); + participantDTO.setMiddleName(participant.getMiddleName()); + participantDTO.setEmail(participant.getEmail()); + participantDTO.setNumberPhone(participant.getNumberPhone()); + participantDTO.setRoles(RoleDTO.from(participant)); + participantDTO.setCreatedAt(participant.getAuditInfo().getCreatedAt()); + participantDTO.setUpdatedAt(participant.getAuditInfo().getUpdatedAt()); + + if (participant.getUser() != null) { + participantDTO.setUser(UserDTO.fromEntity(participant.getUser(), participantDTO)); + } + + return participantDTO; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ParticipantSaveDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ParticipantSaveDTO.java new file mode 100644 index 0000000..a9b5fbb --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/ParticipantSaveDTO.java @@ -0,0 +1,64 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import lombok.Getter; +import lombok.ToString; +import ru.mskobaro.tdms.business.service.RoleService.Authority; + +import java.util.List; + +@Getter +@ToString +public class ParticipantSaveDTO { + private Long id; + @NotEmpty(message = "Имя не может быть пустым") + @Pattern(regexp = "^[a-zA-Zа-яА-ЯёЁ\\s-]+$", message = "Имя должно содержать только буквы английского или русского алфавита, пробелы и дефис") + private String firstName; + @NotEmpty(message = "Фамилия не может быть пустой") + @Pattern(regexp = "^[a-zA-Zа-яА-ЯёЁ\\s-]+$", message = "Фамилия должна содержать только буквы английского или русского алфавита, пробелы и дефис") + private String lastName; + private String middleName; + @NotNull(message = "Почта не может быть пустой") + @Email(message = "Почта должна быть валидным адресом электронной почты") + private String email; + @NotNull(message = "Номер телефона не может быть пустым") + @Pattern(regexp = "^\\+[1-9]\\d{6,14}$", message = "Номер телефона должен начинаться с '+' и содержать от 7 до 15 цифр") + private String numberPhone; + private List authorities; + + @Valid + private UserRegistrationDTO userData; + @Valid + private StudentRegistrationDTO studentData; + @Valid + private TeacherRegistrationDTO teacherData; + + @Getter + @ToString + public static class UserRegistrationDTO { + @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Логин должен содержать только латинские буквы, цифры и знак подчеркивания") + @Size(min = 5, message = "Логин должен содержать минимум 5 символов") + @Size(max = 32, message = "Логин должен содержать максимум 32 символов") + private String login; + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "Пароль должен содержать хотя бы одну цифру, одну заглавную и одну строчную букву, минимум 8 символов") + private String password; + } + + @Getter + @ToString + public static class StudentRegistrationDTO { + private Long groupId; + private Long curatorId; + private Long diplomaTopicId; + } + + @Getter + @ToString + public static class TeacherRegistrationDTO { + private List curatingGroups; + private List advisingStudents; + private String degree; + } + +} \ No newline at end of file diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/PreparationDirectionDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/PreparationDirectionDTO.java new file mode 100644 index 0000000..7568ed4 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/PreparationDirectionDTO.java @@ -0,0 +1,29 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.mskobaro.tdms.business.entity.DirectionOfPreparation; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@Getter +@Setter +public class PreparationDirectionDTO { + private Long id; + private String name; + private String code; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static PreparationDirectionDTO from(DirectionOfPreparation preparationDirection) { + PreparationDirectionDTO dto = new PreparationDirectionDTO(); + dto.setId(preparationDirection.getId()); + dto.setName(preparationDirection.getName()); + dto.setCode(preparationDirection.getCode()); + dto.setCreatedAt(preparationDirection.getAuditInfo().getCreatedAt()); + dto.setUpdatedAt(preparationDirection.getAuditInfo().getUpdatedAt()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/RoleDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/RoleDTO.java new file mode 100644 index 0000000..8192de3 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/RoleDTO.java @@ -0,0 +1,19 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + + +import ru.mskobaro.tdms.business.entity.Participant; +import ru.mskobaro.tdms.business.entity.Role; + +import java.util.List; + + +public record RoleDTO(String name, String code) { + + public static RoleDTO from(Role role) { + return new RoleDTO(role.getName(), role.getAuthority()); + } + + public static List from(Participant participant) { + return participant.getRoles().stream().map(RoleDTO::from).toList(); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/StudentDataDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/StudentDataDTO.java new file mode 100644 index 0000000..bdd2173 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/StudentDataDTO.java @@ -0,0 +1,31 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.Setter; +import ru.mskobaro.tdms.business.entity.StudentData; + +@Setter +@Getter +public class StudentDataDTO { + private Long id; + private GroupDTO group; + private ParticipantDTO participant; + private TeacherDataDTO curator; + private DiplomaTopicDTO diplomaTopic; + + public static StudentDataDTO from(StudentData studentData, boolean includeGroup) { + StudentDataDTO dto = new StudentDataDTO(); + dto.setId(studentData.getId()); + if (includeGroup && studentData.getGroup() != null) { + dto.setGroup(GroupDTO.from(studentData.getGroup(), false)); + } + dto.setParticipant(ParticipantDTO.fromEntity(studentData.getParticipant())); + if (studentData.getCurator() != null) { + dto.setCurator(TeacherDataDTO.from(studentData.getCurator())); + } + if (studentData.getDiplomaTopic() != null) { + dto.setDiplomaTopic(DiplomaTopicDTO.from(studentData.getDiplomaTopic())); + } + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/TaskDto.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/TaskDto.java new file mode 100644 index 0000000..feba773 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/TaskDto.java @@ -0,0 +1,33 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.mskobaro.tdms.business.entity.Task; +import ru.mskobaro.tdms.business.taskfields.TaskFields; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@Getter +@Setter +public class TaskDto { + private Long id; + private Task.Type type; + private Task.Status status; + private TaskFields fields; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + + public static TaskDto from(Task task) { + TaskDto dto = new TaskDto(); + dto.setId(task.getId()); + dto.setType(task.getType()); + dto.setStatus(task.getStatus()); + dto.setFields(task.getFields()); + dto.setCreatedAt(task.getAuditInfo().getCreatedAt()); + dto.setUpdatedAt(task.getAuditInfo().getUpdatedAt()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/TeacherDataDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/TeacherDataDTO.java new file mode 100644 index 0000000..f79ebe1 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/TeacherDataDTO.java @@ -0,0 +1,27 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.mskobaro.tdms.business.entity.TeacherData; + +@Getter +@Setter +@NoArgsConstructor +public class TeacherDataDTO { + private Long id; + private ParticipantDTO participant; + private String degree; + private String createdAt; + private String updatedAt; + + public static TeacherDataDTO from(TeacherData teacherData) { + TeacherDataDTO dto = new TeacherDataDTO(); + dto.setId(teacherData.getId()); + dto.setParticipant(ParticipantDTO.fromEntity(teacherData.getParticipant())); + dto.setDegree(teacherData.getDegree()); + dto.setCreatedAt(teacherData.getAuditInfo().getCreatedAt().toString()); + dto.setUpdatedAt(teacherData.getAuditInfo().getUpdatedAt().toString()); + return dto; + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/UserDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/UserDTO.java new file mode 100644 index 0000000..59df214 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/controller/payload/UserDTO.java @@ -0,0 +1,41 @@ +package ru.mskobaro.tdms.presentation.controller.payload; + + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import lombok.Builder; +import ru.mskobaro.tdms.business.entity.User; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + + +@Builder +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public record UserDTO( + Long id, + String login, + ParticipantDTO participant, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static UserDTO fromEntity(User user) { + return UserDTO.builder() + .id(user.getId()) + .login(user.getLogin()) + .participant(ParticipantDTO.fromEntity(user.getParticipant())) + .createdAt(user.getAuditInfo().getCreatedAt()) + .updatedAt(user.getAuditInfo().getUpdatedAt()) + .build(); + } + + public static UserDTO fromEntity(User user, ParticipantDTO participant) { + return UserDTO.builder() + .id(user.getId()) + .login(user.getLogin()) + .participant(participant) + .createdAt(user.getAuditInfo().getCreatedAt()) + .updatedAt(user.getAuditInfo().getUpdatedAt()) + .build(); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/exception/ApplicationExceptionHandler.java b/server/src/main/java/ru/mskobaro/tdms/presentation/exception/ApplicationExceptionHandler.java index f672711..84cbeb3 100644 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/exception/ApplicationExceptionHandler.java +++ b/server/src/main/java/ru/mskobaro/tdms/presentation/exception/ApplicationExceptionHandler.java @@ -9,16 +9,16 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.resource.NoResourceFoundException; -import ru.mskobaro.tdms.domain.exception.AccessDeniedException; -import ru.mskobaro.tdms.domain.exception.BusinessException; -import ru.mskobaro.tdms.presentation.payload.ErrorDTO; +import ru.mskobaro.tdms.business.exception.AccessDeniedException; +import ru.mskobaro.tdms.business.exception.BusinessException; +import ru.mskobaro.tdms.presentation.controller.payload.ErrorDTO; import java.util.UUID; import java.util.stream.Collectors; import static org.springframework.http.HttpStatus.*; import static org.springframework.http.HttpStatus.NOT_FOUND; -import static ru.mskobaro.tdms.presentation.payload.ErrorDTO.ErrorCode.*; +import static ru.mskobaro.tdms.presentation.controller.payload.ErrorDTO.ErrorCode.*; @RestControllerAdvice @Slf4j diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupCreateDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupCreateDTO.java deleted file mode 100644 index 2591364..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupCreateDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.Getter; - -@Getter -public class GroupCreateDTO { - @NotEmpty(message = "Имя группы не может быть пустым") - @Size(min = 3, max = 50, message = "Имя группы должно быть от 3 до 50 символов") - @Pattern(regexp = "^[а-яА-ЯёЁ0-9_-]*$", message = "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры") - private String name; -} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupDTO.java deleted file mode 100644 index ee07feb..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -public class GroupDTO { - private Long id; - private String name; - private String curatorName; - private Boolean iAmCurator; -} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupEditDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupEditDTO.java deleted file mode 100644 index 1fc54f0..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/GroupEditDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - -import jakarta.validation.constraints.*; -import lombok.Getter; -import lombok.ToString; - -@Getter -@ToString -public class GroupEditDTO { - @NotNull(message = "Идентификатор группы не может быть пустым") - @Min(value = 1, message = "Идентификатор группы должен быть больше 0") - private Long id; - @NotEmpty(message = "Имя группы не может быть пустым") - @Size(min = 3, max = 50, message = "Имя группы должно быть от 3 до 50 символов") - @Pattern(regexp = "^[а-яА-ЯёЁ0-9_-]*$", message = "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры") - private String name; -} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/RegistrationDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/RegistrationDTO.java deleted file mode 100644 index d47a973..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/RegistrationDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - -import jakarta.validation.constraints.*; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.hibernate.validator.constraints.Length; -import ru.mskobaro.tdms.domain.service.RoleService.Authority; - -import java.util.List; - -@Getter -@ToString -public class RegistrationDTO { - @NotEmpty(message = "Логин не может быть пустым") - @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Логин должен содержать только латинские буквы, цифры и знак подчеркивания") - @Size(min = 5, message = "Логин должен содержать минимум 5 символов") - @Size(max = 32, message = "Логин должен содержать максимум 32 символов") - private String login; - @NotEmpty(message = "Пароль не может быть пустым") - @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "Пароль должен содержать хотя бы одну цифру, одну заглавную и одну строчную букву, минимум 8 символов") - private String password; - @NotEmpty(message = "Имя не может быть пустым") - @Length(min = 3, message = "Имя должно содержать минимум 3 символа") - @Pattern(regexp = "^[a-zA-Zа-яА-ЯёЁ\\s]+$", message = "Имя должно содержать только буквы английского или русского алфавита и пробелы") - private String fullName; - @NotNull(message = "Почта не может быть пустой") - @Email(message = "Почта должна быть валидным адресом электронной почты") - private String email; - @NotNull(message = "Номер телефона не может быть пустым") - @Pattern(regexp = "^\\+[1-9]\\d{6,14}$", message = "Номер телефона должен начинаться с '+' и содержать от 7 до 15 цифр") - private String numberPhone; - @NotNull(message = "Тип аккаунта не может быть пустым") - private Authority accountType; - - private StudentRegistrationDTO studentData; - private TeacherRegistrationDTO teacherData; - - @Getter - @ToString - public static class StudentRegistrationDTO { - @NotNull(message = "Группа не может быть пустой") - private Long groupId; - } - - @Getter - @Setter - @ToString - public static class TeacherRegistrationDTO { - private List curatingGroups; - private List advisingStudents; - } - -} \ No newline at end of file diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/RoleDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/RoleDTO.java deleted file mode 100644 index aacf797..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/RoleDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - - -import ru.mskobaro.tdms.domain.entity.Role; -import ru.mskobaro.tdms.domain.entity.User; - -import java.util.List; - - -public record RoleDTO(String name, String authority) { - - public static RoleDTO from(Role role) { - return new RoleDTO(role.getName(), role.getAuthority()); - } - - public static List from(User user) { - return user.getRoles().stream().map(RoleDTO::from).toList(); - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/StudentDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/StudentDTO.java deleted file mode 100644 index 6cc4d74..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/StudentDTO.java +++ /dev/null @@ -1,47 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - - -import lombok.Data; -import ru.mskobaro.tdms.domain.entity.Student; - - -@Data -public class StudentDTO { - private Long id; - private Boolean form; - private Integer protectionOrder; - private String magistracy; - private Boolean digitalFormatPresent; - private Integer markComment; - private Integer markPractice; - private String predefenceComment; - private String normalControl; - private Integer antiPlagiarism; - private String note; - private Boolean recordBookReturned; - private String work; - private UserDTO user; - private String diplomaTopic; - private UserDTO mentorUser; - private GroupDTO group; - - public static StudentDTO from(Student student) { - StudentDTO studentDTO = new StudentDTO(); - studentDTO.setId(student.getId()); - studentDTO.setForm(student.getForm()); - studentDTO.setProtectionOrder(student.getProtectionOrder()); - studentDTO.setMagistracy(student.getMagistracy()); - studentDTO.setDigitalFormatPresent(student.getDigitalFormatPresent()); - studentDTO.setMarkComment(student.getMarkComment()); - studentDTO.setMarkPractice(student.getMarkPractice()); - studentDTO.setPredefenceComment(student.getPredefenceComment()); - studentDTO.setNormalControl(student.getNormalControl()); - studentDTO.setAntiPlagiarism(student.getAntiPlagiarism()); - studentDTO.setNote(student.getNote()); - studentDTO.setRecordBookReturned(student.getRecordBookReturned()); - studentDTO.setWork(student.getWork()); - studentDTO.setUser(UserDTO.from(student.getUser())); - studentDTO.setDiplomaTopic(student.getDiplomaTopic().getName()); - return studentDTO; - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/UserDTO.java b/server/src/main/java/ru/mskobaro/tdms/presentation/payload/UserDTO.java deleted file mode 100644 index eb2cbc6..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/presentation/payload/UserDTO.java +++ /dev/null @@ -1,45 +0,0 @@ -package ru.mskobaro.tdms.presentation.payload; - - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Builder; -import org.springframework.security.core.GrantedAuthority; -import ru.mskobaro.tdms.domain.entity.User; - -import java.time.ZonedDateTime; -import java.util.List; - - -@Builder -@JsonInclude(JsonInclude.Include.NON_ABSENT) -public record UserDTO( - Long id, - boolean authenticated, - String login, - String fullName, - String email, - String phone, - ZonedDateTime createdAt, - ZonedDateTime updatedAt, - List authorities -) { - public static UserDTO unauthenticated() { - return UserDTO.builder() - .authenticated(false) - .build(); - } - - public static UserDTO from(User user) { - return UserDTO.builder() - .id(user.getId()) - .authenticated(true) - .login(user.getLogin()) - .fullName(user.getFullName()) - .email(user.getEmail()) - .phone(user.getNumberPhone()) - .createdAt(user.getCreatedAt()) - .updatedAt(user.getUpdatedAt()) - .authorities(user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()) - .build(); - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/system/config/SecurityConfig.java b/server/src/main/java/ru/mskobaro/tdms/system/config/SecurityConfig.java index 074f9ff..78d945e 100644 --- a/server/src/main/java/ru/mskobaro/tdms/system/config/SecurityConfig.java +++ b/server/src/main/java/ru/mskobaro/tdms/system/config/SecurityConfig.java @@ -11,31 +11,28 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.access.ExceptionTranslationFilter; -import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.context.SecurityContextHolderFilter; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; -import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import ru.mskobaro.tdms.system.web.DevAuthenticationRequestFilter; import ru.mskobaro.tdms.system.web.LoggingRequestFilter; import java.time.Duration; import java.util.List; -import static ru.mskobaro.tdms.domain.service.RoleService.Authority.*; +import static ru.mskobaro.tdms.business.service.RoleService.Authority.ADMIN; +import static ru.mskobaro.tdms.business.service.RoleService.Authority.SECRETARY; @Slf4j @@ -45,27 +42,31 @@ public class SecurityConfig { private Environment environment; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager, CorsConfigurationSource cors) throws Exception { - if (environment.matchesProfiles("dev")) { - httpSecurity.addFilterAfter(new DevAuthenticationRequestFilter(), SecurityContextHolderFilter.class); - } - + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, + AuthenticationManager authenticationManager, + CorsConfigurationSource cors + ) throws Exception { return httpSecurity - .addFilterAfter(new LoggingRequestFilter(), AnonymousAuthenticationFilter.class) - .authorizeHttpRequests(this::configureHttpAuthorization) - .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) - .cors(a -> a.configurationSource(cors)) - .authenticationManager(authenticationManager) - .sessionManagement(cfg -> { - cfg.sessionCreationPolicy(SessionCreationPolicy.NEVER); - cfg.maximumSessions(1); - }) - .build(); + .addFilterAfter(new LoggingRequestFilter(), SecurityContextHolderFilter.class) + .authorizeHttpRequests(this::configureHttpAuthorization) + // .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) + .csrf(AbstractHttpConfigurer::disable) + .cors(a -> a.configurationSource(cors)) + .authenticationManager(authenticationManager) + .sessionManagement(cfg -> { + cfg.sessionCreationPolicy(SessionCreationPolicy.NEVER); + cfg.maximumSessions(1); + }) + .build(); } @Bean @Primary - public CorsConfigurationSource corsConfigurationProd(@Value("${application.domain}") String domain, @Value("${application.port}") String port, @Value("${application.protocol}") String protocol) { + public CorsConfigurationSource corsConfiguration( + @Value("${application.domain}") String domain, + @Value("${application.port}") String port, + @Value("${application.protocol}") String protocol + ) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.OPTIONS.name())); corsConfiguration.setAllowedHeaders(List.of("Authorization", "Content-Type")); @@ -78,14 +79,21 @@ public class SecurityConfig { } log.info("CORS configuration: [headers: {}, methods: {}, origins: {}, credentials: {}, maxAge: {}]", - corsConfiguration.getAllowedHeaders(), corsConfiguration.getAllowedMethods(), corsConfiguration.getAllowedOrigins(), - corsConfiguration.getAllowCredentials(), corsConfiguration.getMaxAge()); + corsConfiguration.getAllowedHeaders(), corsConfiguration.getAllowedMethods(), corsConfiguration.getAllowedOrigins(), + corsConfiguration.getAllowCredentials(), corsConfiguration.getMaxAge()); return request -> corsConfiguration; } @Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) { - return new ProviderManager(authenticationProvider(userDetailsService)); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder()); + provider.setUserDetailsService(userDetailsService); + return new ProviderManager(provider); + } + + @Bean + public SessionRegistry sessionRegistry() { + return new SessionRegistryImpl(); } @Bean @@ -93,32 +101,42 @@ public class SecurityConfig { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } - private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry httpAuthorization) { - /* SysInfoController */ + private void configureHttpAuthorization( + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry httpAuthorization + ) { httpAuthorization.requestMatchers("/api/v1/sysinfo/**").permitAll(); - /* UserController */ + httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated(); - httpAuthorization.requestMatchers("/api/v1/user/login").anonymous(); + httpAuthorization.requestMatchers("/api/v1/user/login").permitAll(); httpAuthorization.requestMatchers("/api/v1/user/current").permitAll(); - httpAuthorization.requestMatchers("/api/v1/user/get-all").hasAuthority(ADMIN.getAuthority()); - httpAuthorization.requestMatchers("/api/v1/user/register").hasAuthority(ADMIN.getAuthority()); - httpAuthorization.requestMatchers("/api/v1/user/validate-registration").hasAuthority(ADMIN.getAuthority()); - /* StudentController */ - httpAuthorization.requestMatchers("/api/v1/student/current").permitAll(); - httpAuthorization.requestMatchers("api/v1/student/by-user-id").hasAnyAuthority( - SECRETARY.getAuthority(), ADMIN.getAuthority(), STUDENT.getAuthority(), TEACHER.getAuthority()); - /* GroupController */ + + httpAuthorization.requestMatchers("/api/v1/participant/get-all-participants").permitAll(); + httpAuthorization.requestMatchers("/api/v1/participant/get-possible-curators").permitAll(); + // Сложная логика авторизации, так что проверяем явно в ParticipantService + httpAuthorization.requestMatchers("/api/v1/participant/save").authenticated(); + httpAuthorization.requestMatchers("/api/v1/participant/delete").hasAuthority(ADMIN.getAuthority()); + httpAuthorization.requestMatchers("/api/v1/group/get-all-groups").permitAll(); - httpAuthorization.requestMatchers("/api/v1/group/create-group").hasAuthority(ADMIN.getAuthority()); - /* deny all other api requests */ + httpAuthorization.requestMatchers("/api/v1/group/save").hasAnyAuthority(ADMIN.getAuthority(), SECRETARY.getAuthority()); + httpAuthorization.requestMatchers("/api/v1/group/delete").hasAnyAuthority(ADMIN.getAuthority()); + + httpAuthorization.requestMatchers("/api/v1/student/by-partic-id").permitAll(); + httpAuthorization.requestMatchers("/api/v1/student/all-without-group").permitAll(); + + httpAuthorization.requestMatchers("/api/v1/teacher-data/get-all").permitAll(); + httpAuthorization.requestMatchers("/api/v1/teacher-data/by-partic-id").permitAll(); + + httpAuthorization.requestMatchers("/api/v1/prep-direction/get-all").permitAll(); + httpAuthorization.requestMatchers("/api/v1/prep-direction/save").hasAnyAuthority(ADMIN.getAuthority(), SECRETARY.getAuthority()); + + httpAuthorization.requestMatchers("/api/v1/defence/all").permitAll(); + + httpAuthorization.requestMatchers("/api/v1/diploma-topic/all").permitAll(); + httpAuthorization.requestMatchers("/api/v1/diploma-topic/all-for-student").permitAll(); + httpAuthorization.requestMatchers("/api/v1/diploma-topic/save").hasAnyAuthority(ADMIN.getAuthority(), SECRETARY.getAuthority()); + httpAuthorization.requestMatchers("/api/**").denyAll(); - /* since api already blocked, all other requests are static resources */ + httpAuthorization.requestMatchers("/**").permitAll(); } - - private AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) { - DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder()); - provider.setUserDetailsService(userDetailsService); - return provider; - } } diff --git a/server/src/main/java/ru/mskobaro/tdms/system/config/WebConfig.java b/server/src/main/java/ru/mskobaro/tdms/system/config/WebConfig.java new file mode 100644 index 0000000..d27e8fb --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/system/config/WebConfig.java @@ -0,0 +1,28 @@ +package ru.mskobaro.tdms.system.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.PathResourceResolver; + +import java.io.IOException; +import org.springframework.core.io.Resource; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**") + .addResourceLocations("classpath:/static/") + .resourceChain(true) + .addResolver(new PathResourceResolver() { + @Override + protected Resource getResource(String resourcePath, Resource location) throws IOException { + Resource requestedResource = location.createRelative(resourcePath); + return requestedResource.exists() && requestedResource.isReadable() + ? requestedResource + : location.createRelative("index.html"); + } + }); + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/mskobaro/tdms/system/web/AccessDeniedExceptionHandler.java b/server/src/main/java/ru/mskobaro/tdms/system/web/AccessDeniedExceptionHandler.java new file mode 100644 index 0000000..4a85bb8 --- /dev/null +++ b/server/src/main/java/ru/mskobaro/tdms/system/web/AccessDeniedExceptionHandler.java @@ -0,0 +1,20 @@ +package ru.mskobaro.tdms.system.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +@Component +public class AccessDeniedExceptionHandler implements AccessDeniedHandler { + @Autowired + private HandlerExceptionResolver handlerExceptionResolver; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) { + handlerExceptionResolver.resolveException(request, response, null, new ru.mskobaro.tdms.business.exception.AccessDeniedException()); + } +} diff --git a/server/src/main/java/ru/mskobaro/tdms/system/web/DevAuthenticationRequestFilter.java b/server/src/main/java/ru/mskobaro/tdms/system/web/DevAuthenticationRequestFilter.java deleted file mode 100644 index b709407..0000000 --- a/server/src/main/java/ru/mskobaro/tdms/system/web/DevAuthenticationRequestFilter.java +++ /dev/null @@ -1,46 +0,0 @@ -package ru.mskobaro.tdms.system.web; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.TransientSecurityContext; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; -import org.springframework.web.filter.OncePerRequestFilter; -import ru.mskobaro.tdms.domain.entity.Role; -import ru.mskobaro.tdms.domain.entity.User; - -import java.io.IOException; -import java.util.List; - -import static ru.mskobaro.tdms.domain.service.RoleService.Authority.ADMIN; - -@Slf4j -public class DevAuthenticationRequestFilter extends OncePerRequestFilter { - public DevAuthenticationRequestFilter() { - log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!"); - log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!"); - log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!"); - log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!"); - log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!"); - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - Role role = new Role(-1L, "dev_admin_role", ADMIN.getAuthority()); - User admin = new User(); - admin.setRoles(List.of(role)); - admin.setId(-1L); - admin.setLogin("dev_admin"); - admin.setEmail("dev_admin@main.mail"); - admin.setFullName("dev_admin"); - admin.setNumberPhone("+79999999999"); - admin.setPassword("{bcrypt}$2a$06$BHHQMjwQB2KI9sDdC9rRHOuYkTskjDt9WAyrscWP/Dcn7my3Jr77K"); - var auth = new PreAuthenticatedAuthenticationToken(admin, null, admin.getAuthorities()); - var context = new TransientSecurityContext(auth); - SecurityContextHolder.setContext(context); - filterChain.doFilter(request, response); - } -} diff --git a/server/src/main/java/ru/mskobaro/tdms/system/web/LoggingRequestFilter.java b/server/src/main/java/ru/mskobaro/tdms/system/web/LoggingRequestFilter.java index 062f8b5..01397de 100644 --- a/server/src/main/java/ru/mskobaro/tdms/system/web/LoggingRequestFilter.java +++ b/server/src/main/java/ru/mskobaro/tdms/system/web/LoggingRequestFilter.java @@ -6,24 +6,30 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.UUID; @Slf4j public class LoggingRequestFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + UUID uuid = UUID.randomUUID(); long startTime = System.currentTimeMillis(); + String username = SecurityContextHolder.getContext().getAuthentication() != null ? + SecurityContextHolder.getContext().getAuthentication().getName() : "anonymousUser"; HttpSession session = request.getSession(false); - log.info("Request received: [{}] {} user: {}, session: {}, remote ip: {}", - request.getMethod(), request.getRequestURI(), request.getRemoteUser(), - session == null ? "no" : session.getId(), request.getRemoteAddr()); + log.info("Request received: [{}] {} user: {}, session: {}, remote ip: {} [{}]", + request.getMethod(), request.getRequestURI(), username, + session == null ? "no" : session.getId(), request.getRemoteAddr(), uuid); try { filterChain.doFilter(request, response); } finally { long duration = System.currentTimeMillis() - startTime; - log.info("Response with {} status duration: {} ms", response.getStatus(), duration); + log.info("Response with {} status duration: {} ms [{}]", response.getStatus(), duration, uuid); } } } diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 72731bc..2649fb3 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -30,11 +30,6 @@ spring: schemas: ${db.schema} banner: location: banner.txt - web: - resources: - static-locations: classpath:/static/ -# autoconfigure: -# exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration server: port: ${application.port} address: ${application.domain} diff --git a/server/src/main/resources/db/migration/V00000__Initial_schema.sql b/server/src/main/resources/db/migration/V00000__Initial_schema.sql new file mode 100644 index 0000000..22f5225 --- /dev/null +++ b/server/src/main/resources/db/migration/V00000__Initial_schema.sql @@ -0,0 +1,280 @@ +CREATE TABLE defense +( + id BIGSERIAL PRIMARY KEY, + defense_date DATE, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE defense_best_student_works +( + defense_id BIGINT, + student_data_id BIGINT +); + +CREATE TABLE defense_commission +( + defense_id BIGINT, + commission_member_data_id BIGINT +); + +CREATE TABLE diploma_topic +( + id BIGSERIAL PRIMARY KEY, + name TEXT, + teacher_id BIGINT, + direction_of_preparation_id BIGINT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE direction_of_preparation +( + id BIGSERIAL PRIMARY KEY, + name TEXT, + code TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE "group" +( + id BIGSERIAL PRIMARY KEY, + name TEXT, + defense_id BIGINT, + direction_of_preparation_id BIGINT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE participant +( + id BIGSERIAL PRIMARY KEY, + first_name TEXT, + last_name TEXT, + middle_name TEXT, + email TEXT, + number_phone TEXT, + deleted BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE participant_role +( + partic_id BIGINT NOT NULL, + role_id BIGINT NOT NULL +); + +CREATE TABLE role +( + id BIGINT PRIMARY KEY, + name TEXT, + authority TEXT +); + +/* todo */ +CREATE TABLE student_data +( + id BIGSERIAL PRIMARY KEY, + partic_id BIGINT, + study_form_id BIGINT, + + curator_id BIGINT, + protection_order INTEGER, + protection_day INTEGER, + + mark_comment INTEGER, + mark_practice INTEGER, + + predefnese_comment TEXT, + normal_control BOOLEAN, + anti_plagiarism INTEGER, + record_book_returned BOOLEAN, + work TEXT, + diploma_topic_id BIGINT, + adviser_teacher_partic_id BIGINT, + group_id BIGINT, + marks_3 BIGINT, + marks_4 BIGINT, + marks_5 BIGINT, + commission_mark BIGINT, + estimated BOOLEAN, + diploma_with_honors BOOLEAN, + magistracy_recommendation BOOLEAN, + magistracy_wanted BOOLEAN, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +/* not implemented */ +create table questionnaire +( + id BIGSERIAL PRIMARY KEY, + student_data_id BIGINT, + data TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +create table study_form +( + id BIGSERIAL PRIMARY KEY, + name TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +/* todo */ +create table stud_comment +( + id BIGSERIAL PRIMARY KEY, + comment TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +/* todo */ +create table student_data_comment +( + id BIGSERIAL PRIMARY KEY, + student_data_id BIGINT, + stud_comment_id BIGINT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE teacher_data +( + id BIGSERIAL PRIMARY KEY, + participant_id BIGINT, + degree TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE commission_member_data +( + id BIGSERIAL PRIMARY KEY, + partic_id BIGINT, + work_place TEXT, + work_position TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE "user" +( + id BIGSERIAL PRIMARY KEY, + login TEXT, + password TEXT, + partic_id BIGINT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE task +( + id BIGSERIAL PRIMARY KEY, + type TEXT, + status TEXT, + fields jsonb, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE +); + +ALTER TABLE defense_commission + ADD CONSTRAINT FK_DEFCOM_ON_DEFNESE FOREIGN KEY (defense_id) REFERENCES defense (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE defense_commission + ADD CONSTRAINT FK_DEFCOM_ON_COMMEMD FOREIGN KEY (commission_member_data_id) REFERENCES commission_member_data (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "group" + ADD CONSTRAINT FK_GROUP_ON_defnese FOREIGN KEY (defense_id) REFERENCES defense (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "group" + ADD CONSTRAINT FK_GROUP_ON_DOP FOREIGN KEY (direction_of_preparation_id) REFERENCES direction_of_preparation (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE participant_role + ADD CONSTRAINT FK_PARROL_ON_PARTICIPANT FOREIGN KEY (partic_id) REFERENCES participant (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE participant_role + ADD CONSTRAINT FK_PARROL_ON_ROLE FOREIGN KEY (role_id) REFERENCES role (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE student_data + ADD CONSTRAINT UC_STUDENT_DATA_PARTIC UNIQUE (partic_id); + +ALTER TABLE student_data + ADD CONSTRAINT FK_STUDENT_DATA_ON_ADVISER_TEACHER_PARTIC FOREIGN KEY (adviser_teacher_partic_id) REFERENCES participant (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE student_data + ADD CONSTRAINT FK_STUDENT_DATA_ON_DIPLOMA_TOPIC FOREIGN KEY (diploma_topic_id) REFERENCES diploma_topic (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE student_data + ADD CONSTRAINT FK_STUDENT_DATA_ON_GROUP FOREIGN KEY (group_id) REFERENCES "group" (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE student_data + ADD CONSTRAINT FK_STUDENT_DATA_ON_PARTIC FOREIGN KEY (partic_id) REFERENCES participant (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE participant + ADD CONSTRAINT UC_PARTICIPANT_EMAIL UNIQUE (email); + +ALTER TABLE participant + ADD CONSTRAINT UC_PARTICIPANT_NUMBER_PHONE UNIQUE (number_phone); + +ALTER TABLE "user" + ADD CONSTRAINT UC_USER_LOGIN UNIQUE (login); + +ALTER TABLE "user" + ADD CONSTRAINT UC_USER_PARTIC UNIQUE (partic_id); + +ALTER TABLE "user" + ADD CONSTRAINT FK_USER_ON_PARTIC FOREIGN KEY (partic_id) REFERENCES participant (id); + +ALTER TABLE teacher_data + ADD CONSTRAINT UC_TEACHER_DATA_PARTIC UNIQUE (participant_id); + +ALTER TABLE teacher_data + ADD CONSTRAINT FK_TEACHER_DATA_ON_PARTIC FOREIGN KEY (participant_id) REFERENCES participant (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE commission_member_data + ADD CONSTRAINT UC_COMMISION_MEMBER_DATA_PARTIC UNIQUE (partic_id); + +ALTER TABLE commission_member_data + ADD CONSTRAINT FK_COMMISION_MEMBER_DATA_ON_PARTIC FOREIGN KEY (partic_id) REFERENCES participant (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE direction_of_preparation + ADD CONSTRAINT UC_DIRECTION_OF_PREPARATION_NAME UNIQUE (name); + +ALTER TABLE direction_of_preparation + ADD CONSTRAINT UC_DIRECTION_OF_PREPARATION_CODE UNIQUE (code); + +ALTER TABLE diploma_topic + ADD CONSTRAINT UC_DIPLOMA_TOPIC_NAME FOREIGN KEY (direction_of_preparation_id) REFERENCES direction_of_preparation (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE defense_best_student_works + ADD CONSTRAINT FK_DEFENSE_BEST_STUDENT_WORKS_ON_DEFENSE FOREIGN KEY (defense_id) REFERENCES defense (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE defense_best_student_works + ADD CONSTRAINT FK_DEFENSE_BEST_STUDENT_WORKS_ON_STUDENT_DATA FOREIGN KEY (student_data_id) REFERENCES student_data (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE stud_comment + ADD CONSTRAINT UC_STUD_COMMENT_COMMENT UNIQUE (comment); + +ALTER TABLE student_data + ADD CONSTRAINT UC_STUDENT_DATA_DIPLOMA_TOPIC FOREIGN KEY (diploma_topic_id) REFERENCES diploma_topic (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE student_data + ADD CONSTRAINT UC_STUDENT_DATA_STUDY_FORM FOREIGN KEY (study_form_id) REFERENCES study_form (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE student_data + ADD CONSTRAINT UC_STUDENT_DATA_CURATOR FOREIGN KEY (curator_id) REFERENCES teacher_data (id) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE study_form + ADD CONSTRAINT UC_STUDY_FORM_NAME UNIQUE (name); + +alter table questionnaire + add constraint UC_QUESTIONNAIRE_STUDENT_DATA_ID unique (student_data_id); + +alter table questionnaire + add constraint FK_QUESTIONNAIRE_STUDENT_DATA foreign key (student_data_id) references student_data (id) on delete cascade on update cascade; \ No newline at end of file diff --git a/server/src/main/resources/db/migration/V00010__Create__role_table.sql b/server/src/main/resources/db/migration/V00010__Create__role_table.sql deleted file mode 100644 index 90f769c..0000000 --- a/server/src/main/resources/db/migration/V00010__Create__role_table.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table role -( - id bigint primary key, - - name text not null unique, - authority text not null unique -); - --- COMMENTS -comment on table role is 'Таблица ролей пользователей'; - -comment on column role.name is 'Человекочитаемое имя роли'; -comment on column role.authority is 'Имя роли в системе'; diff --git a/server/src/main/resources/db/migration/V00020__Create__user_table.sql b/server/src/main/resources/db/migration/V00020__Create__user_table.sql deleted file mode 100644 index a8b6962..0000000 --- a/server/src/main/resources/db/migration/V00020__Create__user_table.sql +++ /dev/null @@ -1,22 +0,0 @@ -create table "user" -( - id bigserial primary key, - - login text not null unique, - password text not null, - full_name text not null, - email text not null unique, - number_phone text not null unique, - - created_at timestamptz not null, - updated_at timestamptz -); - --- COMMENTS -comment on table "user" is 'Таблица пользователей'; - -comment on column "user".login is 'Логин пользователя'; -comment on column "user".password is 'Пароль пользователя'; -comment on column "user".full_name is 'Полное имя пользователя в формате Фамилия Имя Отчество'; -comment on column "user".email is 'Почта пользователя'; -comment on column "user".number_phone is 'Номер телефона пользователя'; diff --git a/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql b/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql deleted file mode 100644 index 1082a87..0000000 --- a/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql +++ /dev/null @@ -1,24 +0,0 @@ -create table user_role -( - id bigserial primary key, - - user_id bigint not null, - role_id bigint not null -); - --- FOREIGN KEY -alter table user_role - add constraint fk_user_role_user_id - foreign key (user_id) references "user" (id) - on delete cascade on update cascade; - -alter table user_role - add constraint fk_user_role_role_id - foreign key (role_id) references role (id) - on delete restrict on update cascade; - --- COMMENTS -comment on table user_role is 'Таблица связи пользователей и ролей'; - -comment on column user_role.user_id is 'Идентификатор пользователя'; -comment on column user_role.role_id is 'Идентификатор роли'; diff --git a/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql b/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql deleted file mode 100644 index 4c9d3c9..0000000 --- a/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table diploma_topic -( - id bigserial primary key, - - name text not null -); - --- COMMENTS -comment on table diploma_topic is 'Таблица тем дипломных работ'; - -comment on column diploma_topic.name is 'Название темы дипломной работы'; diff --git a/server/src/main/resources/db/migration/V00050__Create__teacher_table.sql b/server/src/main/resources/db/migration/V00050__Create__teacher_table.sql deleted file mode 100644 index 8d7df2f..0000000 --- a/server/src/main/resources/db/migration/V00050__Create__teacher_table.sql +++ /dev/null @@ -1,19 +0,0 @@ -create table teacher -( - id bigserial primary key, - user_id bigint not null, - - created_at timestamptz not null, - updated_at timestamptz -); - --- FOREIGN KEY -alter table teacher - add constraint fk_teacher_user_id - foreign key (user_id) references "user" (id) - on delete cascade on update cascade; - --- COMMENTS -comment on table teacher is 'Таблица преподавателей'; - -comment on column teacher.user_id is 'Идентификатор пользователя'; diff --git a/server/src/main/resources/db/migration/V00060__Create__group_table.sql b/server/src/main/resources/db/migration/V00060__Create__group_table.sql deleted file mode 100644 index 4246453..0000000 --- a/server/src/main/resources/db/migration/V00060__Create__group_table.sql +++ /dev/null @@ -1,22 +0,0 @@ -create table "group" -( - id bigserial primary key, - - name text not null unique, - curator_teacher_id bigint, - - created_at timestamptz not null, - updated_at timestamptz -); - --- FOREIGN KEY -alter table "group" - add constraint fk_group_curator_teacher_id - foreign key (curator_teacher_id) references teacher (id) - on delete set null on update cascade; - --- COMMENTS -comment on table "group" is 'Таблица групп студентов'; - -comment on column "group".name is 'Название группы'; -comment on column "group".curator_teacher_id is 'Идентификатор куратора группы'; diff --git a/server/src/main/resources/db/migration/V00070__Create__student_table.sql b/server/src/main/resources/db/migration/V00070__Create__student_table.sql deleted file mode 100644 index a7739c3..0000000 --- a/server/src/main/resources/db/migration/V00070__Create__student_table.sql +++ /dev/null @@ -1,65 +0,0 @@ -create table student -( - id bigserial primary key, - user_id bigint not null, - diploma_topic_id bigint, - adviser_teacher_id bigint, - group_id bigint, - - form boolean, - protection_day int, - protection_order int, - magistracy text, - digital_format_present boolean, - mark_comment int, - mark_practice int, - predefence_comment text, - normal_control text, - anti_plagiarism int, - note text, - record_book_returned boolean, - work text, - - created_at timestamptz not null, - updated_at timestamptz -); - --- FOREIGN KEY -alter table student - add constraint fk_student_user_id - foreign key (user_id) references "user" (id) - on delete cascade on update cascade; -alter table student - add constraint fk_student_diploma_topic_id - foreign key (diploma_topic_id) references diploma_topic (id) - on delete set null on update cascade; -alter table student - add constraint fk_student_adviser_teacher_id - foreign key (adviser_teacher_id) references teacher (id) - on delete set null on update cascade; -alter table student - add constraint fk_student_group_id - foreign key (group_id) references "group" (id) - on delete set null on update cascade; - --- COMMENTS -comment on table student is 'Таблица студентов'; - -comment on column student.user_id is 'Идентификатор пользователя'; -comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы'; -comment on column student.adviser_teacher_id is 'Идентификатор научного руководителя'; -comment on column student.group_id is 'Идентификатор группы'; - -comment on column student.form is 'Форма обучения'; -comment on column student.protection_day is 'День защиты'; -comment on column student.protection_order is 'Порядок защиты'; -comment on column student.magistracy is 'Магистратура'; -comment on column student.digital_format_present is 'Предоставлен в электронном виде'; -comment on column student.mark_comment is 'Комментарий к оценке'; -comment on column student.mark_practice is 'Оценка практики'; -comment on column student.predefence_comment is 'Комментарий к защите'; -comment on column student.normal_control is 'Обычный контроль'; -comment on column student.anti_plagiarism is 'Антиплагиат'; -comment on column student.note is 'Примечание'; -comment on column student.record_book_returned is 'Ведомость возвращена'; -comment on column student.work is 'Работа'; diff --git a/server/src/main/resources/db/migration/V00500__Insert_default_roles__role.sql b/server/src/main/resources/db/migration/V00500__Insert_default_roles.sql similarity index 67% rename from server/src/main/resources/db/migration/V00500__Insert_default_roles__role.sql rename to server/src/main/resources/db/migration/V00500__Insert_default_roles.sql index a108c7d..8c143c4 100644 --- a/server/src/main/resources/db/migration/V00500__Insert_default_roles__role.sql +++ b/server/src/main/resources/db/migration/V00500__Insert_default_roles.sql @@ -3,4 +3,5 @@ values (1, 'Преподаватель', 'ROLE_TEACHER'), (2, 'Студент', 'ROLE_STUDENT'), (3, 'Член комиссии ГЭК', 'ROLE_COMMISSION_MEMBER'), (4, 'Администратор', 'ROLE_ADMINISTRATOR'), - (5, 'Секретарь', 'ROLE_SECRETARY'); + (5, 'Секретарь', 'ROLE_SECRETARY'), + (6, 'Проверяющий на плагиат', 'ROLE_PLAGIARISM_CHECKER'); diff --git a/server/src/main/resources/db/migration/V00510__Insert_administrator.sql b/server/src/main/resources/db/migration/V00510__Insert_administrator.sql deleted file mode 100644 index d1de6cb..0000000 --- a/server/src/main/resources/db/migration/V00510__Insert_administrator.sql +++ /dev/null @@ -1,8 +0,0 @@ -insert into "user" (id, login, password, full_name, email, number_phone, created_at) -values (1, 'admin', '{noop}Admin000', 'Администратор', 'admin@tdms.tu-byransk.ru', '', now()); - -insert into user_role (id, user_id, role_id) -values (1, 1, 4); - -select setval('user_id_seq', (select max(id) from "user")); -select setval('user_role_id_seq', (select max(id) from user_role)); diff --git a/server/src/main/resources/db/migration/V00510__Insert_system_administrator.sql b/server/src/main/resources/db/migration/V00510__Insert_system_administrator.sql new file mode 100644 index 0000000..e3e769a --- /dev/null +++ b/server/src/main/resources/db/migration/V00510__Insert_system_administrator.sql @@ -0,0 +1,21 @@ +insert into participant (id, first_name, email, number_phone, created_at, updated_at) +values (1, + 'Администратор', + 'admin@tdms.tu-bryansk.ru', + '+74832580058', + now(), + now()); + +insert into "user" (id, login, password, partic_id, created_at, updated_at) +values (1, + 'admin', + '{noop}Admin000', + 1, + now(), + now()); + +insert into participant_role (partic_id, role_id) +values (1, 4); + +select setval('participant_id_seq', (select max(id) from participant)); +select setval('user_id_seq', (select max(id) from "user")); diff --git a/server/src/main/resources/db/test-data/V00070__Create__defence_table.sql b/server/src/main/resources/db/test-data/V00070__Create__defence_table.sql index 30e30e1..c0bb458 100644 --- a/server/src/main/resources/db/test-data/V00070__Create__defence_table.sql +++ b/server/src/main/resources/db/test-data/V00070__Create__defence_table.sql @@ -1,4 +1,4 @@ -create table defence +create table defense ( id bigserial primary key, defence_date timestamptz, @@ -8,5 +8,5 @@ create table defence ); -- COMMENTS -comment on table defence is 'Таблица для хранения данных о защитах'; -comment on column defence.defence_date is 'Дата защиты'; \ No newline at end of file +comment on table defense is 'Таблица для хранения данных о защитах'; +comment on column defense.defence_date is 'Дата защиты'; \ No newline at end of file diff --git a/server/src/main/resources/db/test-data/group.sql b/server/src/main/resources/db/test-data/group.sql index 2f9f72d..0b0c6a4 100644 --- a/server/src/main/resources/db/test-data/group.sql +++ b/server/src/main/resources/db/test-data/group.sql @@ -1,3 +1,3 @@ -INSERT INTO "group" (name, principal_user_id) -VALUES ('ИВТ-1', 40), - ('ИВТ-2', 40); +INSERT INTO "group" (name, curator_teacher_id, created_at, updated_at) +VALUES ('ИВТ-1', 40, now(), now()), + ('ИВТ-2', 40, now(), now()); diff --git a/server/src/main/resources/db/test-data/student.sql b/server/src/main/resources/db/test-data/student.sql index d610ec4..840c618 100644 --- a/server/src/main/resources/db/test-data/student.sql +++ b/server/src/main/resources/db/test-data/student.sql @@ -1,7 +1,7 @@ do $$ begin - INSERT INTO student (form, + INSERT INTO studentData (form, protection_order, magistracy, digital_format_present, diff --git a/web/package-lock.json b/web/package-lock.json index b71c94c..346a708 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -32,6 +32,7 @@ "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/webpack": "^5.28.5", + "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "html-webpack-plugin": "^5.6.2", "style-loader": "^4.0.0", @@ -3128,6 +3129,100 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", + "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/core-js-compat": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", @@ -6215,6 +6310,51 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/web/package.json b/web/package.json index 63c358b..bc379ea 100644 --- a/web/package.json +++ b/web/package.json @@ -31,6 +31,7 @@ "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/webpack": "^5.28.5", + "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "html-webpack-plugin": "^5.6.2", "style-loader": "^4.0.0", diff --git a/web/pom.xml b/web/pom.xml index 0e9b08e..1ae56ab 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -9,7 +9,6 @@ 0.0.1 - ru.mskobaro.tdms web 0.0.1 pom diff --git a/web/src/Application.tsx b/web/src/Application.tsx index a1fef20..30bc287 100644 --- a/web/src/Application.tsx +++ b/web/src/Application.tsx @@ -5,17 +5,21 @@ import React from "react"; import 'bootstrap/dist/css/bootstrap.min.css'; import './index.css' import {RootStoreContext} from './utils/context'; -import {Home} from "./components/custom/layout/Home"; -import {UserProfilePage} from "./components/user/UserProfilePage"; -import {UserListPage} from "./components/user/UserListPage"; +import {Home} from "./components/layout/Home"; import {GroupListPage} from "./components/group/GroupListPage"; -import {Error} from "./components/custom/layout/Error"; +import {Error} from "./components/layout/Error"; +import {ParticipantListPage} from "./components/participant/ParticipantListPage"; +import {DefenceListPage} from "./components/defence/DefenceListPage"; +import {PreparationDirectionListPage} from "./components/dictionary/PreparationDirectionList"; +import {DiplomaTopicListPage} from "./components/dictionary/DiplomaTopicList"; const viewMap: ViewMap = { home: , - profile: , - userList: , + participantList: , groupList: , + defenceList: , + themeList: , + preparationDirectionList: , error: , } diff --git a/web/src/components/NotificationContainer.tsx b/web/src/components/NotificationContainer.tsx index fe8d3b4..91b96f5 100644 --- a/web/src/components/NotificationContainer.tsx +++ b/web/src/components/NotificationContainer.tsx @@ -1,9 +1,9 @@ import {ComponentContext} from "../utils/ComponentContext"; import {observer} from "mobx-react"; -import {Notification, NotificationType} from "../store/NotificationStore"; import {Card, CardBody, CardHeader, CardText, CardTitle} from "react-bootstrap"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {action, makeObservable} from "mobx"; +import {Notification, NotificationService, NotificationType} from "../services/NotificationService"; @observer export class NotificationContainer extends ComponentContext { @@ -15,10 +15,10 @@ export class NotificationContainer extends ComponentContext { render() { return
- {this.forEachNotificationRender(this.notificationStore.errors, NotificationType.ERROR)} - {this.forEachNotificationRender(this.notificationStore.successes, NotificationType.SUCCESS)} - {this.forEachNotificationRender(this.notificationStore.warnings, NotificationType.WARNING)} - {this.forEachNotificationRender(this.notificationStore.infos, NotificationType.INFO)} + {this.forEachNotificationRender(NotificationService.errors, NotificationType.ERROR)} + {this.forEachNotificationRender(NotificationService.successes, NotificationType.SUCCESS)} + {this.forEachNotificationRender(NotificationService.warnings, NotificationType.WARNING)} + {this.forEachNotificationRender(NotificationService.infos, NotificationType.INFO)}
} } @@ -33,7 +33,7 @@ class NotificationPopup extends ComponentContext<{ notification: Notification, t @action.bound close() { - this.notificationStore.close(this.props.notification.uuid); + NotificationService.close(this.props.notification.uuid); } get cardClassName() { diff --git a/web/src/components/controls/ReactiveControls.css b/web/src/components/controls/ReactiveControls.css new file mode 100644 index 0000000..43b8304 --- /dev/null +++ b/web/src/components/controls/ReactiveControls.css @@ -0,0 +1,25 @@ +.l-no-bg label::after { + background-color: rgba(0, 0, 0, 0) !important; +} + +.btn-group-lg > .btn, .btn-lg { + --bs-btn-border-radius: var(--bs-border-radius); +} + +.no-reaction { + transition: none !important; + background-color: rgba(255,255,255,0) !important; + color: inherit !important; + border: 0 !important; + box-shadow: none !important; +} + +.no-reaction:hover, +.no-reaction:focus, +.no-reaction:active { + transition: none !important; + background-color: rgba(255,255,255,0) !important; + color: inherit !important; + border: 0 !important; + box-shadow: none !important; +} \ No newline at end of file diff --git a/web/src/components/controls/ReactiveControls.tsx b/web/src/components/controls/ReactiveControls.tsx new file mode 100644 index 0000000..f809c78 --- /dev/null +++ b/web/src/components/controls/ReactiveControls.tsx @@ -0,0 +1,451 @@ +import React, { + ChangeEvent, + Component +} from "react"; +import { + ReactiveValue +} from "../../utils/reactive/reactiveValue"; +import { + observer +} from "mobx-react"; +import { + action, + makeObservable, + observable, + runInAction +} from "mobx"; +import { + Badge, + Button, + ButtonGroup, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownToggle, + FloatingLabel, + FormCheck, + FormControl, + FormSelect, + FormText +} from "react-bootstrap"; +import { + FontAwesomeIcon +} from "@fortawesome/react-fontawesome"; +import './ReactiveControls.css'; +import { + TableDescriptor +} from "../../utils/tables"; +import { + DataTable +} from "../data-tables/DataTable"; +import { + ModalState +} from "../../utils/modalState"; + +export interface ReactiveInputProps { + value: ReactiveValue; + label?: string; + disabled?: boolean; + className?: string; + validateless?: boolean; +} + +/* Boolean Input */ + +@observer +export class BooleanInput extends Component> { + constructor(props: any) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto(false); + } + this.props.value.setField(this.props.label); + } + + @action.bound + onChange(event: React.ChangeEvent) { + this.props.value.set(event.currentTarget.checked); + } + + render() { + return
+
+ {this.props.label} + +
+ +
+ } +} + +/* String Input */ + +@observer +export class StringInput extends Component> { + constructor(props: any) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto(''); + } + this.props.value.setField(this.props.label); + } + + @action.bound + onChange(event: React.ChangeEvent) { + this.props.value.set(event.currentTarget.value); + } + + render() { + const inputClassName = `${this.props.validateless ? '' : this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`; + + return
+ + + + +
+ } +} + +/* Password Input */ + +@observer +export class PasswordInput extends Component> { + @observable showPassword = false; + + constructor(props: any) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto(''); + } + this.props.value.setField(this.props.label); + } + + @action.bound + onChange(event: React.ChangeEvent) { + this.props.value.set(event.currentTarget.value); + } + + @action.bound + toggleShowPassword() { + this.showPassword = !this.showPassword; + } + + render() { + return
+
+ + + + +
+ +
+ } +} + +/* Select Input */ + +export interface SelectInputValue { + value: string; + label: string; +} + +export interface SelectInputProps extends ReactiveInputProps { + possibleValues: SelectInputValue[]; + unselectedLabel?: string; + initial?: SelectInputValue; +} + +@observer +export class SelectInput extends Component { + constructor(props: SelectInputProps) { + super(props); + makeObservable(this); + runInAction(() => { + this.options = props.possibleValues; + this.value = props.value; + this.value.setField(this.props.label); + + if (this.value.value === undefined && this.props.unselectedLabel !== undefined) { + this.options.unshift({ + value: '__unselected__', + label: this.props.unselectedLabel + }); + this.value.setAuto(this.options[0]); + } else if (this.props.initial) { + this.value.set(this.props.initial); + } else { + this.value.set(this.options[0]); + } + }); + } + + @observable options: { + value: string, + label: string + }[]; + @observable value: ReactiveValue; + + @action.bound + onChange(event: ChangeEvent) { + this.value.set(this.options.find(option => option.value === event.currentTarget.value)); + } + + render() { + const inputClassName = `${this.props.validateless ? '' : this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`; + + return
+ { + + + { + this.options.map(option => + ) + } + + + } + +
+ } +} + +/* Multiple select */ + +export interface MultipleSelectInputProps extends ReactiveInputProps { + possibleValues: SelectInputValue[]; + singleSelect?: boolean; + filtering?: boolean; +} + +@observer +export class DropdownSelectInput extends Component { + constructor(props: MultipleSelectInputProps) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto([]); + } + this.initField(this.props); + runInAction(() => { + this.options = props.possibleValues; + }); + } + + componentDidUpdate(prevProps: Readonly) { + if (this.value != prevProps.value) { + this.initField(this.props); + } + } + + @action.bound + initField(prps: MultipleSelectInputProps) { + this.value = prps.value; + this.value.setField(prps.label); + } + + @observable options: SelectInputValue[]; + @observable value: ReactiveValue; + @observable filter: string = ''; + + @action.bound + onCloseClick(event: React.MouseEvent) { + event.stopPropagation(); + const value = (event.target as HTMLElement).closest('span').getAttribute('data-value'); + this.value.set(this.value.value.filter(sel => sel.value !== value)); + } + + @action.bound + onAddClick(event: React.MouseEvent) { + const value = (event.target as HTMLElement).getAttribute('data-value'); + const option = this.options.find(opt => opt.value === value); + if (option) { + let contains = this.value.value.find(sel => sel.value === value); + if (contains) { + this.value.set(this.value.value.filter(sel => sel.value !== value)); + } else { + if (this.props.singleSelect) { + this.value.set([option]); + } else { + this.value.set([...this.value.value, option]); + } + } + } + } + + @action.bound + onFilterChange(event: React.ChangeEvent) { + this.filter = (event.target as HTMLInputElement).value; + if (this.filter === '') { + this.options = this.props.possibleValues; + } else { + this.options = this.props.possibleValues.filter(opt => opt.label.toLowerCase().includes(this.filter.toLowerCase())); + } + } + + render() { + const inputClassName = `${this.props.validateless ? '' : this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`; + const inputDisabledBackgroundStyle = this.props.disabled ? {backgroundColor: 'rgb(233, 236, 239)'} : {}; + const inputDisabledColor = this.props.disabled ? '#000000' : 'rgb(33, 37, 41)'; + + return
+ { + + + + { + !this.props.disabled && + + } + + + + { + this.props.filtering && +
+ +
+ } + { + this.options?.map(option => + + {option.label} + ) + } +
+
+ } + +
+ } +} + +/* Table */ + +export class TableInputNewEntryState { + constructor() { + makeObservable(this); + } + + @observable newEntryCallback: (entry: T) => void; + + @action.bound + newEntry(entry: T) { + this.newEntryCallback && this.newEntryCallback(entry); + } +} + +export interface TableInputProps { + table: TableDescriptor; + searchNewEntryState: TableInputNewEntryState; + searchNewEntryModalState: ModalState; + disabled?: boolean; +} + +@observer +export class TableInput extends Component> { + @observable table: TableDescriptor = this.props.table; + @observable searchNewEntryState: TableInputNewEntryState = this.props.searchNewEntryState; + @observable searchNewEntryModalState: ModalState = this.props.searchNewEntryModalState; + @observable disabled: boolean = this.props.disabled; + + constructor(props: any) { + super(props); + makeObservable(this); + this.table.pageable = false; + this.searchNewEntryState.newEntryCallback = this.addNewEntry; + } + + @action.bound + addNewEntry(entry: T) { + this.table.data.push(entry); + } + + componentDidUpdate() { + runInAction(() => { + this.table = this.props.table; + this.searchNewEntryState = this.props.searchNewEntryState; + this.searchNewEntryModalState = this.props.searchNewEntryModalState; + this.disabled = this.props.disabled; + }); + } + + render() { + return + } +} diff --git a/web/src/components/custom/controls/ReactiveControls.css b/web/src/components/custom/controls/ReactiveControls.css deleted file mode 100644 index 9efa40b..0000000 --- a/web/src/components/custom/controls/ReactiveControls.css +++ /dev/null @@ -1,3 +0,0 @@ -.l-no-bg label::after { - background-color: rgba(0, 0, 0, 0) !important; -} \ No newline at end of file diff --git a/web/src/components/custom/controls/ReactiveControls.tsx b/web/src/components/custom/controls/ReactiveControls.tsx deleted file mode 100644 index c604766..0000000 --- a/web/src/components/custom/controls/ReactiveControls.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, {ChangeEvent, Component} from "react"; -import {ReactiveValue} from "../../../utils/reactive/reactiveValue"; -import {observer} from "mobx-react"; -import {action, makeObservable, observable, runInAction} from "mobx"; -import {Button, FloatingLabel, FormControl, FormSelect, FormText} from "react-bootstrap"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import './ReactiveControls.css'; - -export interface ReactiveInputProps { - value: ReactiveValue; - label?: string; - disabled?: boolean; - className?: string; - validateless?: boolean; -} - -@observer -export class StringInput extends Component> { - constructor(props: any) { - super(props); - makeObservable(this); - if (this.props.value.value === undefined) { - this.props.value.setAuto(''); - } - this.props.value.setField(this.props.label); - } - - @action.bound - onChange(event: React.ChangeEvent) { - this.props.value.set(event.currentTarget.value); - } - - render() { - const inputClassName = `${this.props.validateless ? '' : this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`; - - return
- - - - -
- } -} - -@observer -export class PasswordInput extends Component> { - @observable showPassword = false; - - constructor(props: any) { - super(props); - makeObservable(this); - if (this.props.value.value === undefined) { - this.props.value.setAuto(''); - } - this.props.value.setField(this.props.label); - } - - @action.bound - onChange(event: React.ChangeEvent) { - this.props.value.set(event.currentTarget.value); - } - - @action.bound - toggleShowPassword() { - this.showPassword = !this.showPassword; - } - - render() { - return
-
- - - - -
- -
- } -} - -export interface ReactiveSelectInputSelect { - value: string; - label: string; -} - -export interface ReactiveSelectInputProps extends ReactiveInputProps { - possibleValues: ReactiveSelectInputSelect[]; - unselectedLabel?: string; - initial?: ReactiveSelectInputSelect; -} - -@observer -export class SelectInput extends Component { - constructor(props: ReactiveSelectInputProps) { - super(props); - makeObservable(this); - runInAction(() => { - this.options = props.possibleValues; - this.value = props.value; - this.value.setField(this.props.label); - - if (this.value.value === undefined && this.props.unselectedLabel !== undefined) { - this.options.unshift({value: '__unselected__', label: this.props.unselectedLabel}); - this.value.setAuto(this.options[0]); - } else if (this.props.initial) { - this.value.set(this.props.initial); - } else { - this.value.set(this.options[0]); - } - }); - } - - @observable options: { value: string, label: string }[]; - @observable value: ReactiveValue; - - @action.bound - onChange(event: ChangeEvent) { - this.value.set(this.options.find(option => option.value === event.currentTarget.value)); - } - - render() { - const inputClassName = `${this.props.validateless ? '' : this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`; - - return
- { - - - { - this.options.map(option => - ) - } - - - } - -
- } -} \ No newline at end of file diff --git a/web/src/components/custom/layout/Header.tsx b/web/src/components/custom/layout/Header.tsx deleted file mode 100644 index b720f97..0000000 --- a/web/src/components/custom/layout/Header.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap"; -import {RouterLink} from "mobx-state-router"; -import {IAuthenticated} from "../../../models/user"; -import {observer} from "mobx-react"; -import {post} from "../../../utils/request"; -import {UserLoginModal} from "../../user/UserLoginModal"; -import {ModalState} from "../../../utils/modalState"; -import {action, makeObservable, observable} from "mobx"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {ComponentContext} from "../../../utils/ComponentContext"; -import {AddGroupModal} from "../../group/AddGroupModal"; -import {UserRegistrationModal} from "../../user/UserRegistrationModal"; - -@observer -export class Header extends ComponentContext { - constructor(props: any) { - super(props); - makeObservable(this); - } - - @observable loginModalState = new ModalState(); - @observable addGroupModalState = new ModalState(); - @observable userRegistrationModalState = new ModalState(); - - render() { - let userThink = this.thinkStore.isThinking('updateCurrentUser'); - - return <> -
- - - - TDMS - - - - - -
- - - - - } -} - -@observer -class AuthenticatedItems extends ComponentContext { - constructor(props: any) { - super(props); - makeObservable(this); - } - - @action.bound - logout() { - post('user/logout').then(() => { - this.userStore.updateCurrentUser(); - this.routerStore.goTo('home').then(); - this.notificationStore.success('Вы успешно вышли из системы', 'Выход'); - }); - } - - render() { - return <> - Пользователь: - - Моя страница - - Выйти - - - } -} diff --git a/web/src/components/custom/layout/Home.tsx b/web/src/components/custom/layout/Home.tsx deleted file mode 100644 index 1f4e41b..0000000 --- a/web/src/components/custom/layout/Home.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import {Page} from "./Page"; -import {observer} from "mobx-react"; - -@observer -export class Home extends Page { - get page() { - return

Home

- } -} diff --git a/web/src/components/custom/DataTable.css b/web/src/components/data-tables/DataTable.css similarity index 100% rename from web/src/components/custom/DataTable.css rename to web/src/components/data-tables/DataTable.css diff --git a/web/src/components/custom/DataTable.tsx b/web/src/components/data-tables/DataTable.tsx similarity index 76% rename from web/src/components/custom/DataTable.tsx rename to web/src/components/data-tables/DataTable.tsx index bc54750..d29f4e5 100644 --- a/web/src/components/custom/DataTable.tsx +++ b/web/src/components/data-tables/DataTable.tsx @@ -4,7 +4,7 @@ import {observer} from "mobx-react"; import {action, computed, makeObservable, observable, runInAction} from "mobx"; import {Button, ButtonGroup, FormSelect, FormText, Table} from "react-bootstrap"; import _ from "lodash"; -import {ChangeEvent} from "react"; +import React, {ChangeEvent} from "react"; import {ModalState} from "../../utils/modalState"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import './DataTable.css'; @@ -14,10 +14,13 @@ export interface DataTableProps { filterModalState?: ModalState; name?: string; headless?: boolean; + editable?: boolean; + editableModalState?: ModalState; + additionalButtons?: React.ReactNode; } @observer -export class DataTable extends ComponentContext> { +export class DataTable extends ComponentContext & { className?: string }> { constructor(props: DataTableProps) { super(props); makeObservable(this); @@ -27,6 +30,23 @@ export class DataTable extends ComponentContext> { @observable name = this.props.name; @observable headless = this.props.headless; @observable filterModalState = this.props.filterModalState; + @observable editable = this.props.editable; + @observable editableModalState = this.props.editableModalState; + @observable className = this.props.className; + @observable additionalButtons?: React.ReactNode = this.props.additionalButtons; + + componentDidUpdate() { + runInAction(() => { + this.descriptor = this.props.tableDescriptor; + this.name = this.props.name; + this.headless = this.props.headless; + this.filterModalState = this.props.filterModalState; + this.editable = this.props.editable; + this.editableModalState = this.props.editableModalState; + this.className = this.props.className; + this.additionalButtons = this.props.additionalButtons; + }); + } @computed get isFirstPage() { @@ -85,7 +105,7 @@ export class DataTable extends ComponentContext> { borderBottomRightRadius: this.descriptor.pageable ? '0' : '0.25rem', } - return <> + return
{ !this.headless && this.header @@ -104,7 +124,7 @@ export class DataTable extends ComponentContext> { this.descriptor.pageable && this.footer } - +
} @computed @@ -120,16 +140,21 @@ export class DataTable extends ComponentContext> { return
{this.name} - +
{ - this.descriptor.pageable && -
{`Записей на странице: ${this.descriptor.pageSize} `}
+ this.additionalButtons && + this.additionalButtons } -
+ { - <> - {`Всего записей: ${this.filteredData.length}`} - + this.descriptor.pageable && +
{`Записей на странице: ${this.descriptor.pageSize} `}
+ } +
+ { + <> + {`Всего записей: ${this.filteredData.length}`} + { this.descriptor.pageable && <> @@ -137,20 +162,21 @@ export class DataTable extends ComponentContext> { } - { - this.filterModalState && -
- -
- } - + { + this.filterModalState && +
+ +
+ } + - } -
-
+ } +
+ +
} @@ -168,7 +194,7 @@ export class DataTable extends ComponentContext> { borderRight: lastColumn ? 'none' : '1px solid var(--bs-table-border-color)', }; - return + return
runInAction(() => { const other = this.descriptor.columns @@ -215,13 +241,13 @@ export class DataTable extends ComponentContext> { return 0; } - return this.filteredData + let elements = this.filteredData .sort(masterComparator) .slice(this.descriptor.page * this.descriptor.pageSize, (this.descriptor.page + 1) * this.descriptor.pageSize) .map((row, i) => { const rowAny = row as any; const lastRow = i === this.descriptor.pageSize - 1; - return + return { this.descriptor.columns.map(column => { const firstColumn = column === this.descriptor.columns[0]; @@ -230,9 +256,9 @@ export class DataTable extends ComponentContext> { const style = { borderLeft: firstColumn ? 'none' : '1px solid var(--bs-table-border-color)', borderRight: lastColumn ? 'none' : '1px solid var(--bs-table-border-color)', - borderBottom: lastRow ? 'none' : '1px solid var(--bs-table-border-color)', + borderBottom: lastRow && !this.editable ? 'none' : '1px solid var(--bs-table-border-color)', } - return {column.format(rowAny[column.key], row)} { @@ -244,6 +270,22 @@ export class DataTable extends ComponentContext> { } }); + + if (this.editable) { + elements.push( + + + + ); + } + + return elements; } @computed diff --git a/web/src/components/defence/DefenceListPage.tsx b/web/src/components/defence/DefenceListPage.tsx new file mode 100644 index 0000000..3a0a0a6 --- /dev/null +++ b/web/src/components/defence/DefenceListPage.tsx @@ -0,0 +1,74 @@ +import {observer} from "mobx-react"; +import {Page} from "../layout/Page"; +import {action, makeObservable, observable, reaction, runInAction} from "mobx"; +import {get} from "../../utils/request"; +import {Group} from "../../models/group"; +import React from "react"; +import {ThinkService} from "../../services/ThinkService"; +import {DataTable} from "../data-tables/DataTable"; +import {Column, TableDescriptor} from "../../utils/tables"; +import {Defence} from "../../models/defence"; +import {Participant} from "../../models/participant"; +import {datetimeConverter} from "../../utils/converters"; +import {StudentData} from "../../models/studentData"; + +class DefenceListPageState { + constructor() { + makeObservable(this); + this.updateDefences(); + reaction(() => this.defences, () => { + this.tableDescriptor = new TableDescriptor([ + new Column('commissionMembers', 'ГЭК', p => p ? p.length : 0), + new Column('groups', 'Группы', g => g && g.length ? g.map(g => g.name).join(', ') : 'Пусто'), + new Column('groups', 'Студентов', g => { + if (!g) return 'Пусто'; + const students: StudentData[] = []; + g.forEach(group => group.students.forEach(student => students.push(student))); + return students.length; + }), + new Column('createdAt', 'Дата создания', datetimeConverter), + new Column('updatedAt', 'Дата обновления', datetimeConverter), + ], this.defences, true + ); + }); + } + + @observable defences: Defence[] = []; + @observable tableDescriptor: TableDescriptor; + + @action.bound + updateDefences() { + ThinkService.think(); + get('/defence/all').then((defences) => { + runInAction(() => { + this.defences = defences; + }); + }).finally(() => { + ThinkService.completeAll(); + }); + } +} + +@observer +export class DefenceListPage extends Page { + constructor(props: any) { + super(props); + makeObservable(this); + this.fields = new DefenceListPageState(); + } + + @observable fields: DefenceListPageState; + + get page() { + return <> + { + <> + { + this.fields.tableDescriptor && + + } + + } + + } +} diff --git a/web/src/components/dictionary/DiplomaTopicList.tsx b/web/src/components/dictionary/DiplomaTopicList.tsx new file mode 100644 index 0000000..3703d08 --- /dev/null +++ b/web/src/components/dictionary/DiplomaTopicList.tsx @@ -0,0 +1,118 @@ +import {observer} from "mobx-react"; +import {Page} from "../layout/Page"; +import {action, makeObservable, observable, reaction, runInAction} from "mobx"; +import React from "react"; +import {DataTable} from "../data-tables/DataTable"; +import {Column, TableDescriptor} from "../../utils/tables"; +import {PreparationDirection} from "../../models/preparationDirection"; +import {get} from "../../utils/request"; +import {datetimeConverter} from "../../utils/converters"; +import {Button} from "react-bootstrap"; +import {ModalState} from "../../utils/modalState"; +import {UserService} from "../../services/UserService"; +import {DiplomaTopic} from "../../models/diplomaTopic"; +import {TeacherData} from "../../models/teacherData"; +import {fullName} from "../../models/participant"; +import DiplomaTopicModal from "./DiplomaTopicModal"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import _ from "lodash"; + +const getAllDiplomaTopics = () => { + return get("diploma-topic/all"); +} + +class DiplomaTopicPageState { + constructor() { + makeObservable(this); + this.updateDiplomaTopics(); + reaction(() => this.diplomaTopics, () => { + this.tableDescriptor = new TableDescriptor([ + new Column('name', 'Название', undefined, (dp) => { + return ; + }), + new Column('teacher', 'Преподаватель', (value) => { + return value ? fullName(value.participant) : "Не указан"; + }), + new Column('preparationDirection', 'Код направления подготовки', value => { + return value ? value.code : "Не указано"; + }), + new Column('preparationDirection', 'Название Направления подготовки', value => { + return value ? value.name : "Не указано"; + }), + new Column('createdAt', 'Дата создания', datetimeConverter), + new Column('updatedAt', 'Дата обновления', datetimeConverter), + ], this.diplomaTopics, true + ); + }, {fireImmediately: true}); + } + + @observable diplomaTopics: DiplomaTopic[] = []; + @observable selectedTopic: DiplomaTopic = undefined; + @observable tableDescriptor: TableDescriptor; + @observable diplomaTopicModalState = new ModalState(false, this.updateDiplomaTopics); + @observable diplomaTopicEditModalState = new ModalState(false, this.updateDiplomaTopics); + + @action.bound + updateDiplomaTopics() { + getAllDiplomaTopics().then(dt => { + runInAction(() => { + this.diplomaTopics = dt; + }) + }); + } + + @action.bound + onIconClicked(event: React.MouseEvent) { + const pd = this.diplomaTopics.find(pd => pd.id === _.toNumber(event.currentTarget.getAttribute('data-value'))); + if (pd) { + this.selectedTopic = pd; + this.diplomaTopicEditModalState.open(); + } + } +} + +@observer +export class DiplomaTopicListPage extends Page { + constructor(props: any) { + super(props); + makeObservable(this); + this.fields = new DiplomaTopicPageState(); + } + + @observable fields: DiplomaTopicPageState; + + get page() { + return <> + { + <> + { + this.fields.tableDescriptor && + } + /> + } + + { + this.fields.selectedTopic && + + } + + } + + } +} + +const AdditionalButtons = observer(({state}: {state: DiplomaTopicPageState}) => { + return ( +
+ {(UserService.isSecretary || UserService.isAdministrator) && ( + + )} +
+ ); +}); + + diff --git a/web/src/components/dictionary/DiplomaTopicModal.tsx b/web/src/components/dictionary/DiplomaTopicModal.tsx new file mode 100644 index 0000000..18d7366 --- /dev/null +++ b/web/src/components/dictionary/DiplomaTopicModal.tsx @@ -0,0 +1,180 @@ +import {action, makeObservable, observable, runInAction} from "mobx"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {ModalState} from "../../utils/modalState"; +import {observer} from "mobx-react"; +import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; +import {DropdownSelectInput, SelectInputValue, StringInput} from "../controls/ReactiveControls"; +import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {NotificationService} from "../../services/NotificationService"; +import {get, post} from "../../utils/request"; +import {Group} from "../../models/group"; +import {required, strLength, strPattern} from "../../utils/reactive/validators"; +import _ from "lodash"; +import {fullName, Participant} from "../../models/participant"; +import {datetimeConverter} from "../../utils/converters"; +import {StudentData} from "../../models/studentData"; +import {PreparationDirection} from "../../models/preparationDirection"; +import {DiplomaTopic} from "../../models/diplomaTopic"; +import {TeacherData} from "../../models/teacherData"; + +const getAllTeachers = () => { + return get("teacher-data/get-all"); +} + +const getAllPreparationDirections = () => { + return get("prep-direction/get-all"); +} + +class DiplomaTopicModalState { + constructor(modalState: ModalState, topic: DiplomaTopic) { + makeObservable(this); + this.modalState = modalState; + getAllTeachers().then(teachers => { + runInAction(() => { + this.allTeachers = teachers; + }); + }); + getAllPreparationDirections().then(preparationDirections => { + runInAction(() => { + this.allPreparationDirections = preparationDirections; + }); + }); + + if (topic) { + this.editMode = false; + this.adding = false; + this.name.setAuto(topic.name); + this.teacher.setAuto([{ + value: _.toString(topic.id), + label: topic.name, + }]); + this.preparationDirection.setAuto([{ + value: _.toString(topic.preparationDirection.id), + label: topic.preparationDirection.name, + }]); + this.topicId = topic.id; + } + } + + @observable modalState: ModalState; + @observable editMode = true; + @observable adding = true; + @observable topicId: number = undefined; + + @observable allTeachers: TeacherData[] = []; + @observable allPreparationDirections: PreparationDirection[] = []; + + @observable name = new ReactiveValue().setAuto("").addValidator(required); + @observable teacher = new ReactiveValue(); + @observable preparationDirection = new ReactiveValue(); + + anyInvalid(): boolean { + let invalid = this.name.invalid + || this.teacher.invalid + || this.preparationDirection.invalid; + + if (this.adding) { + invalid = invalid || !this.name.touched + } + + return invalid; + } + + touchAll() { + this.name.touch(); + this.teacher.touch(); + this.preparationDirection.touch(); + } + + @action.bound + create() { + if (this.anyInvalid()) { + this.touchAll(); + NotificationService.error("Пожалуйста, исправьте ошибки на форме"); + return; + } + + post("diploma-topic/save", { + id: this.topicId, + name: this.name.value, + teacher: this.teacher.value && this.teacher.value[0] + ? this.getTeacherById(_.toNumber(this.teacher.value[0].value)) + : undefined, + preparationDirection: this.preparationDirection.value && this.preparationDirection.value[0] + ? this.getPreparationDirectionById(_.toNumber(this.preparationDirection.value[0].value)) + : undefined, + }).then(() => { + if (this.adding) + NotificationService.success(`Тема ВКР ${this.name.value} успешно добавлена`); + else + NotificationService.success(`Тема ВКР ${this.name.value} успешно изменена`); + if (this.modalState) this.modalState.onApply(); + }); + + this.modalState.close(); + } + + getTeacherById(id: number): TeacherData | undefined { + return this.allTeachers.find(teacher => teacher.id === id); + } + + getPreparationDirectionById(id: number): PreparationDirection | undefined { + return this.allPreparationDirections.find(direction => direction.id === id); + } + + @action.bound + changeEditMode() { + this.editMode = !this.editMode; + } +} + +@observer +export default class DiplomaTopicModal extends ComponentContext<{ modalState: ModalState, topic?: DiplomaTopic}> { + constructor(props: any) { + super(props); + makeObservable(this); + runInAction(() => { + this.fields = new DiplomaTopicModalState(props.modalState, props.topic); + }); + } + + @observable fields: DiplomaTopicModalState; + + render() { + return ( + + + { + this.fields.editMode + ? "Редактирование темы ВКР" + : "Новая тема ВКР" + } + + + + { + return {label: fullName(value.participant), value: value.id?.toString() || ""}; + })} + value={this.fields.teacher} disabled={!this.fields.editMode}/> + { + return {label: value.name, value: value.id?.toString() || ""}; + })} + value={this.fields.preparationDirection} disabled={!this.fields.editMode}/> + + + { + !this.fields.editMode && + + } + { + this.fields.editMode && + + } + + + + ); + } +} \ No newline at end of file diff --git a/web/src/components/dictionary/PreparationDirectionList.tsx b/web/src/components/dictionary/PreparationDirectionList.tsx new file mode 100644 index 0000000..61cbf3b --- /dev/null +++ b/web/src/components/dictionary/PreparationDirectionList.tsx @@ -0,0 +1,101 @@ +import {observer} from "mobx-react"; +import {Page} from "../layout/Page"; +import {action, makeObservable, observable, reaction, runInAction} from "mobx"; +import React from "react"; +import {DataTable} from "../data-tables/DataTable"; +import {Column, TableDescriptor} from "../../utils/tables"; +import {PreparationDirection} from "../../models/preparationDirection"; +import {get} from "../../utils/request"; +import {datetimeConverter} from "../../utils/converters"; +import {Button} from "react-bootstrap"; +import {ModalState} from "../../utils/modalState"; +import PreparationDirectionModal from "./PreparationDirectionModal"; +import {UserService} from "../../services/UserService"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import _ from "lodash"; + +const getAllPreparationDirections = () => { + return get("prep-direction/get-all"); +} + +class PreparationDirectionPageState { + constructor() { + makeObservable(this); + getAllPreparationDirections().then(pd => { + runInAction(() => { + this.preparationDirections = pd; + }) + }); + + reaction(() => this.preparationDirections, () => { + this.tableDescriptor = new TableDescriptor([ + new Column('code', 'Код', undefined, (pd) => { + return ; + }), + new Column('name', 'Название'), + new Column('createdAt', 'Дата создания', datetimeConverter), + new Column('updatedAt', 'Дата обновления', datetimeConverter), + ], this.preparationDirections, true + ); + }, {fireImmediately: true}); + } + + @observable preparationDirections: PreparationDirection[] = []; + @observable tableDescriptor: TableDescriptor; + @observable preparationDirectionModalState = new ModalState(); + @observable preparationDirectionEditModalState = new ModalState(); + @observable selectedDirection: PreparationDirection; + + @action.bound + onIconClicked(event: React.MouseEvent) { + const pd = this.preparationDirections.find(pd => pd.id === _.toNumber(event.currentTarget.getAttribute('data-value'))); + if (pd) { + this.selectedDirection = pd; + this.preparationDirectionEditModalState.open(); + } + } +} + +@observer +export class PreparationDirectionListPage extends Page { + constructor(props: any) { + super(props); + makeObservable(this); + this.fields = new PreparationDirectionPageState(); + } + + @observable fields: PreparationDirectionPageState; + + get page() { + return <> + { + <> + { + this.fields.tableDescriptor && + }/> + } + + { + this.fields.selectedDirection && + + } + + } + + } +} + +const AdditionalButtons = observer(({state}: {state: PreparationDirectionPageState}) => { + return ( +
+ {(UserService.isSecretary || UserService.isAdministrator) && ( + + )} +
+ ); +}); + diff --git a/web/src/components/dictionary/PreparationDirectionModal.tsx b/web/src/components/dictionary/PreparationDirectionModal.tsx new file mode 100644 index 0000000..d4a1d80 --- /dev/null +++ b/web/src/components/dictionary/PreparationDirectionModal.tsx @@ -0,0 +1,140 @@ +import {action, makeObservable, observable, runInAction} from "mobx"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {ModalState} from "../../utils/modalState"; +import {observer} from "mobx-react"; +import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; +import {DropdownSelectInput, SelectInputValue, StringInput} from "../controls/ReactiveControls"; +import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {NotificationService} from "../../services/NotificationService"; +import {get, post} from "../../utils/request"; +import {Group} from "../../models/group"; +import {required, strLength, strPattern} from "../../utils/reactive/validators"; +import _ from "lodash"; +import {fullName, Participant} from "../../models/participant"; +import {datetimeConverter} from "../../utils/converters"; +import {StudentData} from "../../models/studentData"; +import {PreparationDirection} from "../../models/preparationDirection"; + +class PreparationDirectionModalState { + constructor(modalState: ModalState, direction?: PreparationDirection) { + makeObservable(this); + this.modalState = modalState; + if (direction) { + console.log('PreparationDirectionModalState: initializing with direction', direction); + this.name.setAuto(direction.name); + this.code.setAuto(direction.code); + this.id = direction.id; + this.editMode = true; + } else { + this.name.setAuto(""); + this.code.setAuto(""); + this.viewMode = false; + } + } + + @observable modalState: ModalState; + @observable name = new ReactiveValue().addValidator(required); + @observable code = new ReactiveValue().addValidator(required); + @observable editMode = false; + @observable viewMode = true; + @observable id: number = undefined; + + anyInvalid(): boolean { + let invalid = this.name.invalid || this.code.invalid; + if (!this.editMode) { + invalid = invalid || !this.name.touched || !this.code.touched; + } + return invalid; + } + + touchAll() { + this.name.touch(); + this.code.touch(); + } + + @action.bound + save() { + if (this.anyInvalid()) { + this.touchAll(); + NotificationService.error("Пожалуйста, исправьте ошибки на форме"); + return; + } + + post("prep-direction/save", { + id: this.id, + name: this.name.value, + code: this.code.value + }).then(() => { + if (this.editMode) { + NotificationService.success(`Направление подготовки ${this.code.value} успешно обновлено`); + } else { + NotificationService.success(`Направление подготовки ${this.code.value} успешно добавлено`); + } + }); + + this.modalState.close(); + if (this.editMode) { + this.toView(); + } + } + + @action.bound + toEdit() { + this.viewMode = false; + } + + @action.bound + toView() { + this.viewMode = true; + } +} + +@observer +export default class PreparationDirectionModal extends ComponentContext<{ modalState: ModalState, direction?: PreparationDirection }> { + constructor(props: any) { + super(props); + makeObservable(this); + runInAction(() => { + this.fields = new PreparationDirectionModalState(props.modalState, props.direction); + }); + } + + componentDidUpdate(prevProps: Readonly<{ modalState: ModalState; direction?: PreparationDirection }>) { + if (this.props.direction !== prevProps.direction) { + runInAction(() => { + this.fields = new PreparationDirectionModalState(this.props.modalState, this.props.direction); + }); + } + } + + @observable fields: PreparationDirectionModalState; + + render() { + return ( + + + { + this.fields.editMode + ? "Редактирование направления подготовки " + this.fields.code.value + : "Добавление направления подготовки" + } + + + + + + + { + !this.fields.viewMode && + + } + { + this.fields.editMode && this.fields.viewMode && + + } + + + + ); + } +} \ No newline at end of file diff --git a/web/src/components/group/AddGroupModal.tsx b/web/src/components/group/AddGroupModal.tsx deleted file mode 100644 index b83ce98..0000000 --- a/web/src/components/group/AddGroupModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import {ComponentContext} from "../../utils/ComponentContext"; -import {observer} from "mobx-react"; -import {ModalState} from "../../utils/modalState"; -import {action, computed, makeObservable, observable} from "mobx"; -import {ReactiveValue} from "../../utils/reactive/reactiveValue"; -import {required, strLength, strPattern} from "../../utils/reactive/validators"; -import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; -import {StringInput} from "../custom/controls/ReactiveControls"; -import {post} from "../../utils/request"; - -export interface CreateGroupModalProps { - modalState: ModalState; -} - -@observer -export class AddGroupModal extends ComponentContext { - constructor(props: any) { - super(props); - makeObservable(this); - } - - @observable name = new ReactiveValue() - .addValidator(required) - .addValidator(strLength(3, 50)) - .addInputRestriction(strPattern(/^[а-яА-ЯёЁ0-9_-]*$/, "Имя группы должно содержать только русские буквы, цифры и символы _-")); - - @action.bound - creationRequest() { - post('/group/create-group', {name: this.name.value}, false).then(() => { - this.notificationStore.success(`Группа ${this.name.value} создана`); - }).finally(() => { - this.props.modalState.close(); - }); - } - - @computed - get formInvalid() { - return this.name.invalid || !this.name.touched; - } - - render() { - return - - Добавление группы - - - - - - - - - - } -} \ No newline at end of file diff --git a/web/src/components/group/GroupListPage.tsx b/web/src/components/group/GroupListPage.tsx index 421811f..0a8036f 100644 --- a/web/src/components/group/GroupListPage.tsx +++ b/web/src/components/group/GroupListPage.tsx @@ -1,203 +1,92 @@ import {observer} from "mobx-react"; -import {Page} from "../custom/layout/Page"; -import {action, computed, makeObservable, observable, reaction, runInAction} from "mobx"; -import {Column, TableDescriptor} from "../../utils/tables"; +import {Page} from "../layout/Page"; +import {action, makeObservable, observable, reaction, runInAction} from "mobx"; import {get} from "../../utils/request"; -import {DataTable} from "../custom/DataTable"; -import {IGroup} from "../../models/IGroup"; -import {Component} from "react"; -import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; -import {StringInput} from "../custom/controls/ReactiveControls"; -import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {Group} from "../../models/group"; +import React from "react"; import {ModalState} from "../../utils/modalState"; +import {ThinkService} from "../../services/ThinkService"; +import {DataTable} from "../data-tables/DataTable"; +import {Column, TableDescriptor} from "../../utils/tables"; +import {datetimeConverter} from "../../utils/converters"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {ComponentContext} from "../../utils/ComponentContext"; -import {required, strLength, strPattern} from "../../utils/reactive/validators"; +import _ from "lodash"; +import {StudentData} from "../../models/studentData"; +import GroupProfileModal from "./GroupProfileModal"; +import {GroupListFilterModal} from "./GroupTableFilterModal"; + +class GroupListPageState { + constructor() { + makeObservable(this); + this.updateGroups(); + reaction(() => this.groups, () => { + this.tableDescriptor = new TableDescriptor([ + new Column('name', 'Название', undefined, (group) => { + return ; + }), + new Column('students', 'Студентов', v => v ? v.length : 0), + new Column('createdAt', 'Дата создания', datetimeConverter), + new Column('updatedAt', 'Дата обновления', datetimeConverter), + ], this.groups, true, this.filters + ); + }, {fireImmediately: true}); + } + + @observable filterModalState = new ModalState(); + @observable groupModalState = new ModalState(); + @observable groups: Group[] = []; + @observable filters: ((group: Group) => boolean)[] = []; + + @observable selectedGroup: Group = null; + @observable tableDescriptor: TableDescriptor; + + @action.bound + updateGroups() { + ThinkService.think(); + get('/group/get-all-groups').then((groups) => { + runInAction(() => { + this.groups = groups; + }); + }).finally(() => { + ThinkService.completeAll(); + }); + } + + @action.bound + openModal(event: React.MouseEvent) { + const group = this.groups.find(g => g.id === _.toNumber(event.currentTarget.getAttribute('data-value'))); + if (group) { + this.selectedGroup = group; + this.groupModalState.open(); + } + } +} @observer export class GroupListPage extends Page { constructor(props: {}) { super(props); makeObservable(this); - reaction(() => this.groups, () => { - this.tableDescriptor = new TableDescriptor(this.groupColumns, this.groups); - }); + this.fields = new GroupListPageState(); } - componentDidMount() { - this.requestGroups(); - } - - componentDidUpdate() { - runInAction(() => { - this.isAdministrator = this.userStore.isAdministrator; - }); - } - - @observable filterModalState = new ModalState(); - @observable editModalState = new ModalState(); - @observable currentGroup: IGroup; - - @observable groups: IGroup[]; - @observable tableDescriptor: TableDescriptor; - @observable isAdministrator: boolean; - - groupColumns = [ - new Column('name', 'Название', x => x, (grp) => { - return this.isAdministrator && { - this.openEditModal(grp) - }} icon={'pen-to-square'}/> - } - ), - new Column('curatorName', 'Куратор', (value: string) => value ?? 'Не назначен'), - ]; - - @action.bound - requestGroups() { - this.thinkStore.think(); - get('/group/get-all-groups').then(groups => { - runInAction(() => { - this.groups = groups; - }); - }).finally(() => { - this.thinkStore.completeOne(); - }); - } - - @action.bound - openEditModal(group: IGroup) { - runInAction(() => { - this.currentGroup = group; - this.editModalState.open(); - }); - } + @observable fields: GroupListPageState; get page() { return <> { - this.tableDescriptor && <> - - { - this.currentGroup && - + this.fields.tableDescriptor && + } + { + this.fields.selectedGroup && + + } + } } } - -interface GroupListFilterProps { - modalState: ModalState; - filters: ((group: IGroup) => boolean)[]; -} - -@observer -class GroupListFilterModal extends Component { - constructor(props: GroupListFilterProps) { - super(props); - makeObservable(this); - - runInAction(() => { - this.filters.push(this.nameFilter); - this.filters.push(this.curatorFilter); - }); - } - - @observable filters = this.props.filters; - @observable modalState = this.props.modalState; - @observable nameField = new ReactiveValue().syncWithParam('name'); - @observable curatorField = new ReactiveValue().syncWithParam('curator'); - - @observable nameFilter = (group: IGroup) => { - if (!this.nameField.value) return true; - return group.name?.includes(this.nameField.value) - }; - - @observable curatorFilter = (group: IGroup) => { - if (!this.curatorField.value) return true; - return group.curatorName?.includes(this.curatorField.value) - }; - - @action.bound - reset() { - this.nameField.set(""); - this.curatorField.set(""); - } - - render() { - return - - Фильтр - - - - - - - - - - - } -} - -interface EditGroupModalProps { - modalState: ModalState; - group: IGroup -} - -@observer -class EditGroupModal extends ComponentContext { - constructor(props: EditGroupModalProps) { - super(props); - makeObservable(this); - } - - componentDidUpdate() { - runInAction(() => { - this.group = this.props.group; - this.nameField.setAuto(this.group.name); - this.curatorField.setAuto(this.group.curatorName ?? ''); - }); - } - - @observable group = this.props.group; - @observable modalState = this.props.modalState; - - @observable nameField = new ReactiveValue().setAuto(this.group.name) - .addValidator(required) - .addValidator(strLength(3, 50)) - .addInputRestriction(strPattern(/^[а-яА-ЯёЁ0-9_-]*$/, "Имя группы должно содержать только русские буквы, цифры и символы _-")); - @observable curatorField = new ReactiveValue().setAuto(this.group.curatorName ?? ''); - - @action.bound - save() { - this.notificationStore.warn('Приносим извинения за неудобства', 'Сохранение не реализовано'); - this.modalState.close(); - } - - @computed - get formInvalid() { - return this.nameField.invalid || !this.nameField.touched - || this.curatorField.invalid || !this.curatorField.touched; - } - - render() { - return - - Редактирование группы {this.group.name} - - - - - - - - - - - } -} diff --git a/web/src/components/group/GroupProfileModal.tsx b/web/src/components/group/GroupProfileModal.tsx new file mode 100644 index 0000000..004991d --- /dev/null +++ b/web/src/components/group/GroupProfileModal.tsx @@ -0,0 +1,230 @@ +import {action, computed, makeObservable, observable, runInAction} from "mobx"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {ModalState} from "../../utils/modalState"; +import {observer} from "mobx-react"; +import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; +import {DropdownSelectInput, SelectInputValue, StringInput} from "../controls/ReactiveControls"; +import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {NotificationService} from "../../services/NotificationService"; +import {get, post} from "../../utils/request"; +import {Group} from "../../models/group"; +import {required, strLength, strPattern} from "../../utils/reactive/validators"; +import _ from "lodash"; +import {fullName} from "../../models/participant"; +import {datetimeConverter} from "../../utils/converters"; +import {StudentData} from "../../models/studentData"; +import {UserService} from "../../services/UserService"; +import PreparationDirectionModal from "../dictionary/PreparationDirectionModal"; +import {PreparationDirection} from "../../models/preparationDirection"; + +const mapStudent = (student: StudentData) => { + return {value: _.toString(student.id), label: fullName(student.participant)}; +} + +const mapDirOfPrep = (prep: PreparationDirection) => { + return {value: _.toString(prep.id), label: prep.code} +} + +const getAllDirOfPrep = () => { + return get("prep-direction/get-all"); +} + +class GroupProfileState { + constructor(props: GroupProfileModalProps) { + makeObservable(this); + this.modalState = props.modalState; + this.viewMode = !props.creation; + + this.group = props.group ?? {name: ""}; + this.groupName.setAuto(this.group.name); + this.createdAt.setAuto(datetimeConverter(this.group.createdAt)); + this.updatedAt.setAuto(datetimeConverter(this.group.updatedAt)); + this.students.setAuto(this.group.students?.map(mapStudent)); + if (this.group.preparationDirection) { + this.directionOfPreparation.setAuto([mapDirOfPrep(this.group.preparationDirection)]); + } + + get('student/all-without-group').then((students) => { + runInAction(() => { + this.allStudents = students; + }); + }); + + getAllDirOfPrep().then(dop => { + runInAction(() => { + this.allDirOfPrep = dop; + }); + }); + } + + @observable viewMode: boolean; + @observable editMode: boolean = false; + @observable group: Group; + @observable modalState: ModalState; + + @observable allDirOfPrep: PreparationDirection[] = []; + @observable allStudents: StudentData[] = []; + + @observable groupName = new ReactiveValue().addValidator(required).addValidator(strLength(3, 50)) + .addInputRestriction(strPattern(/^[а-яА-ЯёЁ0-9_-]*$/, "Имя группы должно содержать только русские буквы, цифры и символы _-")); + @observable students = new ReactiveValue(); + @observable directionOfPreparation = new ReactiveValue().addValidator(required); + + @observable createdAt = new ReactiveValue(); + @observable updatedAt = new ReactiveValue(); + + @action.bound + closeModal() { + this.modalState.close(); + this.setViewMode(); + } + + anyInvalid(): boolean { + let invalid = this.groupName.invalid || this.students.invalid || this.directionOfPreparation.invalid; + + if (!this.editMode) { + invalid = invalid || !this.groupName.touched || !this.directionOfPreparation.touched; + } + + return invalid; + } + + touchAll() { + this.groupName.touch(); + this.students.touch(); + this.directionOfPreparation.touch(); + } + + @action.bound + save() { + if (this.anyInvalid()) { + this.touchAll(); + NotificationService.error("Пожалуйста, исправьте ошибки на форме"); + return; + } + + const studentIds: StudentData[] = this.students.value.map((s) => ({ + id: _.toNumber(s.value) + })); + + post("group/save", { + id: this.group.id ? this.group.id : undefined, + name: this.groupName.value, + students: studentIds, + preparationDirection: this.directionOfPreparation.value && this.directionOfPreparation.value[0] + ? {id: this.directionOfPreparation.value[0].value} + : undefined, + }).then(() => { + if (this.editMode) + NotificationService.success(`Профиль группы ${this.groupName.value} успешно обновлен`); + else + NotificationService.success(`Профиль группы ${this.groupName.value} успешно создан`); + }).finally(() => { + this.closeModal(); + }); + } + + @action.bound + setEditMode() { + this.editMode = true; + this.viewMode = false; + } + + @action.bound + setViewMode() { + this.editMode = false; + this.viewMode = true; + } + + @computed + get canManipulateGroups() { + return UserService.isSecretary || UserService.isAdministrator; + } + + @action.bound + deleteGroup() { + post(`group/delete?id=${this.group.id}`).then(() => { + NotificationService.success(`Группа ${this.groupName.value} удалена`); + }); + + this.closeModal(); + } +} + +interface GroupProfileModalProps { + creation?: boolean; + modalState: ModalState; + group?: Group; +} + +@observer +export default class GroupProfileModal extends ComponentContext { + constructor(props: GroupProfileModalProps) { + super(props); + makeObservable(this); + runInAction(() => { + this.fields = new GroupProfileState(this.props); + }); + } + + componentDidUpdate(prevProps: Readonly) { + if (prevProps.group !== this.props.group) { + runInAction(() => { + this.fields = new GroupProfileState(this.props); + }); + } + } + + @observable fields: GroupProfileState; + + render() { + return ( + + + + { + this.props.creation + ? `Создание группы` + : this.fields.editMode + ? `Редактирование профиля группы ${this.fields.groupName.value}` + : `Профиль группы ${this.fields.groupName.value}` + } + + + + + + + { + this.fields.viewMode && + <> + + + + } + + + { + this.props.creation && this.fields.canManipulateGroups && + + } + { + this.fields.editMode && this.fields.canManipulateGroups && + + } + { + !this.props.creation && !this.fields.editMode && this.props.group && this.fields.canManipulateGroups && + + } + { + !this.props.creation && this.props.group && this.fields.canManipulateGroups && !this.fields.editMode && + + } + + + + ); + } +} \ No newline at end of file diff --git a/web/src/components/group/GroupTableFilterModal.tsx b/web/src/components/group/GroupTableFilterModal.tsx new file mode 100644 index 0000000..b15821e --- /dev/null +++ b/web/src/components/group/GroupTableFilterModal.tsx @@ -0,0 +1,54 @@ +import {ModalState} from "../../utils/modalState"; +import {Group} from "../../models/group"; +import {observer} from "mobx-react"; +import React, {Component} from "react"; +import {action, makeObservable, observable, runInAction} from "mobx"; +import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; +import {StringInput} from "../controls/ReactiveControls"; + +export interface GroupListFilterProps { + modalState: ModalState; + filters: ((group: Group) => boolean)[]; +} + +@observer +export class GroupListFilterModal extends Component { + constructor(props: GroupListFilterProps) { + super(props); + makeObservable(this); + + runInAction(() => { + this.filters.push(this.nameFilter); + }); + } + + @observable filters = this.props.filters; + @observable modalState = this.props.modalState; + @observable nameField = new ReactiveValue().syncWithParam('name'); + + @observable nameFilter = (group: Group) => { + if (!this.nameField.value) return true; + return group.name?.includes(this.nameField.value) + }; + + @action.bound + reset() { + this.nameField.set(""); + } + + render() { + return + + Фильтр + + + + + + + + + + } +} diff --git a/web/src/components/custom/layout/Error.tsx b/web/src/components/layout/Error.tsx similarity index 100% rename from web/src/components/custom/layout/Error.tsx rename to web/src/components/layout/Error.tsx diff --git a/web/src/components/custom/layout/Footer.tsx b/web/src/components/layout/Footer.tsx similarity index 75% rename from web/src/components/custom/layout/Footer.tsx rename to web/src/components/layout/Footer.tsx index e6b2bbc..ef03e5b 100644 --- a/web/src/components/custom/layout/Footer.tsx +++ b/web/src/components/layout/Footer.tsx @@ -1,9 +1,11 @@ -import {ComponentContext} from "../../../utils/ComponentContext"; +import {ComponentContext} from "../../utils/ComponentContext"; import {observer} from "mobx-react"; import {makeObservable} from "mobx"; import {Container, Nav, Navbar, NavbarText, NavLink} from "react-bootstrap"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {findIconDefinition} from "@fortawesome/fontawesome-svg-core"; +import {ThinkService} from "../../services/ThinkService"; +import {SysInfoService} from "../../services/SysInfoService"; @observer export class Footer extends ComponentContext { @@ -20,12 +22,12 @@ export class Footer extends ComponentContext {
Thesis Defence Management System — { - this.thinkStore.isThinking('updateVersion') && + ThinkService.isThinking('updateVersion') && } { - !this.thinkStore.isThinking('updateVersion') && - {this.sysInfoStore.version} + !ThinkService.isThinking('updateVersion') && + {SysInfoService.version} }
diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx new file mode 100644 index 0000000..9e33dad --- /dev/null +++ b/web/src/components/layout/Header.tsx @@ -0,0 +1,118 @@ +import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap"; +import {RouterLink} from "mobx-state-router"; +import {observer} from "mobx-react"; +import {post} from "../../utils/request"; +import {UserLoginModal} from "../user/UserLoginModal"; +import {ModalState} from "../../utils/modalState"; +import {action, makeObservable, observable} from "mobx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {NotificationService} from "../../services/NotificationService"; +import {ThinkService} from "../../services/ThinkService"; +import ParticipantProfileModal from "../participant/ParticipantProfileModal"; +import GroupProfileModal from "../group/GroupProfileModal"; +import {fullName} from "../../models/participant"; +import {UserService} from "../../services/UserService"; + +@observer +export class Header extends ComponentContext { + constructor(props: any) { + super(props); + makeObservable(this); + } + + @observable loginModalState = new ModalState(); + @observable addGroupModalState = new ModalState(); + @observable userRegistrationModalState = new ModalState(); + + render() { + let userThink = ThinkService.isThinking('updateCurrentUser'); + + return <> +
+ + + + TDMS + + + + + +
+ + + + + } +} + +@observer +class AuthenticatedItems extends ComponentContext { + constructor(props: any) { + super(props); + makeObservable(this); + } + + @observable userParticipantProfileModalState = new ModalState(); + + @action.bound + logout() { + post('user/logout').then(() => { + UserService.updateCurrentUser(); + this.routerStore.goTo('home').then(); + NotificationService.success('Вы успешно вышли из системы', 'Выход'); + }); + } + + render() { + return <> + Пользователь: + + Мой профиль + + Выйти + + + + + } +} diff --git a/web/src/components/layout/Home.tsx b/web/src/components/layout/Home.tsx new file mode 100644 index 0000000..4dd0d64 --- /dev/null +++ b/web/src/components/layout/Home.tsx @@ -0,0 +1,18 @@ +import {Page} from "./Page"; +import {observer} from "mobx-react"; +import {makeObservable} from "mobx"; + +@observer +export class Home extends Page { + + constructor(props: any) { + super(props); + makeObservable(this); + } + + get page() { + return <> +

home

+ + } +} diff --git a/web/src/components/custom/layout/Page.tsx b/web/src/components/layout/Page.tsx similarity index 82% rename from web/src/components/custom/layout/Page.tsx rename to web/src/components/layout/Page.tsx index d839e29..0d5b6b8 100644 --- a/web/src/components/custom/layout/Page.tsx +++ b/web/src/components/layout/Page.tsx @@ -2,9 +2,10 @@ import {ReactNode} from "react"; import {Container} from "react-bootstrap"; import {Header} from "./Header"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {ComponentContext} from "../../../utils/ComponentContext"; -import {NotificationContainer} from "../../NotificationContainer"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {NotificationContainer} from "../NotificationContainer"; import {Footer} from "./Footer"; +import {ThinkService} from "../../services/ThinkService"; export abstract class Page extends ComponentContext { get page(): ReactNode { @@ -15,7 +16,7 @@ export abstract class Page extends ComponentContext { } render() { - const thinking = this.thinkStore.isThinking(); + const thinking = ThinkService.isThinking(); return <>
diff --git a/web/src/components/participant/ParticipantListPage.tsx b/web/src/components/participant/ParticipantListPage.tsx new file mode 100644 index 0000000..a2b3255 --- /dev/null +++ b/web/src/components/participant/ParticipantListPage.tsx @@ -0,0 +1,121 @@ +import {observer} from "mobx-react"; +import {action, makeObservable, observable, reaction, runInAction} from "mobx"; +import {Page} from "../layout/Page"; +import {get} from "../../utils/request"; +import {Column, TableDescriptor} from "../../utils/tables"; +import {ThinkService} from "../../services/ThinkService"; +import {fullName, Participant} from "../../models/participant"; +import {Role} from "../../models/roles"; +import {datetimeConverter} from "../../utils/converters"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {DataTable} from "../data-tables/DataTable"; +import ParticipantProfileModal from "./ParticipantProfileModal"; +import {ModalState} from "../../utils/modalState"; +import React from "react"; +import _ from "lodash"; + +class ParticipantListPageState { + constructor() { + makeObservable(this); + this.updateParticipants(); + reaction(() => this.participants, () => { + this.tableDescriptor = new TableDescriptor(this.tableColumns, this.participants.map(p => new ParticipantRecord(p))); + }, {fireImmediately: true}); + } + + @action.bound + updateParticipants() { + ThinkService.think(); + get('participant/get-all-participants').then((participants) => { + runInAction(() => { + this.participants = participants; + }); + }).finally(() => { + ThinkService.completeAll(); + }); + } + + @action.bound + onIconClicked(event: React.MouseEvent) { + const participant = this.participants.find(p => p.id === _.toNumber(event.currentTarget.getAttribute('data-value'))); + if (participant) { + this.selectedParticipant = participant; + this.participantModalState.open(); + } + } + + @observable participantModalState = new ModalState(); + + tableColumns = [ + new Column('fullName', 'Полное имя', undefined, (partic) => { + return ; + }), + new Column('roles', 'Роли', (value) => { + return value.length + ? value.map(role => {role.name}).reduce((prev, curr) => <>{prev}, {curr}) + : Нет ролей; + } + ), + new Column('email', 'Email'), + new Column('phone', 'Телефон'), + new Column('createdAt', 'Дата создания', (value: string) => value ? datetimeConverter(value) : 'Не создавалось'), + new Column('updatedAt', 'Дата обновления', (value: string) => value ? datetimeConverter(value) : 'Не обновлялось'), + ]; + + @observable participants: Participant[] = []; + @observable selectedParticipant?: Participant; + @observable tableDescriptor?: TableDescriptor; +} + +class ParticipantRecord { + original: Participant; + firstName: string; + lastName: string; + middleName: string; + fullName: string; + roles: Role[]; + email: string; + phone: string; + createdAt: string; + updatedAt: string; + + constructor(participant: Participant) { + this.original = participant; + this.firstName = participant.firstName; + this.lastName = participant.lastName; + this.middleName = participant.middleName; + this.fullName = fullName(participant); + this.roles = participant.roles; + this.email = participant.email; + this.phone = participant.numberPhone; + this.createdAt = participant.createdAt; + this.updatedAt = participant.updatedAt; + } +} + +@observer +export class ParticipantListPage extends Page { + constructor(props: {}) { + super(props); + makeObservable(this); + } + + state = new ParticipantListPageState(); + + componentDidMount() { + this.state.updateParticipants(); + } + + get page() { + return <> + { + this.state.tableDescriptor && + + } + { + this.state.selectedParticipant && + + } + + } +} \ No newline at end of file diff --git a/web/src/components/participant/ParticipantProfileModal.tsx b/web/src/components/participant/ParticipantProfileModal.tsx new file mode 100644 index 0000000..bf6f866 --- /dev/null +++ b/web/src/components/participant/ParticipantProfileModal.tsx @@ -0,0 +1,478 @@ +import {action, computed, makeObservable, observable, reaction} from "mobx"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {ModalState} from "../../utils/modalState"; +import {fullName, Participant} from "../../models/participant"; +import {observer} from "mobx-react"; +import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Tab, Tabs} from "react-bootstrap"; +import {BooleanInput, DropdownSelectInput, PasswordInput, SelectInputValue, StringInput} from "../controls/ReactiveControls"; +import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {getRoleArrayByCode, isRolePresent, Roles, RolesArray} from "../../models/roles"; +import {datetimeConverter} from "../../utils/converters"; +import { + email, + emailChars, + loginChars, + loginLength, + loginMaxLength, + nameChars, + nameLength, + password, + passwordChars, + passwordLength, + passwordMaxLength, + phone, + phoneChars, + required +} from "../../utils/reactive/validators"; +import {NotificationService} from "../../services/NotificationService"; +import {get, post} from "../../utils/request"; +import {ParticipantSaveDTO} from "../../models/registration"; +import {Group} from "../../models/group"; +import _ from "lodash"; +import {StudentData} from "../../models/studentData"; +import {UserService} from "../../services/UserService"; +import {RouterService} from "../../services/RouterService"; +import { TeacherData } from "../../models/teacherData"; + +const RolesAsSelectValues = RolesArray.map(role => { + return { + value: role.code, + label: role.name + } as SelectInputValue; +}).sort((a, b) => a.label.localeCompare(b.label)); + +let allGroups: Group[] = []; +let allTeachers: TeacherData[] = []; + +const getAllTeachers = () => { + return get("teacher-data/get-all"); +} + +class ParticipantProfileState { + constructor(props: ParticipantProfileModalProps) { + makeObservable(this); + this.modalState = props.modalState; + this.participant = !props.registration ? props.participant : { + id: undefined, + firstName: "", + lastName: "", + middleName: "", + numberPhone: "", + user: undefined, + email: "", + roles: [], + updatedAt: "", + createdAt: "", + }; + this.fields = new ReactiveFields(this.participant, this); + this.viewMode = !props.registration; + + get("/group/get-all-groups").then(data => { + allGroups = data; + }); + + getAllTeachers().then(data => {allTeachers = data;}); + } + + @observable viewMode: boolean; + @observable editMode: boolean; + @observable participant: Participant; + @observable fields: ReactiveFields; + @observable modalState: ModalState; + + @action.bound + close() { + this.modalState.close(); + this.editMode = false; + this.viewMode = true; + } + + @action.bound + save() { + if (this.fields.anyInvalid()) { + this.fields.touchAll(); + NotificationService.error("Пожалуйста, исправьте ошибки на форме"); + return; + } + + let data: ParticipantSaveDTO = { + id: this.participant ? this.participant.id : undefined, + firstName: this.fields.firstName.value, + lastName: this.fields.lastName.value, + middleName: this.fields.middleName.value, + email: this.fields.email.value, + numberPhone: this.fields.phone.value, + authorities: UserService.isSecretary || UserService.isAdministrator ? this.fields.roles.value.map(role => role.value) : undefined, + userData: this.fields.userPresent.value ? { + login: this.fields.userLogin.touched && !_.isEmpty(this.fields.userLogin.value) ? this.fields.userLogin.value : undefined, + password: this.fields.userPassword.touched && !_.isEmpty(this.fields.userPassword.value) ? this.fields.userPassword.value : undefined, + } : undefined, + studentData: this.fields.studentPresent.value ? { + groupId: _.toNumber(this.fields.studentGroup.value[0]?.value), + curatorId: _.toNumber(this.fields.studentCurator.value[0]?.value), + } : undefined, + teacherData: this.fields.teacherPresent.value ? { + degree: this.fields.teacherDegree.value, + } : undefined, + } + + post("participant/save", data).then(() => { + if (this.editMode) { + NotificationService.success(`Профиль участника успешно обновлен`); + } else { + NotificationService.success(`Профиль участника успешно зарегистрирован`); + } + + if (this.isOwnRecord && this.credentialsChanged) { + NotificationService.success("Ваш профиль был обновлен, пожалуйста, перезайдите в систему"); + UserService.updateCurrentUser(() => { + RouterService.redirect('home'); + }); + } + }).finally(() => { + this.modalState.close(); + if (this.editMode) { + this.exitEditMode(); + } + }); + } + + @computed + get canChangeRoles(): boolean { + return UserService.isAdministrator || UserService.isSecretary; + } + + @computed + get canViewUserTab(): boolean { + return UserService.isAdministrator || UserService.isSecretary || this.isOwnRecord; + } + + @computed + get isOwnRecord(): boolean { + return this.participant?.id === UserService.user?.participant?.id; + } + + @computed + get credentialsChanged(): boolean { + return !_.isEmpty(this.fields.userPassword.value) + || (!_.isEmpty(this.fields.userLogin.value) && this.fields.userLogin.value !== this.participant?.user?.login); + } + + @action.bound + enterEditMode() { + this.viewMode = false; + this.editMode = true; + } + + @action.bound + exitEditMode() { + this.viewMode = true; + this.editMode = false; + } + + @action.bound + delete() { + post("participant/delete", {id: this.participant.id}).then(() => { + console.log("participant/delete", this.participant); + NotificationService.success(`Профиль участника успешно удален`); + if (this.isOwnRecord && this.credentialsChanged) { + NotificationService.success("Ваш профиль был обновлен, пожалуйста, перезайдите в систему"); + UserService.updateCurrentUser(() => { + RouterService.redirect('home'); + }); + } + }).finally(() => { + this.modalState.close(); + this.exitEditMode(); + }); + } +} + +class ReactiveFields { + constructor(participant: Participant, state: ParticipantProfileState) { + makeObservable(this); + this.state = state; + this.firstName.setAuto(participant.firstName); + this.lastName.setAuto(participant.lastName); + this.middleName.setAuto(participant.middleName); + this.email.setAuto(participant.email); + this.phone.setAuto(participant.numberPhone); + this.roles.setAuto(participant.roles.map(role => { + return { + value: role.code, + label: role.name + }; + })); + this.createdAt.setAuto(datetimeConverter(participant.createdAt)); + this.updatedAt.setAuto(datetimeConverter(participant.updatedAt)); + + this.userPresent.setAuto(participant.user != null); + if (this.userPresent.value) { + this.userLogin.setAuto(participant.user?.login); + this.userCreatedAt.setAuto(datetimeConverter(participant.user?.createdAt)); + this.userUpdatedAt.setAuto(datetimeConverter(participant.user?.updatedAt)); + this.userPassword.setAuto(''); + } + + reaction(() => this.roles.value, () => { + let rolesArray = getRoleArrayByCode(this.roles.value.map( + role => ({code: role.value} as { + code: string + } + ))); + this.studentPresent.setAuto(isRolePresent(Roles.STUDENT.code, rolesArray)); + this.teacherPresent.setAuto(isRolePresent(Roles.TEACHER.code, rolesArray)); + }, {fireImmediately: true}); + + if (this.studentPresent.value) { + get('/student/by-partic-id', {id: participant.id}).then(data => { + if (data.group) { + this.studentGroup.setAuto([{ + value: _.toString(data.group.id), + label: allGroups.find(group => group.id === data.group.id)?.name + } as SelectInputValue]); + } + + if (data.curator) { + this.studentCurator.setAuto([{ + value: _.toString(data.curator.id), + label: fullName(allTeachers.find(t => t.id === data.curator.id)?.participant) + }]); + } + + this.studentDiplomaTopic.setAuto(data.diplomaTopic?.name); + }); + } + + if (this.teacherPresent.value) { + get('teacher-data/by-partic-id', {id: participant.id}).then(data => { + this.teacherDegree.setAuto(data.degree); + }); + } + } + + @observable state : ParticipantProfileState; + + /* PARTICIPANT */ + @observable firstName = new ReactiveValue().addValidator(required).addValidator(nameLength).addInputRestriction(nameChars); + @observable lastName = new ReactiveValue().addValidator(required).addValidator(nameLength).addInputRestriction(nameChars); + @observable middleName = new ReactiveValue().addInputRestriction(nameChars); + @observable email = new ReactiveValue().addValidator(required).addValidator(email).addInputRestriction(emailChars); + @observable phone = new ReactiveValue().addValidator(required).addValidator(phone).addInputRestriction(phoneChars); + @observable roles = new ReactiveValue(); + @observable createdAt = new ReactiveValue(); + @observable updatedAt = new ReactiveValue(); + + /* USER */ + @observable userPresent = new ReactiveValue(); + @observable userLogin = new ReactiveValue().addValidator((value, field) => { + if (this.userPresent.value && !this.state.editMode) + return required(value, field); + }).addValidator((value, field) => { + if (this.userPresent.value) + return loginLength(value, field); + }).addInputRestriction(loginChars).addInputRestriction(loginMaxLength); + @observable userPassword = new ReactiveValue() + .addValidator((v, f) => { + if (this.userPresent.value && !this.state.editMode) + return required(v, f); + }).addValidator((v, f) => { + if (this.userPresent.value && (!this.state.editMode || !_.isEmpty(this.userPassword.value))) + return password(v, f); + }).addValidator((v, f) => { + if (this.userPresent.value && (!this.state.editMode || !_.isEmpty(this.userPassword.value))) + return passwordLength(v, f); + }).addInputRestriction(passwordChars).addInputRestriction(passwordMaxLength); + @observable userCreatedAt = new ReactiveValue(); + @observable userUpdatedAt = new ReactiveValue(); + + /* STUDENT */ + @observable studentPresent = new ReactiveValue(); + @observable studentGroup = new ReactiveValue(); + @observable studentCurator = new ReactiveValue(); + @observable studentDiplomaTopic = new ReactiveValue(); + + /* TEACHER */ + @observable teacherPresent = new ReactiveValue(); + @observable teacherDegree = new ReactiveValue().addValidator(required); + + anyInvalid(): boolean { + let reqInvalid = this.firstName.invalid + || this.lastName.invalid || this.middleName.invalid + || this.email.invalid || this.email.invalid + || this.phone.invalid || this.roles.invalid; + + if (this.userPresent.value) { + reqInvalid = reqInvalid || this.userLogin.invalid || this.userPassword.invalid; + } + + if (this.studentPresent.value) { + reqInvalid = reqInvalid || this.studentGroup.invalid || this.studentCurator.invalid; + } + + if (this.teacherPresent.value) { + reqInvalid = reqInvalid || this.teacherDegree.invalid; + } + + if (!this.state.editMode) { + reqInvalid = reqInvalid || !this.firstName.touched || !this.lastName.touched; + reqInvalid = reqInvalid || !this.email.touched || !this.phone.touched; + if (this.userPresent.value) { + reqInvalid = reqInvalid || !this.userLogin.touched || !this.userPassword.touched; + } + } + + return reqInvalid; + } + + touchAll() { + this.firstName.touch(); + this.lastName.touch(); + this.middleName.touch(); + this.email.touch(); + this.phone.touch(); + this.roles.touch(); + + if (this.userPresent.value) { + this.userLogin.touch(); + this.userPassword.touch(); + } + + if (this.studentPresent.value) { + this.studentGroup.touch(); + this.studentCurator.touch(); + } + + if (this.teacherPresent.value) { + this.teacherDegree.touch(); + } + } +} + +interface ParticipantProfileModalProps { + registration?: boolean; + modalState: ModalState; + participant?: Participant; +} + +@observer +export default class ParticipantProfileModal extends ComponentContext { + constructor(props: ParticipantProfileModalProps) { + super(props); + makeObservable(this); + this.setState(new ParticipantProfileState(this.props)); + } + + componentDidUpdate(prevProps: Readonly) { + if (prevProps.participant !== this.props.participant) { + this.setState(new ParticipantProfileState(this.props)); + } + } + + @observable myState: ParticipantProfileState; + + @action.bound + setState(state: ParticipantProfileState) { + this.myState = state; + } + + render() { + return ( + + + + { + this.props.registration + ? "Регистрация участника" + : this.myState.editMode + ? `Редактирование профиля участника ${fullName(this.myState.participant)}` + : `Профиль участника ${fullName(this.myState.participant)}` + } + + + + + + + + + + + + { + this.myState.viewMode && + <> + + + + } + + { + this.myState.canViewUserTab && + + + { + this.myState.fields.userPresent.value && + <> + + { + (this.props.registration || this.myState.editMode) && + + } + { + this.myState.viewMode && + <> + + + + } + + } + + } + { + this.myState.fields.studentPresent.value && + + { + return {value: _.toString(value.id), label: value.name} as SelectInputValue; + })} label={"Группа"} value={this.myState.fields.studentGroup} filtering singleSelect disabled={this.myState.viewMode}/> + { + return {value: _.toString(value.id), label: fullName(value.participant)} as SelectInputValue; + })} label={"Куратор"} value={this.myState.fields.studentCurator} filtering singleSelect disabled={this.myState.viewMode}/> + + + } + { + this.myState.fields.teacherPresent.value && + + + + } + + + + { + this.props.registration && + + } + { + !this.props.registration && this.myState.editMode && + + } + { !this.props.registration && !this.myState.editMode + && (UserService.isAdministrator || UserService.isSecretary || this.myState.isOwnRecord) && + + } + { !this.props.registration && (UserService.isAdministrator || UserService.isSecretary) && + + } + + + + ); + } +} \ No newline at end of file diff --git a/web/src/components/tasks/DiplomaTopicAgreement.tsx b/web/src/components/tasks/DiplomaTopicAgreement.tsx new file mode 100644 index 0000000..0465e5d --- /dev/null +++ b/web/src/components/tasks/DiplomaTopicAgreement.tsx @@ -0,0 +1,52 @@ +import {ModalState} from "../../utils/modalState"; +import {observer} from "mobx-react"; +import {makeObservable, observable} from "mobx"; +import React from "react"; +import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; +import {StringInput} from "../controls/ReactiveControls"; +import {ReactiveValue} from "../../utils/reactive/reactiveValue"; +import {required} from "../../utils/reactive/validators"; + +class Fields { + constructor(modalState: ModalState) { + makeObservable(this); + this.modalState = modalState; + } + + @observable modalState: ModalState; + + @observable studentName = new ReactiveValue().addValidator(required); + @observable curatorName = new ReactiveValue().addValidator(required); + @observable topicName = new ReactiveValue().addValidator(required); + @observable canChangeTopic = false; +} + +@observer +class DiplomaTopicAgreementModal extends React.Component<{ modalState: ModalState }> { + constructor(props: any) { + super(props); + makeObservable(this); + this.fields = new Fields(props.modalState); + } + + @observable fields: Fields; + + render() { + return ( + + + Согласование темы ВКР + + + + + + + + + + + + ); + } +} \ No newline at end of file diff --git a/web/src/components/user/StudentProfile.tsx b/web/src/components/user/StudentProfile.tsx deleted file mode 100644 index 8a9c8ef..0000000 --- a/web/src/components/user/StudentProfile.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import {ComponentContext} from "../../utils/ComponentContext"; -import {IStudent} from "../../models/student"; -import {observer} from "mobx-react"; -import {computed, makeObservable, observable, reaction} from "mobx"; -import {Col, Row} from "react-bootstrap"; -import {StringInput} from "../custom/controls/ReactiveControls"; -import {ReactiveValue} from "../../utils/reactive/reactiveValue"; -import _ from "lodash"; - -export interface StudentProfileProps { - student: IStudent; - viewMode: "VIEW" | "EDIT"; -} - -@observer -export class StudentProfile extends ComponentContext { - @observable student: IStudent = this.props.student; - @observable viewMode: "VIEW" | "EDIT" = this.props.viewMode; - - @observable form = new ReactiveValue().setAuto(this.student.form.toString()); - @observable protectionOrder = new ReactiveValue().setAuto(_.toString(this.student.protectionOrder)); - @observable magistracy = new ReactiveValue().setAuto(this.student.magistracy); - @observable digitalFormatPresent = new ReactiveValue().setAuto(this.student.digitalFormatPresent.toString()); - @observable markComment = new ReactiveValue().setAuto(_.toString(this.student.markComment)); - @observable markPractice = new ReactiveValue().setAuto(_.toString(this.student.markPractice)); - @observable predefenceComment = new ReactiveValue().setAuto(this.student.predefenceComment); - @observable normalControl = new ReactiveValue().setAuto(this.student.normalControl); - @observable antiPlagiarism = new ReactiveValue().setAuto(_.toString(this.student.antiPlagiarism)); - @observable note = new ReactiveValue().setAuto(this.student.note); - @observable recordBookReturned = new ReactiveValue().setAuto(this.student.recordBookReturned.toString()); - @observable work = new ReactiveValue().setAuto(this.student.work); - @observable diplomaTopic = new ReactiveValue().setAuto(this.student.diplomaTopic); - @observable mentorUser = new ReactiveValue().setAuto(this.student.mentorUser.fullName); - @observable group = new ReactiveValue().setAuto(this.student.group.name); - - constructor(props: any) { - super(props); - makeObservable(this); - - reaction(() => this.props.viewMode, (viewMode) => { - this.viewMode = viewMode; - }); - - reaction(() => this.props.student, (student) => { - this.form.set(student.form.toString()); - this.protectionOrder.set(_.toString(student.protectionOrder)); - this.magistracy.set(student.magistracy); - this.digitalFormatPresent.set(student.digitalFormatPresent.toString()); - this.markComment.set(_.toString(student.markComment)); - this.markPractice.set(_.toString(student.markPractice)); - this.predefenceComment.set(student.predefenceComment); - this.normalControl.set(student.normalControl); - this.antiPlagiarism.set(_.toString(student.antiPlagiarism)); - this.note.set(student.note); - this.recordBookReturned.set(student.recordBookReturned.toString()); - this.work.set(student.work); - this.diplomaTopic.set(student.diplomaTopic); - this.mentorUser.set(student.mentorUser.fullName); - this.group.set(student.group.name); - }); - - reaction(() => { - return { - form: this.form.value, - protectionOrder: this.protectionOrder.value, - magistracy: this.magistracy.value, - digitalFormatPresent: this.digitalFormatPresent.value, - markComment: this.markComment.value, - markPractice: this.markPractice.value, - predefenceComment: this.predefenceComment.value, - normalControl: this.normalControl.value, - antiPlagiarism: this.antiPlagiarism.value, - note: this.note.value, - recordBookReturned: this.recordBookReturned.value, - work: this.work.value, - diplomaTopic: this.diplomaTopic.value, - mentorUser: this.mentorUser.value, - group: this.group.value - } - }, () => { - this.student = { - ...this.student, - form: this.form.value === "true", - protectionOrder: _.toNumber(this.protectionOrder.value), - magistracy: this.magistracy.value, - digitalFormatPresent: this.digitalFormatPresent.value === "true", - markComment: _.toNumber(this.markComment.value), - markPractice: _.toNumber(this.markPractice.value), - predefenceComment: this.predefenceComment.value, - normalControl: this.normalControl.value, - antiPlagiarism: _.toNumber(this.antiPlagiarism.value), - note: this.note.value, - recordBookReturned: this.recordBookReturned.value === "true", - work: this.work.value, - diplomaTopic: this.diplomaTopic.value, - group: {name: this.group.value} - } - }); - } - - @computed - get viewOnly() { - return this.viewMode === "VIEW"; - } - - render() { - return
- - - - - - - - - - - - - - - - - - - - - -
; - } -} \ No newline at end of file diff --git a/web/src/components/user/StudentProfileModal.tsx b/web/src/components/user/StudentProfileModal.tsx deleted file mode 100644 index a86a8b6..0000000 --- a/web/src/components/user/StudentProfileModal.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import {IStudent} from "../../models/student"; -import {ComponentContext} from "../../utils/ComponentContext"; -import {action, makeObservable, observable} from "mobx"; -import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap"; -import {StudentProfile} from "./StudentProfile"; -import {ModalState} from "../../utils/modalState"; - -export interface StudentProfileProps { - modalState: ModalState; - fullName: string; - student: IStudent; -} - -export class StudentProfileModal extends ComponentContext { - @observable modalState: ModalState = this.props.modalState; - @observable student: IStudent = this.props.student; - @observable viewMode: "VIEW" | "EDIT" = "VIEW"; - - constructor(props: any) { - super(props); - makeObservable(this); - } - - @action.bound - onEdit() { - this.viewMode = "EDIT"; - } - - @action.bound - onSave() { - this.modalState.close(); - this.viewMode = "VIEW"; - this.notificationStore.warn("Не реализовано"); - } - - render() { - return - - Профиль студента: {this.props.fullName} - - - - - - { - this.viewMode === "EDIT" && - - } - { - this.viewMode === "VIEW" && - - } - - - ; - } -} \ No newline at end of file diff --git a/web/src/components/user/UserListPage.tsx b/web/src/components/user/UserListPage.tsx deleted file mode 100644 index 25b2c5c..0000000 --- a/web/src/components/user/UserListPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import {observer} from "mobx-react"; -import {action, makeObservable, observable, reaction, runInAction} from "mobx"; -import {Page} from "../custom/layout/Page"; -import {IAuthenticated} from "../../models/user"; -import {DataTable} from "../custom/DataTable"; -import {get} from "../../utils/request"; -import {Column, TableDescriptor} from "../../utils/tables"; -import {Authorities, getAuthorityByCode} from "../../models/authorities"; -import {datetimeConverter} from "../../utils/converters"; -import {StudentProfileModal} from "./StudentProfileModal"; -import {ModalState} from "../../utils/modalState"; -import {IStudent} from "../../models/student"; - -@observer -export class UserListPage extends Page { - constructor(props: {}) { - super(props); - makeObservable(this); - } - - componentDidMount() { - this.requestUsers(); - reaction(() => this.users, () => { - if (typeof this.users === 'undefined') { - return; - } - - this.tableDescriptor = new TableDescriptor(this.userColumns, this.users); - }, {fireImmediately: true}); - } - - @observable users?: IAuthenticated[]; - @observable tableDescriptor?: TableDescriptor; - - @observable studentModalState = new ModalState(); - @observable studentFullName: string = ''; - @observable student: IStudent; - - userColumns = [ - new Column('login', 'Логин'), - new Column('fullName', 'Полное имя'), - new Column('authorities', 'Роли', (value, user) => { - return value.map(getAuthorityByCode).map(authority => { - if (authority.code === Authorities.STUDENT.code) { - return { - console.log(user.id); - get('student/by-user-id', {id: user.id}).then((student) => { - runInAction(() => { - this.studentFullName = user.fullName; - this.student = student; - this.studentModalState.open(); - }); - }); - }}>{authority.name}; - } else { - return {authority.name}; - } - }).reduce((prev, curr) => <> - {prev}, {curr} - ); - }), - new Column('email', 'Email'), - new Column('phone', 'Телефон'), - new Column('createdAt', 'Дата создания', (value: string) => value ? datetimeConverter(value) : 'Не создавалось'), - new Column('updatedAt', 'Дата обновления', (value: string) => value ? datetimeConverter(value) : 'Не обновлялось'), - ]; - - - @action.bound - requestUsers() { - this.thinkStore.think('userList'); - get('user/get-all').then((users) => { - runInAction(() => { - this.users = users; - }); - }).finally(() => { - this.thinkStore.completeAll('userList'); - }); - } - - get page() { - return <> - { - this.tableDescriptor && - <> - - { - this.student && - - } - - - } - - } -} \ No newline at end of file diff --git a/web/src/components/user/UserLoginModal.tsx b/web/src/components/user/UserLoginModal.tsx index 69a0652..7e34a0b 100644 --- a/web/src/components/user/UserLoginModal.tsx +++ b/web/src/components/user/UserLoginModal.tsx @@ -7,7 +7,11 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {ComponentContext} from "../../utils/ComponentContext"; import {ReactiveValue} from "../../utils/reactive/reactiveValue"; import {loginChars, loginLength, loginMaxLength, password, passwordChars, passwordLength, passwordMaxLength, required} from "../../utils/reactive/validators"; -import {PasswordInput, StringInput} from "../custom/controls/ReactiveControls"; +import {PasswordInput, StringInput} from "../controls/ReactiveControls"; +import {ThinkService} from "../../services/ThinkService"; +import {NotificationService} from "../../services/NotificationService"; +import {fullName} from "../../models/participant"; +import {UserService} from "../../services/UserService"; interface LoginModalProps { modalState: ModalState; @@ -38,29 +42,28 @@ export class UserLoginModal extends ComponentContext { if (this.modalInvalid) return; - this.thinkStore.think('loginModal'); + ThinkService.think('loginModal'); post('user/login', { login: this.login.value, password: this.password.value }).then(() => { - this.userStore.updateCurrentUser((user) => { - if (user.authenticated) { - this.routerStore.goTo('profile').then(); - this.notificationStore.success('Вы успешно вошли в систему, ' + user.fullName, 'Успешный вход'); + UserService.updateCurrentUser((user) => { + this.routerStore.goTo('home').then(); + if (user != null) { + NotificationService.success('Вы успешно вошли в систему, ' + fullName(user.participant), 'Успешный вход'); } else { - this.routerStore.goTo('root').then(); - this.notificationStore.error('Произошла ошибка при попытке входа в систему', 'Ошибка входа'); + NotificationService.error('Произошла ошибка при попытке входа в систему', 'Ошибка входа'); } }); }).finally(() => { this.props.modalState.close(); - this.thinkStore.completeAll('loginModal'); + ThinkService.completeAll('loginModal'); }); } render() { const open = this.props.modalState.isOpen; - const thinking = this.thinkStore.isThinking('loginModal'); + const thinking = ThinkService.isThinking('loginModal'); return diff --git a/web/src/components/user/UserProfile.tsx b/web/src/components/user/UserProfile.tsx deleted file mode 100644 index 4ef76b9..0000000 --- a/web/src/components/user/UserProfile.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import {ComponentContext} from "../../utils/ComponentContext"; -import {IUser} from "../../models/user"; -import {IStudent} from "../../models/student"; -import {ITeacher} from "../../models/teacher"; - -export interface UserProfileProps { - user: IUser; - student?: IStudent; - teacher?: ITeacher; -} - -export class UserProfile extends ComponentContext { - -} \ No newline at end of file diff --git a/web/src/components/user/UserProfilePage.tsx b/web/src/components/user/UserProfilePage.tsx deleted file mode 100644 index 890a34c..0000000 --- a/web/src/components/user/UserProfilePage.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import {Page} from "../custom/layout/Page"; -import {Col, Form, Row} from "react-bootstrap"; -import {observer} from "mobx-react"; -import {IStudent} from "../../models/student"; -import {action, makeObservable, observable, runInAction} from "mobx"; -import {IAuthenticated} from "../../models/user"; -import {ComponentContext} from "../../utils/ComponentContext"; -import {getAuthorityByCode} from "../../models/authorities"; -import {datetimeConverter} from "../../utils/converters"; - -@observer -class UserInfo extends ComponentContext { - constructor(props: any) { - super(props); - makeObservable(this); - } - - componentDidMount() { - runInAction(() => { - this.user = this.userStore.user as IAuthenticated; - }); - } - - @observable user: IAuthenticated; - - render() { - return
- { - this.user && - - - - ФИО - - - - Имя пользователя - - - - Электронная почта - - - - Телефон - {/* todo: format phone */} - - - - - - Роли - a.code).join(', ')} - disabled={true}/> - - - Дата создания - - - - Дата последней модификации - - - - - } -
- } -} - -@observer -class StudentInfo extends ComponentContext { - constructor(props: any) { - super(props); - makeObservable(this); - } - - componentDidMount() { - runInAction(() => { - this.student = this.userStore.student; - }); - } - - @observable student: IStudent; - - render() { - let student = this.student; - - return ( - - - - Тема дипломной работы - - - - Очередь защиты - - - - Презентация в электронном формате - - - - Оценка за комментарий {/* todo: обсудить с аналитиком */} - - - - Оценка за практику {/* todo: обсудить с аналитиком */} - - - - Комментарий к предзащите - - - - Форма контроля {/* todo: обсудить с аналитиком */} - - - - Антиплагиат (процент - уникальности) {/* todo: обсудить с аналитиком */} - - - - Примечание - - - - - - Группа - - - - Куратор - - - - Форма обучения {/* todo: обсудить с аналитиком */} - - - - Научный руководитель - - - - Зачетная книжка сдана - - - - Работа {/* todo: обсудить с аналитиком */} - - - - Магистратура {/* todo: обсудить с аналитиком */} - - - - - ); - } -} - -@observer -export class UserProfilePage extends Page { - constructor(props: any) { - super(props); - makeObservable(this); - } - - componentDidMount() { - runInAction(() => { - this.user = this.userStore.user as IAuthenticated; - this.student = this.userStore.student; - }); - this.redirectIfNotAuthenticated(); - } - - componentDidUpdate() { - runInAction(() => { - this.user = this.userStore.user as IAuthenticated; - this.student = this.userStore.student; - }); - this.redirectIfNotAuthenticated(); - } - - @observable user: IAuthenticated; - @observable student: IStudent; - - @action.bound - redirectIfNotAuthenticated() { - if (this.thinkStore.isThinking('updateCurrentUser')) { - return; - } - - if (!this.user.authenticated) { - this.routerStore.goToNotFound(); - } - } - - get page() { - return <> - { - !this.thinkStore.isThinking('updateCurrentUser') && -
- { - this.user?.authenticated && - - } - { - this.student && - - } - - } - - } -} diff --git a/web/src/components/user/UserRegistrationModal.tsx b/web/src/components/user/UserRegistrationModal.tsx deleted file mode 100644 index 0663540..0000000 --- a/web/src/components/user/UserRegistrationModal.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import {observer} from "mobx-react"; -import {action, makeObservable, observable} from "mobx"; -import {Button, Col, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Row} from "react-bootstrap"; -import {UserRegistrationDTO} from "../../models/registration"; -import {post} from "../../utils/request"; -import {ReactiveValue} from "../../utils/reactive/reactiveValue"; -import {PasswordInput, ReactiveSelectInputSelect, SelectInput, StringInput} from "../custom/controls/ReactiveControls"; -import { - email, - emailChars, - loginChars, - loginLength, - loginMaxLength, - nameChars, - nameLength, - password, - passwordChars, - passwordLength, - passwordMaxLength, - phone, - phoneChars, - required, - selected -} from "../../utils/reactive/validators"; -import {ComponentContext} from "../../utils/ComponentContext"; -import {ModalState} from "../../utils/modalState"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {Authorities} from "../../models/authorities"; - -export interface UserRegistrationModalProps { - modalState: ModalState; -} - -@observer -export class UserRegistrationModal extends ComponentContext { - constructor(props: any) { - super(props); - makeObservable(this); - } - - @observable login = new ReactiveValue() - .addValidator(required).addValidator(loginLength) - .addInputRestriction(loginChars).addInputRestriction(loginMaxLength); - @observable password = new ReactiveValue() - .addValidator(required).addValidator(password).addValidator(passwordLength) - .addInputRestriction(passwordChars).addInputRestriction(passwordMaxLength); - @observable fullName = new ReactiveValue() - .addValidator(required).addValidator(nameLength) - .addInputRestriction(nameChars); - @observable email = new ReactiveValue() - .addValidator(required).addValidator(email) - .addInputRestriction(emailChars); - @observable numberPhone = new ReactiveValue().setAuto('+7') - .addValidator(required).addValidator(phone) - .addInputRestriction(phoneChars); - @observable userKindEnum = new ReactiveValue().addValidator(required); - - @observable accountType = new ReactiveValue().addValidator(selected).addValidator((value) => { - if (value.value === Authorities.SECRETARY.code || value.value === Authorities.COMMISSION_MEMBER.code) { - return 'не реализовано' - } - }); - - @observable teacherGroup = new ReactiveValue().addValidator(selected); - @observable teacherStudent = new ReactiveValue().addValidator(selected); - - @observable studentGroup = new ReactiveValue().addValidator(selected); - - possibleAccountTypes(): ReactiveSelectInputSelect[] { - const teacher = {value: Authorities.TEACHER.code, label: Authorities.TEACHER.name} as ReactiveSelectInputSelect; - const student = {value: Authorities.STUDENT.code, label: Authorities.STUDENT.name} as ReactiveSelectInputSelect; - const commissionMember = {value: Authorities.COMMISSION_MEMBER.code, label: Authorities.COMMISSION_MEMBER.name} as ReactiveSelectInputSelect; - const administrator = {value: Authorities.ADMINISTRATOR.code, label: Authorities.ADMINISTRATOR.name} as ReactiveSelectInputSelect; - const secretary = {value: Authorities.SECRETARY.code, label: Authorities.SECRETARY.name} as ReactiveSelectInputSelect; - - if (this.userStore.isAdministrator) { - return [teacher, student, commissionMember, administrator, secretary]; - } else if (this.userStore.isSecretary) { - return [teacher, student, commissionMember]; - } else { - return []; - } - } - - get formInvalid() { - return this.login.invalid || !this.login.touched - || this.password.invalid || !this.password.touched - || this.fullName.invalid || !this.fullName.touched - || this.email.invalid || !this.email.touched - || this.numberPhone.invalid || !this.numberPhone.touched - || this.accountType.invalid || !this.accountType.touched - || this.thinkStore.isThinking('userRegistration'); - } - - @action.bound - submit() { - if (this.formInvalid) - return; - - this.thinkStore.think('userRegistration'); - post('user/register', { - login: this.login.value, - password: this.password.value, - fullName: this.fullName.value, - email: this.email.value, - numberPhone: this.numberPhone.value, - accountType: this.accountType.value.value, - studentData: this.accountType.value.value === Authorities.STUDENT.code ? { - groupId: 1 - } : undefined, - teacherData: this.accountType.value.value === Authorities.TEACHER.code ? { - curatingGroups: [], - advisingStudents: [], - } : undefined, - } as UserRegistrationDTO).then(() => { - this.notificationStore.success('Пользователь успешно зарегистрирован'); - }).catch(() => { - this.notificationStore.error('Ошибка регистрации пользователя'); - }).finally(() => { - this.thinkStore.completeOne('userRegistration'); - this.props.modalState.close(); - }); - } - - render() { - const thinking = this.thinkStore.isThinking('userRegistration'); - - return - - Регистрация пользователя - - { - thinking && - -
- -
-
- } - { - !thinking && - - - - - - - - - - - - - - - { - this.accountType.value?.value === Authorities.STUDENT.code && - - - - - - } - - { - this.accountType.value?.value === Authorities.TEACHER.code && - - - - - - - } - - } - - - - -
- } -} \ No newline at end of file diff --git a/web/src/favicon.png b/web/src/favicon.png new file mode 100644 index 0000000..6f21ede Binary files /dev/null and b/web/src/favicon.png differ diff --git a/web/src/index.css b/web/src/index.css index 93c7567..cb02476 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -72,3 +72,13 @@ footer { justify-content: center; align-items: center; } + +.modal-body .nav-tabs button { + padding-left: 10px; + padding-right: 10px; +} + +.modal-body .nav-tabs li { + margin-left: 3px; + margin-right: 3px; +} \ No newline at end of file diff --git a/web/src/index.html b/web/src/index.html index d7f333e..91b27d8 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -3,6 +3,7 @@ + TDMS diff --git a/web/src/models/IGroup.ts b/web/src/models/IGroup.ts deleted file mode 100644 index 06e2816..0000000 --- a/web/src/models/IGroup.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IGroup { - name: string; - curatorName?: string; - iAmCurator?: boolean; -} \ No newline at end of file diff --git a/web/src/models/authorities.ts b/web/src/models/authorities.ts deleted file mode 100644 index 03148f8..0000000 --- a/web/src/models/authorities.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface IAuthority { - code: string; - name: string; -} - -export const Authorities = Object.freeze({ - TEACHER: {code: "ROLE_TEACHER", name: "Преподаватель"} as IAuthority, - STUDENT: {code: "ROLE_STUDENT", name: "Студент"} as IAuthority, - COMMISSION_MEMBER: {code: "ROLE_COMMISSION_MEMBER", name: "Член комиссии ГЭК"} as IAuthority, - ADMINISTRATOR: {code: "ROLE_ADMINISTRATOR", name: "Администратор"} as IAuthority, - SECRETARY: {code: "ROLE_SECRETARY", name: "Секретарь"} as IAuthority, -}); - -export type Authority = typeof Authorities[keyof typeof Authorities]; -export type AuthorityCode = Authority['code']; -export type AuthorityName = Authority['name']; - -export function getAuthorityByCode(code: AuthorityCode): Authority { - return Object.values(Authorities).find(authority => authority.code === code) as Authority; -} diff --git a/web/src/models/defence.ts b/web/src/models/defence.ts new file mode 100644 index 0000000..d9fb27b --- /dev/null +++ b/web/src/models/defence.ts @@ -0,0 +1,10 @@ +import {Participant} from "./participant"; +import {Group} from "./group"; + +export interface Defence { + id: number; + commissionMembers: Participant[]; + groups: Group[]; + createdAt: string; + updatedAt: string; +} \ No newline at end of file diff --git a/web/src/models/diplomaTopic.ts b/web/src/models/diplomaTopic.ts new file mode 100644 index 0000000..e17d5eb --- /dev/null +++ b/web/src/models/diplomaTopic.ts @@ -0,0 +1,11 @@ +import {PreparationDirection} from "./preparationDirection"; +import {TeacherData} from "./teacherData"; + +export interface DiplomaTopic { + id?: number; + name: string; + teacher?: TeacherData; + preparationDirection?: PreparationDirection; + createdAt?: string; + updatedAt?: string; +} \ No newline at end of file diff --git a/web/src/models/error.ts b/web/src/models/errorResponse.ts similarity index 100% rename from web/src/models/error.ts rename to web/src/models/errorResponse.ts diff --git a/web/src/models/group.ts b/web/src/models/group.ts new file mode 100644 index 0000000..c0325ac --- /dev/null +++ b/web/src/models/group.ts @@ -0,0 +1,13 @@ +import {StudentData} from "./studentData"; +import {PreparationDirection} from "./preparationDirection"; + +export interface Group { + id?: number; + name?: string; + curatorName?: string; + curatorTeacherId?: number; + students?: StudentData[]; + preparationDirection?: PreparationDirection; + createdAt?: string; + updatedAt?: string; +} diff --git a/web/src/models/participant.ts b/web/src/models/participant.ts new file mode 100644 index 0000000..c23d96e --- /dev/null +++ b/web/src/models/participant.ts @@ -0,0 +1,23 @@ +import {User} from "./user"; +import {Role} from "./roles"; + +export interface Participant { + id: number; + firstName: string; + lastName: string; + middleName: string; + email: string; + numberPhone: string; + user?: User; + roles: Role[]; + createdAt: string; + updatedAt: string; +} + +export const fullName = (participant: Participant): string => { + let fio = ''; + if (participant.lastName) fio += participant.lastName; + if (participant.firstName) fio += ' ' + participant.firstName; + if (participant.middleName) fio += ' ' + participant.middleName; + return fio; +} \ No newline at end of file diff --git a/web/src/models/preparationDirection.ts b/web/src/models/preparationDirection.ts new file mode 100644 index 0000000..dc3d509 --- /dev/null +++ b/web/src/models/preparationDirection.ts @@ -0,0 +1,7 @@ +export interface PreparationDirection { + id: number; + name: string; + code: string; + createdAt?: string; + updatedAt?: string; +} \ No newline at end of file diff --git a/web/src/models/registration.ts b/web/src/models/registration.ts index 65f493f..6b4dc1c 100644 --- a/web/src/models/registration.ts +++ b/web/src/models/registration.ts @@ -1,18 +1,26 @@ -export interface UserRegistrationDTO { - login: string, - password: string, - fullName: string, +import { + RoleCode +} from "./roles"; + +export interface ParticipantSaveDTO { + id?: number, + firstName: string, + lastName: string, + middleName: string, email: string, numberPhone: string, - studentData?: StudentRegistrationDTO - teacherData?: TeacherRegistrationDTO + authorities: RoleCode[], + userData?: { + login: string; + password: string + } + studentData?: { + groupId: number; + curatorId: number + } + teacherData?: { + curatingGroups?: number[]; + advisingStudents?: number[]; + degree?: string + } } - -export interface StudentRegistrationDTO { - groupId: number; -} - -export interface TeacherRegistrationDTO { - curatingGroups: number[]; - advisingStudents: number[]; -} \ No newline at end of file diff --git a/web/src/models/roles.ts b/web/src/models/roles.ts new file mode 100644 index 0000000..d883005 --- /dev/null +++ b/web/src/models/roles.ts @@ -0,0 +1,27 @@ +export const Roles = Object.freeze({ + TEACHER: {code: "ROLE_TEACHER", name: "Преподаватель"}, + STUDENT: {code: "ROLE_STUDENT", name: "Студент"}, + COMMISSION_MEMBER: {code: "ROLE_COMMISSION_MEMBER", name: "Член комиссии ГЭК"}, + ADMINISTRATOR: {code: "ROLE_ADMINISTRATOR", name: "Администратор"}, + SECRETARY: {code: "ROLE_SECRETARY", name: "Секретарь"}, + PLAGIARISM_CHECKER: {code: "ROLE_PLAGIARISM_CHECKER", name: "Проверяющий на плагиат"}, +}); + +export const RolesArray = Object.values(Roles); + +export type Role = typeof Roles[keyof typeof Roles]; +export type RoleCode = Role['code']; +export type RoleName = Role['name']; + +export function getRoleByCode(code: { code: RoleCode; [key: string]: any }): Role { + return Object.values(Roles).find(role => role.code === code.code) as Role; +} + +export function getRoleArrayByCode(codes: Array<{ code: RoleCode; [key: string]: any }>): Role[] { + let onlyCodes = codes.map(code => code.code); + return Object.values(Roles).filter(role => onlyCodes.includes(role.code)); +} + +export function isRolePresent(code: RoleCode, roles: Role[]): boolean { + return roles.some(role => role.code === code); +} \ No newline at end of file diff --git a/web/src/models/student.ts b/web/src/models/student.ts deleted file mode 100644 index cb6d34d..0000000 --- a/web/src/models/student.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {IAuthenticated, IUser} from "./user"; -import {IGroup} from "./IGroup"; - -export interface IStudent { - id: number; - form: boolean; - protectionOrder: number; - magistracy: string; - digitalFormatPresent: boolean; - markComment: number; - markPractice: number; - predefenceComment: string; - normalControl: string; - antiPlagiarism: number; - note: string; - recordBookReturned: boolean; - work: string; - user: IUser; - diplomaTopic: string; - mentorUser: IAuthenticated; - group: IGroup; -} \ No newline at end of file diff --git a/web/src/models/studentData.ts b/web/src/models/studentData.ts new file mode 100644 index 0000000..279729f --- /dev/null +++ b/web/src/models/studentData.ts @@ -0,0 +1,13 @@ +import {Group} from "./group"; +import {Participant} from "./participant"; +import {TeacherData} from "./teacherData"; +import {DiplomaTopic} from "./diplomaTopic"; + +export interface StudentData { + id?: number; + group?: Group; + participant?: Participant; + name?: string; + curator?: TeacherData; + diplomaTopic?: DiplomaTopic; +} \ No newline at end of file diff --git a/web/src/models/teacher.ts b/web/src/models/teacher.ts deleted file mode 100644 index 8d471fc..0000000 --- a/web/src/models/teacher.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {IGroup} from "./IGroup"; -import {IStudent} from "./student"; - -export interface ITeacher { - id: number; - curatingGroups: IGroup[]; - advisingStudents: IStudent[]; - createdAt: string; - updatedAt: string; -} \ No newline at end of file diff --git a/web/src/models/teacherData.ts b/web/src/models/teacherData.ts new file mode 100644 index 0000000..c7535ae --- /dev/null +++ b/web/src/models/teacherData.ts @@ -0,0 +1,9 @@ +import {Participant} from "./participant"; + +export interface TeacherData { + id?: number; + participant?: Participant; + degree?: string; + createdAt?: string; + updatedAt?: string; +} \ No newline at end of file diff --git a/web/src/models/user.ts b/web/src/models/user.ts index 145af20..9d2448e 100644 --- a/web/src/models/user.ts +++ b/web/src/models/user.ts @@ -1,14 +1,9 @@ -export interface IAuthenticated { - id: number, - authenticated: true, - login: string, - fullName: string, - email: string, - phone: string, - authorities: string[], +import {Participant} from "./participant"; - createdAt: string, - updatedAt: string, -} - -export declare type IUser = {authenticated: false} | IAuthenticated; +export interface User { + id: number; + login: string; + participant: Participant; + createdAt: string; + updatedAt: string; +} \ No newline at end of file diff --git a/web/src/services/NotificationService.ts b/web/src/services/NotificationService.ts index 43eb417..13182a8 100644 --- a/web/src/services/NotificationService.ts +++ b/web/src/services/NotificationService.ts @@ -1,42 +1,93 @@ -import {NotificationStore} from "../store/NotificationStore"; +import {v7 as uuidGen} from "uuid"; +import {action, makeObservable, observable, runInAction} from "mobx"; -export class NotificationService { - static store?: NotificationStore; +export class Notification { + uuid: string; + message: string; + title?: string; - static init(store: NotificationStore) { - this.store = store; - console.debug('NotificationService initialized'); - } - - static error(message: string, title?: string) { - if (!this.store) { - console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`); - return; - } - this.store.error(message, title); - } - - static warn(message: string, title?: string) { - if (!this.store) { - console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`); - return; - } - this.store.warn(message, title); - } - - static info(message: string, title?: string) { - if (!this.store) { - console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`); - return; - } - this.store.info(message, title); - } - - static success(message: string, title?: string) { - if (!this.store) { - console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`); - return; - } - this.store.success(message, title); + constructor(message: string, title?: string) { + this.title = title; + this.message = message; + this.uuid = uuidGen(); } } + +export enum NotificationType { + ERROR = 'error', + WARNING = 'warning', + INFO = 'info', + SUCCESS = 'success' +} + +class NotificationServiceImpl { + @observable errors: Notification[] = []; + @observable warnings: Notification[] = []; + @observable infos: Notification[] = []; + @observable successes: Notification[] = []; + timeout: number = 10000; + + constructor() { + makeObservable(this); + } + + @action.bound + notify(message: string, type: NotificationType, title?: string) { + let notification = new Notification(message, title); + let container: Notification[]; + switch (type) { + case NotificationType.ERROR: + container = this.errors; + break; + case NotificationType.WARNING: + container = this.warnings; + break; + case NotificationType.INFO: + container = this.infos; + break; + case NotificationType.SUCCESS: + container = this.successes; + break; + } + + container.push(notification); + setTimeout(() => runInAction(() => { + this.close(notification.uuid); + }), this.timeout); + } + + @action.bound + error(message: string, title?: string) { + this.notify(message, NotificationType.ERROR, title); + } + + @action.bound + warn(message: string, title?: string) { + this.notify(message, NotificationType.WARNING, title); + } + + @action.bound + info(message: string, title?: string) { + this.notify(message, NotificationType.INFO, title); + } + + @action.bound + success(message: string, title?: string) { + this.notify(message, NotificationType.SUCCESS, title); + } + + @action.bound + close(uuid: string) { + this.errors = this.errors.filter(n => n.uuid !== uuid); + this.warnings = this.warnings.filter(n => n.uuid !== uuid); + this.infos = this.infos.filter(n => n.uuid !== uuid); + this.successes = this.successes.filter(n => n.uuid !== uuid); + } + + @action.bound + accessDenied() { + this.error('Доступ запрещен', 'Ошибка доступа'); + } +} + +export const NotificationService = new NotificationServiceImpl(); diff --git a/web/src/services/RouterService.ts b/web/src/services/RouterService.ts index c466e8e..a11c045 100644 --- a/web/src/services/RouterService.ts +++ b/web/src/services/RouterService.ts @@ -1,6 +1,6 @@ import {AppRouterStore} from "../store/AppRouterStore"; -export interface IRouterOptions { +export interface RouterOptions { redirect: string; } @@ -9,10 +9,9 @@ export class RouterService { static init(router: AppRouterStore) { this.router = router; - console.debug('RouterService initialized'); } - static redirect(state: string, options?: IRouterOptions) { - this.router.goTo(state, options); + static redirect(state: string, options?: RouterOptions) { + this.router.goTo(state, options).then(); } } diff --git a/web/src/store/SysInfoStore.ts b/web/src/services/SysInfoService.ts similarity index 59% rename from web/src/store/SysInfoStore.ts rename to web/src/services/SysInfoService.ts index 5bd9cb7..ceccf9c 100644 --- a/web/src/store/SysInfoStore.ts +++ b/web/src/services/SysInfoService.ts @@ -1,32 +1,31 @@ -import {RootStore} from "./RootStore"; import {action, makeObservable, observable, runInAction} from "mobx"; import {get} from "../utils/request"; +import {ThinkService} from "./ThinkService"; -export class SysInfoStore { - rootStore: RootStore; +class SysInfoServiceImpl { @observable version: string = 'unknown'; - constructor(rootStore: RootStore) { + constructor() { makeObservable(this); - this.rootStore = rootStore; } @action.bound updateVersion() { - this.rootStore.thinkStore.think('updateVersion'); + ThinkService.think('updateVersion'); get('/sysinfo/version').then((response) => { runInAction(() => { this.version = response; }); }).finally(() => { runInAction(() => { - this.rootStore.thinkStore.completeOne('updateVersion'); + ThinkService.completeOne('updateVersion'); }); }); } init() { this.updateVersion(); - console.debug('SysInfoStore initialized'); } -} \ No newline at end of file +} + +export const SysInfoService = new SysInfoServiceImpl(); \ No newline at end of file diff --git a/web/src/store/ThinkStore.ts b/web/src/services/ThinkService.ts similarity index 70% rename from web/src/store/ThinkStore.ts rename to web/src/services/ThinkService.ts index 0d6e7b7..09b17ca 100644 --- a/web/src/store/ThinkStore.ts +++ b/web/src/services/ThinkService.ts @@ -1,12 +1,9 @@ import {action, makeObservable, observable} from "mobx"; -import {RootStore} from "./RootStore"; -export class ThinkStore { - rootStore: RootStore; +class ThinkServiceImpl { @observable thinks: string[] = []; - constructor(rootStore: RootStore) { - this.rootStore = rootStore; + constructor() { makeObservable(this); } @@ -28,8 +25,6 @@ export class ThinkStore { isThinking(key: string = '$default') { return this.thinks.includes(key); } +} - init() { - console.debug('ThinkStore initialized'); - } -} \ No newline at end of file +export const ThinkService = new ThinkServiceImpl(); \ No newline at end of file diff --git a/web/src/services/UserService.ts b/web/src/services/UserService.ts new file mode 100644 index 0000000..2080e77 --- /dev/null +++ b/web/src/services/UserService.ts @@ -0,0 +1,72 @@ +import {action, computed, makeObservable, observable, runInAction} from "mobx"; +import {get} from "../utils/request"; +import {ThinkService} from "./ThinkService"; +import {User} from "../models/user"; +import {RootStore} from "../store/RootStore"; +import {Roles} from "../models/roles"; + +class UserServiceImpl { + @observable user: User; + + constructor() { + makeObservable(this); + } + + @action.bound + updateCurrentUser(callback?: (user: User) => void) { + ThinkService.think('updateCurrentUser'); + get('/user/current', null, true, false).then((response) => { + runInAction(() => { + this.user = response; + }); + }).catch(() => { + runInAction(() => { + this.user = null; + }) + }).finally(() => { + runInAction(() => { + ThinkService.completeOne('updateCurrentUser'); + if (callback) { + callback(this.user); + } + }); + }); + } + + @computed + get isAdministrator() { + return this.user != null && this.user.participant.roles.some(a => a.code === Roles.ADMINISTRATOR.code); + } + + @computed + get isStudent() { + return this.user != null && this.user.participant.roles.some(a => a.code === Roles.STUDENT.code); + } + + @computed + get isTeacher() { + return this.user != null && this.user.participant.roles.some(a => a.code === Roles.TEACHER.code); + } + + @computed + get isCommissionMember() { + return this.user != null && this.user.participant.roles.some(a => a.code === Roles.COMMISSION_MEMBER.code); + } + + @computed + get isSecretary() { + return this.user != null && this.user.participant.roles.some(a => a.code === Roles.SECRETARY.code); + } + + @computed + get authenticated() { + return this.user != null; + } + + @action.bound + init() { + this.updateCurrentUser(); + } +} + +export const UserService = new UserServiceImpl(); \ No newline at end of file diff --git a/web/src/store/AppRouterStore.tsx b/web/src/store/AppRouterStore.tsx index 466e242..40883a2 100644 --- a/web/src/store/AppRouterStore.tsx +++ b/web/src/store/AppRouterStore.tsx @@ -4,7 +4,6 @@ import {RootStore} from "./RootStore"; export class AppRouterStore extends RouterStore { constructor(rootStore: RootStore) { super(routes, createRouterState('error', {notFound: true})); - // makeObservable(this); this.rootStore = rootStore; } @@ -13,7 +12,6 @@ export class AppRouterStore extends RouterStore { init() { const historyAdapter = new HistoryAdapter(this, browserHistory); historyAdapter.observeRouterStateChanges(); - console.debug('MyRouterStore initialized'); return this; } } @@ -22,14 +20,20 @@ const routes: Route[] = [{ name: 'home', pattern: '/', }, { - name: 'profile', - pattern: '/profile', -}, { - name: 'userList', - pattern: '/users', + name: 'participantList', + pattern: '/participants', }, { name: 'groupList', pattern: '/groups', +}, { + name: 'defenceList', + pattern: '/defence', +},{ + name: 'preparationDirectionList', + pattern: '/prep-direction', +},{ + name: 'themeList', + pattern: '/theme', }, { name: 'error', pattern: '/error', diff --git a/web/src/store/NotificationStore.ts b/web/src/store/NotificationStore.ts deleted file mode 100644 index 703e843..0000000 --- a/web/src/store/NotificationStore.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {RootStore} from "./RootStore"; -import {action, makeObservable, observable, runInAction} from "mobx"; -import {v7 as uuidGen} from 'uuid'; - -export class Notification { - uuid: string; - message: string; - title?: string; - - constructor(message: string, title?: string) { - this.title = title; - this.message = message; - this.uuid = uuidGen(); - } -} - -export enum NotificationType { - ERROR = 'error', - WARNING = 'warning', - INFO = 'info', - SUCCESS = 'success' -} - -export class NotificationStore { - rootStore: RootStore; - @observable errors: Notification[] = []; - @observable warnings: Notification[] = []; - @observable infos: Notification[] = []; - @observable successes: Notification[] = []; - timeout: number = 10000; - - constructor(rootStore: RootStore) { - this.rootStore = rootStore; - makeObservable(this); - } - - @action.bound - notify(message: string, type: NotificationType, title?: string) { - let notification = new Notification(message, title); - let container: Notification[]; - switch (type) { - case NotificationType.ERROR: - container = this.errors; - break; - case NotificationType.WARNING: - container = this.warnings; - break; - case NotificationType.INFO: - container = this.infos; - break; - case NotificationType.SUCCESS: - container = this.successes; - break; - } - - container.push(notification); - setTimeout(() => runInAction(() => { - this.close(notification.uuid); - }), this.timeout); - } - - @action.bound - error(message: string, title?: string) { - this.notify(message, NotificationType.ERROR, title); - } - - @action.bound - warn(message: string, title?: string) { - this.notify(message, NotificationType.WARNING, title); - } - - @action.bound - info(message: string, title?: string) { - this.notify(message, NotificationType.INFO, title); - } - - @action.bound - success(message: string, title?: string) { - this.notify(message, NotificationType.SUCCESS, title); - } - - @action.bound - close(uuid: string) { - this.errors = this.errors.filter(n => n.uuid !== uuid); - this.warnings = this.warnings.filter(n => n.uuid !== uuid); - this.infos = this.infos.filter(n => n.uuid !== uuid); - this.successes = this.successes.filter(n => n.uuid !== uuid); - } - - init() { - console.debug('NotificationStore initialized'); - } - - @action.bound - accessDenied() { - this.error('Доступ запрещен', 'Ошибка доступа'); - } -} \ No newline at end of file diff --git a/web/src/store/RootStore.ts b/web/src/store/RootStore.ts index 1ec0639..373e800 100644 --- a/web/src/store/RootStore.ts +++ b/web/src/store/RootStore.ts @@ -1,27 +1,14 @@ import {AppRouterStore} from "./AppRouterStore"; -import {UserStore} from "./UserStore"; -import {ThinkStore} from "./ThinkStore"; -import {NotificationStore} from "./NotificationStore"; -import {SysInfoStore} from "./SysInfoStore"; export class RootStore { - thinkStore = new ThinkStore(this); - userStore = new UserStore(this); routerStore = new AppRouterStore(this); - notificationStore = new NotificationStore(this); - sysInfoStore = new SysInfoStore(this); constructor() { } init() { - this.thinkStore.init(); this.routerStore.init(); - this.notificationStore.init(); - this.sysInfoStore.init(); - this.userStore.init(); - console.debug('RootStore initialized'); return this; } } diff --git a/web/src/store/UserStore.ts b/web/src/store/UserStore.ts deleted file mode 100644 index 8b00b06..0000000 --- a/web/src/store/UserStore.ts +++ /dev/null @@ -1,77 +0,0 @@ -import {get} from "../utils/request"; -import {action, computed, makeObservable, observable, runInAction} from "mobx"; -import {RootStore} from "./RootStore"; -import type {IUser} from "../models/user"; -import {IStudent} from "../models/student"; -import {Authorities} from "../models/authorities"; - -export class UserStore { - rootStore: RootStore; - @observable user: IUser = {authenticated: false}; - @observable student: IStudent; - - constructor(rootStore: RootStore) { - makeObservable(this); - this.rootStore = rootStore; - } - - @action.bound - updateCurrentUser(callback?: (user: IUser) => void) { - this.rootStore.thinkStore.think('updateCurrentUser'); - get('/user/current').then((response) => { - runInAction(() => { - this.user = response; - }); - if (this.isStudent) { - get('/student/current').then((student) => { - runInAction(() => { - this.student = student; - }); - }); - } - }).finally(() => { - runInAction(() => { - this.rootStore.thinkStore.completeOne('updateCurrentUser'); - if (callback) { - callback(this.user); - } - }); - }); - } - - @computed - get isAdministrator() { - return this.user.authenticated && this.user.authorities.some(a => a === Authorities.ADMINISTRATOR.code); - } - - @computed - get isStudent() { - return this.user.authenticated && this.user.authorities.some(a => a === Authorities.STUDENT.code); - } - - @computed - get isTeacher() { - return this.user.authenticated && this.user.authorities.some(a => a === Authorities.TEACHER.code); - } - - @computed - get isCommissionMember() { - return this.user.authenticated && this.user.authorities.some(a => a === Authorities.COMMISSION_MEMBER.code); - } - - @computed - get isSecretary() { - return this.user.authenticated && this.user.authorities.some(a => a === Authorities.SECRETARY.code); - } - - @computed - get authenticated() { - return this.user.authenticated - } - - init() { - this.updateCurrentUser(); - console.debug('UserStore initialized'); - return this; - } -} diff --git a/web/src/utils/ComponentContext.ts b/web/src/utils/ComponentContext.ts index 25afb43..963488a 100644 --- a/web/src/utils/ComponentContext.ts +++ b/web/src/utils/ComponentContext.ts @@ -9,23 +9,7 @@ export abstract class ComponentContext

extends Compone return this.context; } - get thinkStore() { - return this.context.thinkStore; - } - - get userStore() { - return this.context.userStore; - } - get routerStore() { return this.context.routerStore; } - - get notificationStore() { - return this.context.notificationStore; - } - - get sysInfoStore() { - return this.context.sysInfoStore; - } } \ No newline at end of file diff --git a/web/src/utils/init.ts b/web/src/utils/init.ts index a63510f..f9e9ff6 100644 --- a/web/src/utils/init.ts +++ b/web/src/utils/init.ts @@ -1,47 +1,53 @@ -import {configure} from "mobx"; -import {RootStore} from "../store/RootStore"; -import {RouterService} from "../services/RouterService"; -import {library} from '@fortawesome/fontawesome-svg-core'; -import {fas} from '@fortawesome/free-solid-svg-icons'; -import {fab} from "@fortawesome/free-brands-svg-icons"; -import {far} from "@fortawesome/free-regular-svg-icons"; -import {NotificationService} from "../services/NotificationService"; +import { + configure +} from "mobx"; +import { + RootStore +} from "../store/RootStore"; +import { + RouterService +} from "../services/RouterService"; +import { + library +} from '@fortawesome/fontawesome-svg-core'; +import { + fas +} from '@fortawesome/free-solid-svg-icons'; +import { + fab +} from "@fortawesome/free-brands-svg-icons"; +import { + far +} from "@fortawesome/free-regular-svg-icons"; +import { + SysInfoService +} from "../services/SysInfoService"; +import {UserService} from "../services/UserService"; const initMobX = () => { configure({enforceActions: 'observed'}); - console.debug('MobX initialized'); } const initFontAwesome = () => { library.add(fas); library.add(fab); library.add(far); - console.debug('FontAwesome initialized'); } const initLibs = () => { initMobX(); initFontAwesome(); - console.debug('Libraries initialized'); } const initServices = (rootStore: RootStore) => { RouterService.init(rootStore.routerStore); - NotificationService.init(rootStore.notificationStore); - console.debug('Services initialized'); + SysInfoService.init(); + UserService.init(); } export const initApp = () => { - console.debug('Initializing app'); - console.debug('>>>>>>>>>>>>>>>>>>>>>>>>'); - initLibs(); - let rootStore = new RootStore().init(); - initServices(rootStore); - - console.debug('<<<<<<<<<<<<<<<<<<<<<<<<'); - console.debug('App initialized'); return rootStore; } \ No newline at end of file diff --git a/web/src/utils/modalState.ts b/web/src/utils/modalState.ts index 15c959e..feb5973 100644 --- a/web/src/utils/modalState.ts +++ b/web/src/utils/modalState.ts @@ -2,10 +2,12 @@ import {action, makeObservable, observable} from "mobx"; export class ModalState { @observable isOpen: boolean; + @observable onApply?: () => void; - constructor(isOpen: boolean = false) { + constructor(isOpen: boolean = false, onApply?: () => void) { makeObservable(this); this.isOpen = isOpen; + this.onApply = onApply; } @action.bound diff --git a/web/src/utils/reactive/reactiveValue.ts b/web/src/utils/reactive/reactiveValue.ts index 5d3f839..193a81e 100644 --- a/web/src/utils/reactive/reactiveValue.ts +++ b/web/src/utils/reactive/reactiveValue.ts @@ -80,7 +80,7 @@ export class ReactiveValue { @action.bound touch() { - this.isTouched = true; + this.set(this.value); } @action.bound diff --git a/web/src/utils/reactive/validators.ts b/web/src/utils/reactive/validators.ts index de6f5a9..e8b1282 100644 --- a/web/src/utils/reactive/validators.ts +++ b/web/src/utils/reactive/validators.ts @@ -1,4 +1,4 @@ -import {ReactiveSelectInputSelect} from "../../components/custom/controls/ReactiveControls"; +import {SelectInputValue} from "../../components/controls/ReactiveControls"; export const required = (value: any, field = 'Поле') => { const message = `${field} обязательно для заполнения`; @@ -11,7 +11,7 @@ export const required = (value: any, field = 'Поле') => { } } -export const selected = (value: ReactiveSelectInputSelect, field = 'Поле') => { +export const selected = (value: SelectInputValue, field = 'Поле') => { if (!value.value || value.value === '__unselected__') { return `${field} должно быть выбрано`; } @@ -121,14 +121,14 @@ export const passwordChars = (value: string, field = 'Поле') => { } export const nameChars = (value: string, field = 'Поле') => { - if (!/^[a-zA-Zа-яА-ЯёЁ\s]+$/.test(value)) { - return `${field} должно содержать только буквы английского или русского алфавита и пробелы`; + if (!/^[a-zA-Zа-яА-ЯёЁ\s-]+$/.test(value)) { + return `${field} должно содержать только буквы английского или русского алфавита, пробелы и дефис`; } } export const nameLength = (value: string, field = 'Поле') => { - if (value.length < 3) { - return `${field} должно содержать минимум 3 символа`; + if (value.length < 1) { + return `${field} должно содержать минимум 1 символ`; } } diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 63b925d..ed9205f 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -1,6 +1,6 @@ import axios, {AxiosError, AxiosRequestConfig} from "axios"; import {NotificationService} from "../services/NotificationService"; -import {ErrorResponse} from "../models/error"; +import {ErrorResponse} from "../models/errorResponse"; export const apiUrl = "http://localhost:8080/api/v1/"; @@ -8,6 +8,9 @@ export const get = async (url: string, data?: any, doReject = true, showError url: url, method: 'GET', params: data, + headers: { + Accept: 'application/json', + } } as AxiosRequestConfig, doReject, showError); export const post = async (url: string, data?: any, doReject = true, showError = true) => await request({ @@ -16,7 +19,7 @@ export const post = async (url: string, data?: any, doReject = true, showErro data: data, } as AxiosRequestConfig, doReject, showError); -export const request = async (config: AxiosRequestConfig, doReject: boolean, showError: boolean) => { +export const request = async (config: AxiosRequestConfig, doReject: boolean = true, showError: boolean) => { return new Promise((resolve, reject) => { axios.request({...config, baseURL: apiUrl, withCredentials: true}).then((response) => { resolve(response.data); @@ -26,7 +29,6 @@ export const request = async (config: AxiosRequestConfig, doReject: bool else if (error.request) NotificationService.error(error.request, 'Не удалось получить ответ от сервера'); else NotificationService.error(error.message, 'Запрос отправить не удалось'); } - if (doReject) reject(error); }); }); diff --git a/web/src/utils/tables.ts b/web/src/utils/tables.ts index b6a26ca..136c383 100644 --- a/web/src/utils/tables.ts +++ b/web/src/utils/tables.ts @@ -15,7 +15,7 @@ export class TableDescriptor { columns: Column[], data: R[], pageable = true, - filters: ((row: R) => boolean)[] = [() => true] + filters: ((row: R) => boolean)[] = [() => true], ) { makeObservable(this); this.columns = columns; @@ -26,7 +26,8 @@ export class TableDescriptor { } export class Column { - @observable key: string; /* key of the field in the data object */ + @observable key: string; + @observable renderKey: string; @observable title: string; @observable sort: Sort; @observable suffixElement?: (data: R) => ReactNode; @@ -34,16 +35,17 @@ export class Column { constructor( key: string, title: string, - format: (value: C, data: R) => ReactNode = (value: C) => value ? _.toString(value) : 'Пусто', + format?: (value: C, data: R) => ReactNode, suffix?: (data: R) => ReactNode, - sort: Sort = new Sort() + sort?: Sort ) { makeObservable(this); - this.suffixElement = suffix; this.key = key; + this.renderKey = _.uniqueId(this.key); this.title = title; - this.format = format; - this.sort = sort; + this.suffixElement = suffix; + this.format = format ?? (v => v ? _.toString(v) : "Пусто"); + this.sort = sort ?? new Sort(); } } diff --git a/web/webpack.config.js b/web/webpack.config.js index 9ffde96..902143e 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -1,5 +1,6 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry: { @@ -20,6 +21,9 @@ module.exports = { }, { test: /\.(png|jpe?g|gif|svg)$/i, type: 'asset/resource', + generator: { + filename: '[name][ext]' + } }, { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', @@ -31,6 +35,7 @@ module.exports = { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true, + publicPath: '/' }, optimization: { splitChunks: { @@ -53,6 +58,11 @@ module.exports = { plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, 'src', 'index.html') + }), + new CopyWebpackPlugin({ + patterns: [ + { from: 'src/favicon.png', to: 'favicon.png' } + ] }) ] }