.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 |