From 324812853b3a1b0b4dbdba96268c353220dc500f Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 21:43:43 +0000 Subject: [PATCH 1/2] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4a80115..19aec56 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/TvkQWWs6) # Features of modern Java # Цели и задачи л/р: From 2abd37ca8b063da2fd922f527d688096c2984bf1 Mon Sep 17 00:00:00 2001 From: jbisss Date: Sun, 14 Dec 2025 03:31:58 +0300 Subject: [PATCH 2/2] [commit] --- build.gradle.kts | 26 ++ src/main/WHERE_WHAT_IMPLEMENTED.md | 31 ++ src/main/java/org/lab/Main.java | 14 +- .../java/org/lab/config/AuthInterceptor.java | 44 ++ .../java/org/lab/config/DataInitializer.java | 402 ++++++++++++++++++ .../java/org/lab/config/SecurityConfig.java | 27 ++ .../org/lab/controller/AuthController.java | 55 +++ .../lab/controller/BugReportController.java | 122 ++++++ .../lab/controller/MilestoneController.java | 69 +++ .../org/lab/controller/ProjectController.java | 93 ++++ .../org/lab/controller/TicketController.java | 100 +++++ .../org/lab/dto/CreateBugReportRequest.java | 14 + .../org/lab/dto/CreateMilestoneRequest.java | 16 + .../org/lab/dto/CreateProjectRequest.java | 11 + .../java/org/lab/dto/CreateTicketRequest.java | 10 + src/main/java/org/lab/dto/LoginRequest.java | 9 + .../java/org/lab/dto/RegisterRequest.java | 11 + src/main/java/org/lab/entities/BugReport.java | 25 ++ src/main/java/org/lab/entities/MileStone.java | 26 ++ src/main/java/org/lab/entities/Project.java | 74 ++++ src/main/java/org/lab/entities/Ticket.java | 26 ++ src/main/java/org/lab/entities/User.java | 23 + src/main/java/org/lab/enums/BugStatus.java | 8 + .../java/org/lab/enums/MilestoneStatus.java | 7 + src/main/java/org/lab/enums/SystemRole.java | 9 + src/main/java/org/lab/enums/TicketStatus.java | 8 + .../lab/repository/BugReportRepository.java | 31 ++ .../lab/repository/InMemoryRepository.java | 44 ++ .../lab/repository/MilestoneRepository.java | 26 ++ .../org/lab/repository/ProjectRepository.java | 26 ++ .../org/lab/repository/TicketRepository.java | 37 ++ .../org/lab/repository/UserRepository.java | 22 + .../org/lab/service/BugReportService.java | 177 ++++++++ .../org/lab/service/MilestoneService.java | 126 ++++++ .../java/org/lab/service/ProjectService.java | 128 ++++++ .../java/org/lab/service/TicketService.java | 155 +++++++ .../java/org/lab/service/UserService.java | 56 +++ src/main/java/org/lab/util/BugType.java | 62 +++ .../java/org/lab/util/MilestoneTemplate.java | 3 + src/main/java/org/lab/util/TicketType.java | 14 + src/main/resources/application.yml | 34 ++ 41 files changed, 2198 insertions(+), 3 deletions(-) create mode 100644 src/main/WHERE_WHAT_IMPLEMENTED.md create mode 100644 src/main/java/org/lab/config/AuthInterceptor.java create mode 100644 src/main/java/org/lab/config/DataInitializer.java create mode 100644 src/main/java/org/lab/config/SecurityConfig.java create mode 100644 src/main/java/org/lab/controller/AuthController.java create mode 100644 src/main/java/org/lab/controller/BugReportController.java create mode 100644 src/main/java/org/lab/controller/MilestoneController.java create mode 100644 src/main/java/org/lab/controller/ProjectController.java create mode 100644 src/main/java/org/lab/controller/TicketController.java create mode 100644 src/main/java/org/lab/dto/CreateBugReportRequest.java create mode 100644 src/main/java/org/lab/dto/CreateMilestoneRequest.java create mode 100644 src/main/java/org/lab/dto/CreateProjectRequest.java create mode 100644 src/main/java/org/lab/dto/CreateTicketRequest.java create mode 100644 src/main/java/org/lab/dto/LoginRequest.java create mode 100644 src/main/java/org/lab/dto/RegisterRequest.java create mode 100644 src/main/java/org/lab/entities/BugReport.java create mode 100644 src/main/java/org/lab/entities/MileStone.java create mode 100644 src/main/java/org/lab/entities/Project.java create mode 100644 src/main/java/org/lab/entities/Ticket.java create mode 100644 src/main/java/org/lab/entities/User.java create mode 100644 src/main/java/org/lab/enums/BugStatus.java create mode 100644 src/main/java/org/lab/enums/MilestoneStatus.java create mode 100644 src/main/java/org/lab/enums/SystemRole.java create mode 100644 src/main/java/org/lab/enums/TicketStatus.java create mode 100644 src/main/java/org/lab/repository/BugReportRepository.java create mode 100644 src/main/java/org/lab/repository/InMemoryRepository.java create mode 100644 src/main/java/org/lab/repository/MilestoneRepository.java create mode 100644 src/main/java/org/lab/repository/ProjectRepository.java create mode 100644 src/main/java/org/lab/repository/TicketRepository.java create mode 100644 src/main/java/org/lab/repository/UserRepository.java create mode 100644 src/main/java/org/lab/service/BugReportService.java create mode 100644 src/main/java/org/lab/service/MilestoneService.java create mode 100644 src/main/java/org/lab/service/ProjectService.java create mode 100644 src/main/java/org/lab/service/TicketService.java create mode 100644 src/main/java/org/lab/service/UserService.java create mode 100644 src/main/java/org/lab/util/BugType.java create mode 100644 src/main/java/org/lab/util/MilestoneTemplate.java create mode 100644 src/main/java/org/lab/util/TicketType.java create mode 100644 src/main/resources/application.yml diff --git a/build.gradle.kts b/build.gradle.kts index 79bf52a..7149fb0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,20 +1,46 @@ plugins { + id("org.springframework.boot") version "3.3.0" + id("io.spring.dependency-management") version "1.1.0" id("java") } group = "org.lab" version = "1.0-SNAPSHOT" +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + repositories { mavenCentral() } dependencies { + implementation("org.springframework.boot:spring-boot-starter") + + implementation("org.springframework.boot:spring-boot-starter-web") + + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") + + testCompileOnly("org.projectlombok:lombok") + testAnnotationProcessor("org.projectlombok:lombok") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +tasks.withType { + options.compilerArgs.addAll(listOf( + "--enable-preview", + "--release", "21" + )) + options.encoding = "UTF-8" +} + tasks.test { useJUnitPlatform() } \ No newline at end of file diff --git a/src/main/WHERE_WHAT_IMPLEMENTED.md b/src/main/WHERE_WHAT_IMPLEMENTED.md new file mode 100644 index 0000000..6fd2dae --- /dev/null +++ b/src/main/WHERE_WHAT_IMPLEMENTED.md @@ -0,0 +1,31 @@ +# В ходе реализации необходимо использовать возможности современных версий языка Java: +* Pattern matching для switch +* строковые шаблоны)))))))))))))) +* расширенные возможности стандартной библиотеки Java +* sealed классы и record +* программирование в функциональном стиле +* preview как project Valhalla, structured concurrency... +* и т.д. + +# Pattern matching для switch + +Ctrl + Shift + F: createBugReport( - связано с bugType'ами + +# строковые шаблоны)))))))))))))) + +Ctrl + Shift + F: STR. + +# расширенные возможности стандартной библиотеки Java + +Optional +Streams + +# sealed классы и record + +Sealed: org.lab.util.BugType - все наследники + +record: реализующие org.lab.util.BugType + TicketType + MilestoneTemplate + +# программирование в функциональном стиле + +Ctrl + Shift + F: Stream; все сервисы на стримах \ No newline at end of file diff --git a/src/main/java/org/lab/Main.java b/src/main/java/org/lab/Main.java index 22028ef..0fb2289 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,4 +1,12 @@ -void main() { - IO.println("Hello and welcome!"); -} +package org.lab; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/config/AuthInterceptor.java b/src/main/java/org/lab/config/AuthInterceptor.java new file mode 100644 index 0000000..b2574bf --- /dev/null +++ b/src/main/java/org/lab/config/AuthInterceptor.java @@ -0,0 +1,44 @@ +package org.lab.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.lab.service.UserService; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AuthInterceptor implements HandlerInterceptor { + + private final UserService userService; + + public AuthInterceptor(UserService userService) { + this.userService = userService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String userIdHeader = request.getHeader("X-User-Id"); + String authToken = request.getHeader("Authorization"); + + if (userIdHeader == null || authToken == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + try { + Long userId = Long.parseLong(userIdHeader); + + if (userService.findById(userId) == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + request.setAttribute("userId", userId); + return true; + + } catch (NumberFormatException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/config/DataInitializer.java b/src/main/java/org/lab/config/DataInitializer.java new file mode 100644 index 0000000..fb3e10e --- /dev/null +++ b/src/main/java/org/lab/config/DataInitializer.java @@ -0,0 +1,402 @@ +package org.lab.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.lab.entities.*; +import org.lab.enums.*; +import org.lab.repository.*; +import org.lab.util.BugType; +import org.lab.util.MilestoneTemplate; +import org.lab.util.TicketType; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class DataInitializer { + + private final UserRepository userRepository; + private final ProjectRepository projectRepository; + private final MilestoneRepository milestoneRepository; + private final TicketRepository ticketRepository; + private final BugReportRepository bugReportRepository; + + @Bean + @Profile("!test") + public CommandLineRunner initData() { + return args -> { + log.info("Инициализация тестовых данных..."); + + List users = createUsers(); + log.info(STR."Создано пользователей: \{users.size()}"); + + List projects = createProjects(users); + log.info(STR."Создано проектов: \{projects.size()}"); + + List milestones = createMilestones(projects); + log.info(STR."Создано майлстоунов: \{milestones.size()}"); + + List tickets = createTickets(projects, milestones, users); + log.info(STR."Создано тикетов: \{tickets.size()}"); + + List bugReports = createBugReports(projects, users); + log.info(STR."Создано баг-репортов: \{bugReports.size()}"); + + printStatistics(users, projects, milestones, tickets, bugReports); + + log.info("Инициализация тестовых данных завершена!"); + }; + } + + private List createUsers() { + record UserData(String username, String fullName, String email, SystemRole role) {} + + List userDataList = List.of( + new UserData("admin", "Администратор Системы", "admin@company.com", SystemRole.MANAGER), + new UserData("alex.manager", "Алексей Менеджеров", "alex@company.com", SystemRole.MANAGER), + new UserData("ivan.tl", "Иван Тимлидов", "ivan@company.com", SystemRole.TEAM_LEAD), + new UserData("maria.dev", "Мария Разработкина", "maria@company.com", SystemRole.DEVELOPER), + new UserData("sergey.dev", "Сергей Кодеров", "sergey@company.com", SystemRole.DEVELOPER), + new UserData("olga.dev", "Ольга Программистова", "olga@company.com", SystemRole.DEVELOPER), + new UserData("anna.tester", "Анна Тестировщикова", "anna@company.com", SystemRole.TESTER), + new UserData("dmitry.tester", "Дмитрий Багрепортов", "dmitry@company.com", SystemRole.TESTER), + new UserData("ekaterina.dev", "Екатерина Кодилова", "ekaterina@company.com", SystemRole.DEVELOPER), + new UserData("pavel.tl", "Павел Лидеров", "pavel@company.com", SystemRole.TEAM_LEAD) + ); + + return userDataList.stream() + .map(data -> User.builder() + .username(data.username()) + .password("password123") + .fullName(data.fullName()) + .email(data.email()) + .systemRole(data.role()) + .projectIds(new HashSet<>()) + .build()) + .map(userRepository::save) + .toList(); + } + + private List createProjects(List users) { + Map> usersByRole = users.stream() + .collect(java.util.stream.Collectors.groupingBy( + User::getSystemRole, + java.util.stream.Collectors.toList() + )); + + User manager = usersByRole.get(SystemRole.MANAGER).stream() + .findFirst() + .orElseThrow(); + + List developers = usersByRole.getOrDefault(SystemRole.DEVELOPER, List.of()); + List testers = usersByRole.getOrDefault(SystemRole.TESTER, List.of()); + List teamLeads = usersByRole.getOrDefault(SystemRole.TEAM_LEAD, List.of()); + + List projects = List.of( + createProject("E-Comm Platform", "Разработка платформы электронной коммерции", + manager, teamLeads, developers, testers), + createProject("Mobile Banking", "Мобильное банковское приложение", + manager, teamLeads, developers, testers), + createProject("AI Analytics", "Система аналитики на основе ИИ", + manager, teamLeads, developers, testers), + createProject("IoT Dashboard", "Дашборд для управления IoT устройствами", + manager, teamLeads, developers, testers) + ); + + return projects.stream() + .map(projectRepository::save) + .toList(); + } + + private Project createProject(String name, String description, + User manager, List teamLeads, + List developers, List testers) { + Random random = new Random(); + + return Project.builder() + .name(name) + .description(description) + .managerId(manager.getId()) + .teamLeadId(!teamLeads.isEmpty() ? + teamLeads.get(random.nextInt(teamLeads.size())).getId() : null) + .developerIds(developers.stream() + .limit(3) + .map(User::getId) + .collect(java.util.stream.Collectors.toSet())) + .testerIds(testers.stream() + .limit(2) + .map(User::getId) + .collect(java.util.stream.Collectors.toSet())) + .milestoneIds(new HashSet<>()) + .bugReportIds(new HashSet<>()) + .build(); + } + + private List createMilestones(List projects) { + List templates = List.of( + new MilestoneTemplate("Sprint 1", "Первая итерация разработки", 14), + new MilestoneTemplate("Sprint 2", "Вторая итерация разработки", 14), + new MilestoneTemplate("Alpha Release", "Альфа-релиз продукта", 30), + new MilestoneTemplate("Beta Release", "Бета-тестирование", 21), + new MilestoneTemplate("RC", "Release Candidate", 7) + ); + + AtomicLong counter = new AtomicLong(1); + + return projects.stream() + .flatMap(project -> templates.stream() + .map(template -> createMilestone(project, template, counter.getAndIncrement()))) + .map(milestoneRepository::save) + .toList(); + } + + private MileStone createMilestone(Project project, MilestoneTemplate template, long index) { + LocalDate startDate = LocalDate.now().plusDays(index * 14); + + return MileStone.builder() + .name(STR."\{template.name()} - \{project.getName()}") + .description(template.description()) + .projectId(project.getId()) + .startDate(startDate) + .endDate(startDate.plusDays(template.durationDays())) + .status(switch ((int) (index % 3)) { + case 0 -> MilestoneStatus.OPEN; + case 1 -> MilestoneStatus.ACTIVE; + default -> MilestoneStatus.CLOSED; + }) + .ticketIds(new HashSet<>()) + .build(); + } + + private List createTickets(List projects, + List milestones, + List users) { + List ticketTypes = List.of( + new TicketType("Реализовать", "Задача по реализации функциональности"), + new TicketType("Исправить", "Исправление обнаруженной проблемы"), + new TicketType("Доработать", "Доработка существующей функциональности"), + new TicketType("Протестировать", "Тестирование компонента системы"), + new TicketType("Документировать", "Написание документации") + ); + + List developers = users.stream() + .filter(u -> u.getSystemRole() == SystemRole.DEVELOPER) + .toList(); + + return projects.stream() + .flatMap(project -> { + List projectMilestones = milestones.stream() + .filter(m -> m.getProjectId().equals(project.getId())) + .toList(); + + return Stream.iterate(0, i -> i + 1) + .limit(10) + .map(i -> createTicket(project, projectMilestones, developers, ticketTypes, i)); + }) + .map(ticketRepository::save) + .toList(); + } + + private Ticket createTicket(Project project, List milestones, + List developers, List ticketTypes, int index) { + Random random = new Random(); + TicketType type = ticketTypes.get(random.nextInt(ticketTypes.size())); + MileStone milestone = milestones.get(random.nextInt(milestones.size())); + User developer = developers.get(random.nextInt(developers.size())); + + String title = STR."\{type.titlePrefix()} задача #\{index + 1}"; + + return Ticket.builder() + .title(title) + .description(type.generateDescription(project.getName())) + .projectId(project.getId()) + .milestoneId(milestone.getId()) + .status(switch (random.nextInt(4)) { + case 0 -> TicketStatus.NEW; + case 1 -> TicketStatus.ACCEPTED; + case 2 -> TicketStatus.IN_PROGRESS; + default -> TicketStatus.COMPLETED; + }) + .assignedUserId(developer.getId()) + .createdById(project.getManagerId()) + .createdAt(LocalDateTime.now().minusDays(random.nextInt(30))) + .updatedAt(LocalDateTime.now().minusDays(random.nextInt(7))) + .build(); + } + + private List createBugReports(List projects, List users) { + List bugTypes = List.of( + new BugType.CriticalBug(), new BugType.MajorBug(), new BugType.MinorBug(), new BugType.Enhancement() + ); + + List testers = users.stream() + .filter(u -> u.getSystemRole() == SystemRole.TESTER) + .toList(); + + List developers = users.stream() + .filter(u -> u.getSystemRole() == SystemRole.DEVELOPER) + .toList(); + + return projects.stream() + .flatMap(project -> { + Random random = new Random(); + + return Stream.iterate(0, i -> i + 1) + .limit(5) + .map(i -> { + BugType bugType = bugTypes.get(random.nextInt(bugTypes.size())); + User reporter = testers.get(random.nextInt(testers.size())); + User assignee = developers.get(random.nextInt(developers.size())); + + return createBugReport(project, bugType, reporter, assignee, i); + }); + }) + .map(bugReportRepository::save) + .toList(); + } + + private BugReport createBugReport(Project project, BugType bugType, + User reporter, User assignee, int index) { + String steps = switch (bugType) { + case BugType.CriticalBug b -> "1. Открыть систему\n2. Выполнить операцию X\n3. Система падает"; + case BugType.MajorBug b -> "1. Перейти в раздел Y\n2. Нажать кнопку Z\n3. Ожидаемый результат не достигнут"; + case BugType.MinorBug b -> "1. Открыть страницу\n2. Проверить выравнивание элементов"; + case BugType.Enhancement e -> "1. Проанализировать текущую реализацию\n2. Предложить улучшение"; + default -> "Шаги воспроизведения не указаны"; + }; + + return BugReport.builder() + .title(STR."\{bugType.getTitle()} #\{index + 1}") + .description(bugType.getDescription()) + .projectId(project.getId()) + .reportedById(reporter.getId()) + .assignedToId(assignee.getId()) + .status(bugType.getDefaultStatus()) + .createdAt(LocalDateTime.now().minusDays(new Random().nextInt(15))) + .updatedAt(LocalDateTime.now().minusDays(new Random().nextInt(5))) + .build(); + } + + private void printStatistics(List users, List projects, + List milestones, List tickets, + List bugReports) { + String stats = STR.""" + ================= СТАТИСТИКА ТЕСТОВЫХ ДАННЫХ ================= + Пользователей: \{users.size()} + • Менеджеры: \{countByRole(users, SystemRole.MANAGER)} + • Тимлиды: \{countByRole(users, SystemRole.TEAM_LEAD)} + • Разработчики: \{countByRole(users, SystemRole.DEVELOPER)} + • Тестировщики: \{countByRole(users, SystemRole.TESTER)} + + Проектов: \{projects.size()} + • С тимлидами: \{projects.stream().filter(p -> p.getTeamLeadId() != null).count()} + • Средняя команда: \{projects.stream() + .mapToInt(p -> p.getDeveloperIds().size() + p.getTesterIds().size() + 2) + .average() + .orElse(0)} чел. + + Майлстоунов: \{milestones.size()} + • Открыто: \{countByStatus(milestones, MilestoneStatus.OPEN)} + • Активно: \{countByStatus(milestones, MilestoneStatus.ACTIVE)} + • Закрыто: \{countByStatus(milestones, MilestoneStatus.CLOSED)} + + Тикетов: \{tickets.size()} + • Новые: \{countByStatus(tickets, TicketStatus.NEW)} + • Принятые: \{countByStatus(tickets, TicketStatus.ACCEPTED)} + • В работе: \{countByStatus(tickets, TicketStatus.IN_PROGRESS)} + • Выполненные: \{countByStatus(tickets, TicketStatus.COMPLETED)} + + Баг-репортов: \{bugReports.size()} + • Новые: \{countByStatus(bugReports, BugStatus.NEW)} + • Исправленные: \{countByStatus(bugReports, BugStatus.FIXED)} + • Протестированные: \{countByStatus(bugReports, BugStatus.TESTED)} + • Закрытые: \{countByStatus(bugReports, BugStatus.CLOSED)} + ============================================================== + """; + + log.info("\n{}", stats); + } + + private long countByRole(List users, SystemRole role) { + return users.stream() + .filter(user -> user.getSystemRole() == role) + .count(); + } + + private long countByStatus(List items, Enum status) { + return items.stream() + .filter(item -> { + try { + var statusField = item.getClass().getMethod("getStatus"); + var itemStatus = statusField.invoke(item); + return status.equals(itemStatus); + } catch (Exception e) { + return false; + } + }) + .count(); + } + + @Bean + public CommandLineRunner linkEntities() { + return args -> { + log.info("Связывание сущностей..."); + + ticketRepository.findAll().values().forEach(ticket -> { + MileStone milestone = milestoneRepository.findById(ticket.getMilestoneId()); + if (milestone != null) { + milestone.getTicketIds().add(ticket.getId()); + milestoneRepository.save(milestone); + } + }); + + bugReportRepository.findAll().values().forEach(bug -> { + Project project = projectRepository.findById(bug.getProjectId()); + if (project != null) { + project.getBugReportIds().add(bug.getId()); + projectRepository.save(project); + } + }); + + milestoneRepository.findAll().values().forEach(milestone -> { + Project project = projectRepository.findById(milestone.getProjectId()); + if (project != null) { + project.getMilestoneIds().add(milestone.getId()); + + if (milestone.getStatus() == MilestoneStatus.ACTIVE) { + project.setCurrentMilestoneId(milestone.getId()); + } + + projectRepository.save(project); + } + }); + + userRepository.findAll().values().forEach(user -> { + Set userProjectIds = new HashSet<>(); + + projectRepository.findAll().values().forEach(project -> { + if (project.getManagerId().equals(user.getId()) || + (project.getTeamLeadId() != null && project.getTeamLeadId().equals(user.getId())) || + project.getDeveloperIds().contains(user.getId()) || + project.getTesterIds().contains(user.getId())) { + userProjectIds.add(project.getId()); + } + }); + + user.setProjectIds(userProjectIds); + userRepository.save(user); + }); + + log.info("Связывание сущностей завершено!"); + }; + } +} diff --git a/src/main/java/org/lab/config/SecurityConfig.java b/src/main/java/org/lab/config/SecurityConfig.java new file mode 100644 index 0000000..e65c8d3 --- /dev/null +++ b/src/main/java/org/lab/config/SecurityConfig.java @@ -0,0 +1,27 @@ +package org.lab.config; + +import lombok.RequiredArgsConstructor; +import org.lab.service.UserService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig implements WebMvcConfigurer { + + private final UserService userService; + + @Bean + public AuthInterceptor authInterceptor() { + return new AuthInterceptor(userService); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor()) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/auth/**"); + } +} diff --git a/src/main/java/org/lab/controller/AuthController.java b/src/main/java/org/lab/controller/AuthController.java new file mode 100644 index 0000000..5a758ba --- /dev/null +++ b/src/main/java/org/lab/controller/AuthController.java @@ -0,0 +1,55 @@ +package org.lab.controller; + +import lombok.RequiredArgsConstructor; +import org.lab.dto.LoginRequest; +import org.lab.dto.RegisterRequest; +import org.lab.entities.User; +import org.lab.service.UserService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final UserService userService; + + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterRequest request) { + User user = User.builder() + .username(request.getUsername()) + .password(request.getPassword()) + .email(request.getEmail()) + .fullName(request.getFullName()) + .build(); + + User registeredUser = userService.register(user); + return ResponseEntity.ok(registeredUser); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + boolean isValid = userService.validateCredentials( + request.getUsername(), + request.getPassword() + ); + + if (isValid) { + User user = userService.findByUsername(request.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + return ResponseEntity.ok(user); + } + + return ResponseEntity.status(401).build(); + } + + @GetMapping("/me") + public ResponseEntity getCurrentUser(@RequestHeader("X-User-Id") Long userId) { + User user = userService.findById(userId); + if (user == null) { + return ResponseEntity.status(404).build(); + } + return ResponseEntity.ok(user); + } +} diff --git a/src/main/java/org/lab/controller/BugReportController.java b/src/main/java/org/lab/controller/BugReportController.java new file mode 100644 index 0000000..010049a --- /dev/null +++ b/src/main/java/org/lab/controller/BugReportController.java @@ -0,0 +1,122 @@ +package org.lab.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.lab.dto.CreateBugReportRequest; +import org.lab.entities.BugReport; +import org.lab.enums.BugStatus; +import org.lab.service.BugReportService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class BugReportController { + + private final BugReportService bugReportService; + + @GetMapping("/bugs/my") + public ResponseEntity> getMyBugReports(@RequestHeader("X-User-Id") Long userId) { + log.info("Getting bug reports for user: {}", userId); + List bugReports = bugReportService.getMyBugReports(userId); + return ResponseEntity.ok(bugReports); + } + + @GetMapping("/bugs/{bugId}") + public ResponseEntity getBugReport( + @PathVariable Long bugId, + @RequestHeader("X-User-Id") Long userId) { + log.info("Getting bug report: {} by user: {}", bugId, userId); + + BugReport bugReport = bugReportService.getBugReportById(bugId); + if (bugReport == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(bugReport); + } + + @GetMapping("/projects/{projectId}/bugs") + public ResponseEntity> getProjectBugReports( + @PathVariable Long projectId, + @RequestHeader("X-User-Id") Long userId) { + log.info("Getting bug reports for project: {} by user: {}", projectId, userId); + + List bugReports = bugReportService.getProjectBugReports(projectId); + return ResponseEntity.ok(bugReports); + } + + @PostMapping("/projects/{projectId}/bugs") + public ResponseEntity createBugReport( + @PathVariable Long projectId, + @RequestBody CreateBugReportRequest request, + @RequestHeader("X-User-Id") Long userId) { + log.info("Creating bug report for project: {} by user: {}", projectId, userId); + + BugReport bugReport = BugReport.builder() + .title(request.getTitle()) + .description(request.getDescription()) + .projectId(projectId) + .build(); + + BugReport created = bugReportService.createBugReport(bugReport, userId); + return ResponseEntity.ok(created); + } + + @PostMapping("/bugs/{bugId}/assign/{developerId}") + public ResponseEntity assignBugReport( + @PathVariable Long bugId, + @PathVariable Long developerId, + @RequestHeader("X-User-Id") Long userId) { + log.info("Assigning bug report: {} to developer: {} by user: {}", + bugId, developerId, userId); + + BugReport assigned = bugReportService.assignBugReport(bugId, developerId, userId); + return ResponseEntity.ok(assigned); + } + + @PatchMapping("/bugs/{bugId}/status") + public ResponseEntity updateBugStatus( + @PathVariable Long bugId, + @RequestParam BugStatus status, + @RequestHeader("X-User-Id") Long userId) { + log.info("Updating bug report: {} status to {} by user: {}", bugId, status, userId); + + BugReport updated = bugReportService.updateBugStatus(bugId, status, userId); + return ResponseEntity.ok(updated); + } + + @PostMapping("/bugs/{bugId}/fix") + public ResponseEntity markBugAsFixed( + @PathVariable Long bugId, + @RequestHeader("X-User-Id") Long userId) { + log.info("Marking bug report: {} as fixed by user: {}", bugId, userId); + + BugReport updated = bugReportService.updateBugStatus(bugId, BugStatus.FIXED, userId); + return ResponseEntity.ok(updated); + } + + @PostMapping("/bugs/{bugId}/test") + public ResponseEntity testBugFix( + @PathVariable Long bugId, + @RequestHeader("X-User-Id") Long userId) { + log.info("Testing bug fix: {} by user: {}", bugId, userId); + + BugReport updated = bugReportService.updateBugStatus(bugId, BugStatus.TESTED, userId); + return ResponseEntity.ok(updated); + } + + @PostMapping("/bugs/{bugId}/close") + public ResponseEntity closeBugReport( + @PathVariable Long bugId, + @RequestHeader("X-User-Id") Long userId) { + log.info("Closing bug report: {} by user: {}", bugId, userId); + + BugReport updated = bugReportService.updateBugStatus(bugId, BugStatus.CLOSED, userId); + return ResponseEntity.ok(updated); + } +} diff --git a/src/main/java/org/lab/controller/MilestoneController.java b/src/main/java/org/lab/controller/MilestoneController.java new file mode 100644 index 0000000..518384c --- /dev/null +++ b/src/main/java/org/lab/controller/MilestoneController.java @@ -0,0 +1,69 @@ +package org.lab.controller; + +import lombok.RequiredArgsConstructor; +import org.lab.dto.CreateMilestoneRequest; +import org.lab.entities.MileStone; +import org.lab.enums.MilestoneStatus; +import org.lab.service.MilestoneService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/projects/{projectId}/milestones") +@RequiredArgsConstructor +public class MilestoneController { + + private final MilestoneService milestoneService; + + @GetMapping + public ResponseEntity> getProjectMilestones( + @PathVariable Long projectId, + @RequestHeader("X-User-Id") Long userId + ) { + List milestones = milestoneService.getProjectMileStones(projectId); + return ResponseEntity.ok(milestones); + } + + @GetMapping("/current") + public ResponseEntity getCurrentMilestone( + @PathVariable Long projectId, + @RequestHeader("X-User-Id") Long userId + ) { + MileStone milestone = milestoneService.getCurrentMileStone(projectId); + if (milestone == null) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.ok(milestone); + } + + @PostMapping + public ResponseEntity createMilestone( + @PathVariable Long projectId, + @RequestBody CreateMilestoneRequest request, + @RequestHeader("X-User-Id") Long userId + ) { + MileStone milestone = MileStone.builder() + .name(request.getName()) + .description(request.getDescription()) + .projectId(projectId) + .startDate(request.getStartDate()) + .endDate(request.getEndDate()) + .build(); + + MileStone created = milestoneService.createMileStone(milestone, userId); + return ResponseEntity.ok(created); + } + + @PatchMapping("/{milestoneId}/status") + public ResponseEntity updateMilestoneStatus( + @PathVariable Long projectId, + @PathVariable Long milestoneId, + @RequestParam MilestoneStatus status, + @RequestHeader("X-User-Id") Long userId + ) { + MileStone updated = milestoneService.updateMilestoneStatus(milestoneId, status, userId); + return ResponseEntity.ok(updated); + } +} diff --git a/src/main/java/org/lab/controller/ProjectController.java b/src/main/java/org/lab/controller/ProjectController.java new file mode 100644 index 0000000..39b5ebd --- /dev/null +++ b/src/main/java/org/lab/controller/ProjectController.java @@ -0,0 +1,93 @@ +package org.lab.controller; + +import lombok.RequiredArgsConstructor; +import org.lab.dto.CreateProjectRequest; +import org.lab.entities.Project; +import org.lab.entities.User; +import org.lab.service.ProjectService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/projects") +@RequiredArgsConstructor +public class ProjectController { + + private final ProjectService projectService; + + @GetMapping + public ResponseEntity> getAllProjects(@RequestHeader("X-User-Id") Long userId) { + List projects = projectService.getProjectsByUser(userId); + return ResponseEntity.ok(projects); + } + + @GetMapping("/{projectId}") + public ResponseEntity getProject( + @PathVariable Long projectId, + @RequestHeader("X-User-Id") Long userId) { + Project project = projectService.getProjectById(projectId); + if (project == null) { + return ResponseEntity.notFound().build(); + } + + List userProjects = projectService.getProjectsByUser(userId); + if (userProjects.stream().noneMatch(p -> p.getId().equals(projectId))) { + return ResponseEntity.status(403).build(); + } + + return ResponseEntity.ok(project); + } + + @PostMapping + public ResponseEntity createProject(@RequestBody CreateProjectRequest request, @RequestHeader("X-User-Id") Long userId) { + Project project = Project.builder() + .name(request.getName()) + .description(request.getDescription()) + .build(); + + Project createdProject = projectService.createProject(project, userId); + return ResponseEntity.ok(createdProject); + } + + @PostMapping("/{projectId}/developers/{developerId}") + public ResponseEntity addDeveloper( + @PathVariable Long projectId, + @PathVariable Long developerId, + @RequestHeader("X-User-Id") Long userId) { + Project updated = projectService.addDeveloper(projectId, developerId, userId); + return ResponseEntity.ok(updated); + } + + @PostMapping("/{projectId}/testers/{testerId}") + public ResponseEntity addTester( + @PathVariable Long projectId, + @PathVariable Long testerId, + @RequestHeader("X-User-Id") Long userId) { + Project updated = projectService.addTester(projectId, testerId, userId); + return ResponseEntity.ok(updated); + } + + @PostMapping("/{projectId}/team-lead/{teamLeadId}") + public ResponseEntity assignTeamLead( + @PathVariable Long projectId, + @PathVariable Long teamLeadId, + @RequestHeader("X-User-Id") Long userId) { + Project updated = projectService.assignTeamLead(projectId, teamLeadId, userId); + return ResponseEntity.ok(updated); + } + + @GetMapping("/{projectId}/team") + public ResponseEntity> getProjectTeam( + @PathVariable Long projectId, + @RequestHeader("X-User-Id") Long userId) { + List userProjects = projectService.getProjectsByUser(userId); + if (userProjects.stream().noneMatch(p -> p.getId().equals(projectId))) { + return ResponseEntity.status(403).build(); + } + + List team = projectService.getProjectTeam(projectId); + return ResponseEntity.ok(team); + } +} diff --git a/src/main/java/org/lab/controller/TicketController.java b/src/main/java/org/lab/controller/TicketController.java new file mode 100644 index 0000000..b49d449 --- /dev/null +++ b/src/main/java/org/lab/controller/TicketController.java @@ -0,0 +1,100 @@ +package org.lab.controller; + +import lombok.RequiredArgsConstructor; +import org.lab.dto.CreateTicketRequest; +import org.lab.entities.Ticket; +import org.lab.enums.TicketStatus; +import org.lab.service.TicketService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class TicketController { + + private final TicketService ticketService; + + @GetMapping("/tickets/my") + public ResponseEntity> getMyTickets(@RequestHeader("X-User-Id") Long userId) { + List tickets = ticketService.getMyTickets(userId); + return ResponseEntity.ok(tickets); + } + + @GetMapping("/tickets/{ticketId}") + public ResponseEntity getTicket( + @PathVariable Long ticketId, + @RequestHeader("X-User-Id") Long userId) { + Ticket ticket = ticketService.getTicketById(ticketId); + if (ticket == null) { + return ResponseEntity.notFound().build(); + } + + List userTickets = ticketService.getMyTickets(userId); + if (userTickets.stream().noneMatch(t -> t.getId().equals(ticketId))) {} + + return ResponseEntity.ok(ticket); + } + + @GetMapping("/projects/{projectId}/tickets") + public ResponseEntity> getProjectTickets( + @PathVariable Long projectId, + @RequestHeader("X-User-Id") Long userId) { + List tickets = ticketService.getProjectTickets(projectId); + return ResponseEntity.ok(tickets); + } + + @GetMapping("/milestones/{milestoneId}/tickets") + public ResponseEntity> getMilestoneTickets( + @PathVariable Long milestoneId, + @RequestHeader("X-User-Id") Long userId) { + List tickets = ticketService.getMilestoneTickets(milestoneId); + return ResponseEntity.ok(tickets); + } + + @PostMapping("/projects/{projectId}/milestones/{milestoneId}/tickets") + public ResponseEntity createTicket( + @PathVariable Long projectId, + @PathVariable Long milestoneId, + @RequestBody CreateTicketRequest request, + @RequestHeader("X-User-Id") Long userId) { + Ticket ticket = Ticket.builder() + .title(request.getTitle()) + .description(request.getDescription()) + .projectId(projectId) + .milestoneId(milestoneId) + .build(); + + Ticket created = ticketService.createTicket(ticket, userId); + return ResponseEntity.ok(created); + } + + @PostMapping("/tickets/{ticketId}/assign/{developerId}") + public ResponseEntity assignTicket( + @PathVariable Long ticketId, + @PathVariable Long developerId, + @RequestHeader("X-User-Id") Long userId) { + Ticket assigned = ticketService.assignTicket(ticketId, developerId, userId); + return ResponseEntity.ok(assigned); + } + + @PatchMapping("/tickets/{ticketId}/status") + public ResponseEntity updateTicketStatus( + @PathVariable Long ticketId, + @RequestParam TicketStatus status, + @RequestHeader("X-User-Id") Long userId) { + Ticket updated = ticketService.updateTicketStatus(ticketId, status, userId); + return ResponseEntity.ok(updated); + } + + @PostMapping("/tickets/{ticketId}/start") + public ResponseEntity startWorkingOnTicket( + @PathVariable Long ticketId, + @RequestHeader("X-User-Id") Long userId) { + Ticket updated = ticketService.updateTicketStatus( + ticketId, TicketStatus.IN_PROGRESS, userId); + return ResponseEntity.ok(updated); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/dto/CreateBugReportRequest.java b/src/main/java/org/lab/dto/CreateBugReportRequest.java new file mode 100644 index 0000000..eee9c99 --- /dev/null +++ b/src/main/java/org/lab/dto/CreateBugReportRequest.java @@ -0,0 +1,14 @@ +package org.lab.dto; + +import lombok.Data; + +@Data +public class CreateBugReportRequest { + private String title; + + private String description; + + private String stepsToReproduce; + private String expectedBehavior; + private String actualBehavior; +} diff --git a/src/main/java/org/lab/dto/CreateMilestoneRequest.java b/src/main/java/org/lab/dto/CreateMilestoneRequest.java new file mode 100644 index 0000000..cb60655 --- /dev/null +++ b/src/main/java/org/lab/dto/CreateMilestoneRequest.java @@ -0,0 +1,16 @@ +package org.lab.dto; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class CreateMilestoneRequest { + + private String name; + + private String description; + + private LocalDate startDate; + private LocalDate endDate; +} diff --git a/src/main/java/org/lab/dto/CreateProjectRequest.java b/src/main/java/org/lab/dto/CreateProjectRequest.java new file mode 100644 index 0000000..3eef08f --- /dev/null +++ b/src/main/java/org/lab/dto/CreateProjectRequest.java @@ -0,0 +1,11 @@ +package org.lab.dto; + +import lombok.Data; + +@Data +public class CreateProjectRequest { + + private String name; + + private String description; +} diff --git a/src/main/java/org/lab/dto/CreateTicketRequest.java b/src/main/java/org/lab/dto/CreateTicketRequest.java new file mode 100644 index 0000000..1ee7765 --- /dev/null +++ b/src/main/java/org/lab/dto/CreateTicketRequest.java @@ -0,0 +1,10 @@ +package org.lab.dto; + +import lombok.Data; + +@Data +public class CreateTicketRequest { + + private String title; + private String description; +} diff --git a/src/main/java/org/lab/dto/LoginRequest.java b/src/main/java/org/lab/dto/LoginRequest.java new file mode 100644 index 0000000..b310e2a --- /dev/null +++ b/src/main/java/org/lab/dto/LoginRequest.java @@ -0,0 +1,9 @@ +package org.lab.dto; + +import lombok.Data; + +@Data +public class LoginRequest { + private String username; + private String password; +} diff --git a/src/main/java/org/lab/dto/RegisterRequest.java b/src/main/java/org/lab/dto/RegisterRequest.java new file mode 100644 index 0000000..80d9866 --- /dev/null +++ b/src/main/java/org/lab/dto/RegisterRequest.java @@ -0,0 +1,11 @@ +package org.lab.dto; + +import lombok.Data; + +@Data +public class RegisterRequest { + private String username; + private String password; + private String email; + private String fullName; +} diff --git a/src/main/java/org/lab/entities/BugReport.java b/src/main/java/org/lab/entities/BugReport.java new file mode 100644 index 0000000..044924a --- /dev/null +++ b/src/main/java/org/lab/entities/BugReport.java @@ -0,0 +1,25 @@ +package org.lab.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.lab.enums.BugStatus; + +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +@Builder +public class BugReport { + private Long id; + private String title; + private String description; + private Long projectId; + private Long reportedById; + private Long assignedToId; + private BugStatus status = BugStatus.NEW; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/org/lab/entities/MileStone.java b/src/main/java/org/lab/entities/MileStone.java new file mode 100644 index 0000000..f0521fb --- /dev/null +++ b/src/main/java/org/lab/entities/MileStone.java @@ -0,0 +1,26 @@ +package org.lab.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.lab.enums.MilestoneStatus; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class MileStone { + private Long id; + private String name; + private String description; + private Long projectId; + private LocalDate startDate; + private LocalDate endDate; + private MilestoneStatus status = MilestoneStatus.OPEN; + private Set ticketIds = new HashSet<>(); +} diff --git a/src/main/java/org/lab/entities/Project.java b/src/main/java/org/lab/entities/Project.java new file mode 100644 index 0000000..138d2ab --- /dev/null +++ b/src/main/java/org/lab/entities/Project.java @@ -0,0 +1,74 @@ +package org.lab.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +@AllArgsConstructor +@Builder +public class Project { + private Long id; + private String name; + private String description; + private Long managerId; + private Long teamLeadId; + + @Builder.Default + private Set developerIds = new HashSet<>(); + + @Builder.Default + private Set testerIds = new HashSet<>(); + + @Builder.Default + private Set milestoneIds = new HashSet<>(); + + @Builder.Default + private Set bugReportIds = new HashSet<>(); + + private Long currentMilestoneId; + + public Set getDeveloperIds() { + if (developerIds == null) { + developerIds = new HashSet<>(); + } + return developerIds; + } + + public Set getTesterIds() { + if (testerIds == null) { + testerIds = new HashSet<>(); + } + return testerIds; + } + + public Set getMilestoneIds() { + if (milestoneIds == null) { + milestoneIds = new HashSet<>(); + } + return milestoneIds; + } + + public Set getBugReportIds() { + if (bugReportIds == null) { + bugReportIds = new HashSet<>(); + } + return bugReportIds; + } + + public Set getAllUserIds() { + Set allUserIds = new HashSet<>(); + allUserIds.add(managerId); + if (teamLeadId != null) { + allUserIds.add(teamLeadId); + } + allUserIds.addAll(getDeveloperIds()); + allUserIds.addAll(getTesterIds()); + return allUserIds; + } +} diff --git a/src/main/java/org/lab/entities/Ticket.java b/src/main/java/org/lab/entities/Ticket.java new file mode 100644 index 0000000..ca04b0a --- /dev/null +++ b/src/main/java/org/lab/entities/Ticket.java @@ -0,0 +1,26 @@ +package org.lab.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.lab.enums.TicketStatus; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class Ticket { + private Long id; + private String title; + private String description; + private Long projectId; + private Long milestoneId; + private TicketStatus status = TicketStatus.NEW; + private Long assignedUserId; + private Long createdById; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/org/lab/entities/User.java b/src/main/java/org/lab/entities/User.java new file mode 100644 index 0000000..1d127c2 --- /dev/null +++ b/src/main/java/org/lab/entities/User.java @@ -0,0 +1,23 @@ +package org.lab.entities; + +import lombok.*; +import org.lab.enums.SystemRole; + +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class User { + + private Long id; + private String username; + private String password; + private String email; + private String fullName; + private SystemRole systemRole; + private Set projectIds; +} diff --git a/src/main/java/org/lab/enums/BugStatus.java b/src/main/java/org/lab/enums/BugStatus.java new file mode 100644 index 0000000..4530111 --- /dev/null +++ b/src/main/java/org/lab/enums/BugStatus.java @@ -0,0 +1,8 @@ +package org.lab.enums; + +public enum BugStatus { + NEW, + FIXED, + TESTED, + CLOSED +} diff --git a/src/main/java/org/lab/enums/MilestoneStatus.java b/src/main/java/org/lab/enums/MilestoneStatus.java new file mode 100644 index 0000000..a798e91 --- /dev/null +++ b/src/main/java/org/lab/enums/MilestoneStatus.java @@ -0,0 +1,7 @@ +package org.lab.enums; + +public enum MilestoneStatus { + OPEN, + ACTIVE, + CLOSED +} diff --git a/src/main/java/org/lab/enums/SystemRole.java b/src/main/java/org/lab/enums/SystemRole.java new file mode 100644 index 0000000..afeaf0d --- /dev/null +++ b/src/main/java/org/lab/enums/SystemRole.java @@ -0,0 +1,9 @@ +package org.lab.enums; + +public enum SystemRole { + MANAGER, + TEAM_LEAD, + DEVELOPER, + TESTER +} + diff --git a/src/main/java/org/lab/enums/TicketStatus.java b/src/main/java/org/lab/enums/TicketStatus.java new file mode 100644 index 0000000..b396dbb --- /dev/null +++ b/src/main/java/org/lab/enums/TicketStatus.java @@ -0,0 +1,8 @@ +package org.lab.enums; + +public enum TicketStatus { + NEW, + ACCEPTED, + IN_PROGRESS, + COMPLETED +} diff --git a/src/main/java/org/lab/repository/BugReportRepository.java b/src/main/java/org/lab/repository/BugReportRepository.java new file mode 100644 index 0000000..9b49b2e --- /dev/null +++ b/src/main/java/org/lab/repository/BugReportRepository.java @@ -0,0 +1,31 @@ +package org.lab.repository; + +import org.lab.entities.BugReport; +import org.lab.enums.BugStatus; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +public class BugReportRepository extends InMemoryRepository { + + public List findByProjectId(Long projectId) { + return storage.values().stream() + .filter(bug -> bug.getProjectId().equals(projectId)) + .collect(Collectors.toList()); + } + + public List findByAssignedToId(Long userId) { + return storage.values().stream() + .filter(bug -> userId.equals(bug.getAssignedToId())) + .collect(Collectors.toList()); + } + + public List findByStatusAndProjectId(BugStatus status, Long projectId) { + return storage.values().stream() + .filter(bug -> bug.getProjectId().equals(projectId) && + bug.getStatus() == status) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/lab/repository/InMemoryRepository.java b/src/main/java/org/lab/repository/InMemoryRepository.java new file mode 100644 index 0000000..40b946e --- /dev/null +++ b/src/main/java/org/lab/repository/InMemoryRepository.java @@ -0,0 +1,44 @@ +package org.lab.repository; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public abstract class InMemoryRepository { + protected final Map storage = new ConcurrentHashMap<>(); + protected final AtomicLong idGenerator = new AtomicLong(1); + + public T save(T entity) { + try { + var idField = entity.getClass().getDeclaredField("id"); + idField.setAccessible(true); + Long id = (Long) idField.get(entity); + + if (id == null) { + id = idGenerator.getAndIncrement(); + idField.set(entity, id); + } + + storage.put(id, entity); + return entity; + } catch (Exception e) { + throw new RuntimeException("Error saving entity", e); + } + } + + public T findById(Long id) { + return storage.get(id); + } + + public void deleteById(Long id) { + storage.remove(id); + } + + public boolean existsById(Long id) { + return storage.containsKey(id); + } + + public Map findAll() { + return new ConcurrentHashMap<>(storage); + } +} diff --git a/src/main/java/org/lab/repository/MilestoneRepository.java b/src/main/java/org/lab/repository/MilestoneRepository.java new file mode 100644 index 0000000..c7d1327 --- /dev/null +++ b/src/main/java/org/lab/repository/MilestoneRepository.java @@ -0,0 +1,26 @@ +package org.lab.repository; + +import org.lab.entities.MileStone; +import org.lab.enums.MilestoneStatus; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +public class MilestoneRepository extends InMemoryRepository { + + public List findByProjectId(Long projectId) { + return storage.values().stream() + .filter(milestone -> milestone.getProjectId().equals(projectId)) + .collect(Collectors.toList()); + } + + public Optional findActiveByProjectId(Long projectId) { + return storage.values().stream() + .filter(milestone -> milestone.getProjectId().equals(projectId) && + milestone.getStatus() == MilestoneStatus.ACTIVE) + .findFirst(); + } +} diff --git a/src/main/java/org/lab/repository/ProjectRepository.java b/src/main/java/org/lab/repository/ProjectRepository.java new file mode 100644 index 0000000..126dbe8 --- /dev/null +++ b/src/main/java/org/lab/repository/ProjectRepository.java @@ -0,0 +1,26 @@ +package org.lab.repository; + +import org.lab.entities.Project; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +public class ProjectRepository extends InMemoryRepository { + + public List findByUserId(Long userId) { + return storage.values().stream() + .filter(project -> project.getManagerId().equals(userId) || + project.getTeamLeadId() != null && project.getTeamLeadId().equals(userId) || + project.getDeveloperIds().contains(userId) || + project.getTesterIds().contains(userId)) + .collect(Collectors.toList()); + } + + public List findByManagerId(Long managerId) { + return storage.values().stream() + .filter(project -> project.getManagerId().equals(managerId)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/lab/repository/TicketRepository.java b/src/main/java/org/lab/repository/TicketRepository.java new file mode 100644 index 0000000..13dfa31 --- /dev/null +++ b/src/main/java/org/lab/repository/TicketRepository.java @@ -0,0 +1,37 @@ +package org.lab.repository; + +import org.lab.entities.Ticket; +import org.lab.enums.TicketStatus; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +public class TicketRepository extends InMemoryRepository { + + public List findByProjectId(Long projectId) { + return storage.values().stream() + .filter(ticket -> ticket.getProjectId().equals(projectId)) + .collect(Collectors.toList()); + } + + public List findByMilestoneId(Long milestoneId) { + return storage.values().stream() + .filter(ticket -> ticket.getMilestoneId().equals(milestoneId)) + .collect(Collectors.toList()); + } + + public List findByAssignedUserId(Long userId) { + return storage.values().stream() + .filter(ticket -> userId.equals(ticket.getAssignedUserId())) + .collect(Collectors.toList()); + } + + public List findByStatusAndMilestoneId(TicketStatus status, Long milestoneId) { + return storage.values().stream() + .filter(ticket -> ticket.getMilestoneId().equals(milestoneId) && + ticket.getStatus() == status) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/lab/repository/UserRepository.java b/src/main/java/org/lab/repository/UserRepository.java new file mode 100644 index 0000000..a1eb34e --- /dev/null +++ b/src/main/java/org/lab/repository/UserRepository.java @@ -0,0 +1,22 @@ +package org.lab.repository; + +import org.lab.entities.User; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public class UserRepository extends InMemoryRepository { + + public Optional findByUsername(String username) { + return storage.values().stream() + .filter(user -> username.equals(user.getUsername())) + .findFirst(); + } + + public Optional findByEmail(String email) { + return storage.values().stream() + .filter(user -> email.equals(user.getEmail())) + .findFirst(); + } +} diff --git a/src/main/java/org/lab/service/BugReportService.java b/src/main/java/org/lab/service/BugReportService.java new file mode 100644 index 0000000..637fb0c --- /dev/null +++ b/src/main/java/org/lab/service/BugReportService.java @@ -0,0 +1,177 @@ +package org.lab.service; + +import lombok.RequiredArgsConstructor; +import org.lab.entities.BugReport; +import org.lab.entities.Project; +import org.lab.entities.User; +import org.lab.enums.BugStatus; +import org.lab.enums.SystemRole; +import org.lab.repository.BugReportRepository; +import org.lab.repository.ProjectRepository; +import org.lab.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BugReportService { + + private final BugReportRepository bugReportRepository; + private final ProjectRepository projectRepository; + private final UserRepository userRepository; + + public BugReport createBugReport(BugReport bugReport, Long reporterId) { + User reporter = userRepository.findById(reporterId); + Project project = projectRepository.findById(bugReport.getProjectId()); + + if (reporter == null || project == null) { + throw new IllegalArgumentException("Reporter or project not found"); + } + + if (!isUserInProject(reporter, project)) { + throw new IllegalArgumentException("You must be part of the project to report bugs"); + } + + bugReport.setReportedById(reporterId); + bugReport.setStatus(BugStatus.NEW); + bugReport.setCreatedAt(LocalDateTime.now()); + bugReport.setUpdatedAt(LocalDateTime.now()); + + BugReport savedBug = bugReportRepository.save(bugReport); + + project.getBugReportIds().add(savedBug.getId()); + projectRepository.save(project); + + return savedBug; + } + + public BugReport assignBugReport(Long bugId, Long developerId, Long assignerId) { + BugReport bugReport = bugReportRepository.findById(bugId); + User assigner = userRepository.findById(assignerId); + User developer = userRepository.findById(developerId); + + if (bugReport == null || assigner == null || developer == null) { + throw new IllegalArgumentException("Bug report, assigner or developer not found"); + } + + Project project = projectRepository.findById(bugReport.getProjectId()); + + if (!hasBugAssignmentRights(assigner, project)) { + throw new IllegalArgumentException("You don't have permission to assign bug reports"); + } + + if (!project.getDeveloperIds().contains(developerId)) { + throw new IllegalArgumentException("Developer is not part of this project"); + } + + bugReport.setAssignedToId(developerId); + bugReport.setUpdatedAt(LocalDateTime.now()); + + return bugReportRepository.save(bugReport); + } + + public BugReport updateBugStatus(Long bugId, BugStatus newStatus, Long userId) { + BugReport bugReport = bugReportRepository.findById(bugId); + User user = userRepository.findById(userId); + + if (bugReport == null || user == null) { + throw new IllegalArgumentException("Bug report or user not found"); + } + + Project project = projectRepository.findById(bugReport.getProjectId()); + + validateBugStatusUpdatePermission(bugReport, user, project, newStatus); + + validateBugStatusTransition(bugReport.getStatus(), newStatus, user.getSystemRole()); + + bugReport.setStatus(newStatus); + bugReport.setUpdatedAt(LocalDateTime.now()); + + return bugReportRepository.save(bugReport); + } + + public List getMyBugReports(Long userId) { + return bugReportRepository.findByAssignedToId(userId); + } + + public List getProjectBugReports(Long projectId) { + return bugReportRepository.findByProjectId(projectId); + } + + public BugReport getBugReportById(Long bugId) { + return bugReportRepository.findById(bugId); + } + + private boolean isUserInProject(User user, Project project) { + return project.getManagerId().equals(user.getId()) || + (project.getTeamLeadId() != null && project.getTeamLeadId().equals(user.getId())) || + project.getDeveloperIds().contains(user.getId()) || + project.getTesterIds().contains(user.getId()); + } + + private boolean hasBugAssignmentRights(User user, Project project) { + return user.getSystemRole() == SystemRole.MANAGER || + user.getSystemRole() == SystemRole.TEAM_LEAD || + (user.getSystemRole() == SystemRole.TESTER && project.getTesterIds().contains(user.getId())); + } + + private void validateBugStatusUpdatePermission(BugReport bugReport, User user, Project project, BugStatus newStatus) { + switch (user.getSystemRole()) { + case DEVELOPER: + if (!bugReport.getAssignedToId().equals(user.getId())) { + throw new IllegalArgumentException("You can only update your assigned bug reports"); + } + if (newStatus != BugStatus.FIXED) { + throw new IllegalArgumentException("Developers can only mark bugs as FIXED"); + } + break; + + case TESTER: + if (newStatus == BugStatus.TESTED && bugReport.getStatus() != BugStatus.FIXED) { + throw new IllegalArgumentException("Only fixed bugs can be marked as tested"); + } + if (newStatus == BugStatus.CLOSED && bugReport.getStatus() != BugStatus.TESTED) { + throw new IllegalArgumentException("Only tested bugs can be closed"); + } + break; + + case MANAGER: + if (!project.getManagerId().equals(user.getId())) { + throw new IllegalArgumentException("You are not the manager of this project"); + } + break; + + case TEAM_LEAD: + if (project.getTeamLeadId() == null || !project.getTeamLeadId().equals(user.getId())) { + throw new IllegalArgumentException("You are not the team lead of this project"); + } + break; + } + } + + private void validateBugStatusTransition(BugStatus current, BugStatus next, SystemRole userRole) { + if (current == BugStatus.CLOSED) { + throw new IllegalStateException("Cannot modify closed bug report"); + } + + switch (next) { + case FIXED: + if (current != BugStatus.NEW) { + throw new IllegalStateException("Can only fix NEW bugs"); + } + break; + case TESTED: + if (current != BugStatus.FIXED) { + throw new IllegalStateException("Can only test FIXED bugs"); + } + break; + case CLOSED: + if (current != BugStatus.TESTED) { + throw new IllegalStateException("Can only close TESTED bugs"); + } + break; + } + } +} diff --git a/src/main/java/org/lab/service/MilestoneService.java b/src/main/java/org/lab/service/MilestoneService.java new file mode 100644 index 0000000..e216320 --- /dev/null +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -0,0 +1,126 @@ +package org.lab.service; + +import lombok.RequiredArgsConstructor; +import org.lab.entities.MileStone; +import org.lab.entities.Project; +import org.lab.entities.Ticket; +import org.lab.entities.User; +import org.lab.enums.MilestoneStatus; +import org.lab.enums.SystemRole; +import org.lab.enums.TicketStatus; +import org.lab.repository.MilestoneRepository; +import org.lab.repository.ProjectRepository; +import org.lab.repository.TicketRepository; +import org.lab.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class MilestoneService { + + private final MilestoneRepository mileStoneRepository; + private final ProjectRepository projectRepository; + private final TicketRepository ticketRepository; + private final UserRepository userRepository; + + public MileStone createMileStone(MileStone MileStone, Long creatorId) { + User creator = userRepository.findById(creatorId); + Project project = projectRepository.findById(MileStone.getProjectId()); + + if (creator == null || project == null) { + throw new IllegalArgumentException("User or project not found"); + } + + if (!hasMileStoneManagementRights(creator, project)) { + throw new IllegalArgumentException("You don't have permission to create MileStones"); + } + + Optional activeMileStone = mileStoneRepository.findActiveByProjectId(project.getId()); + if (activeMileStone.isPresent()) { + throw new IllegalStateException("Project already has an active MileStone"); + } + + MileStone.setStatus(MilestoneStatus.OPEN); + MileStone savedMileStone = mileStoneRepository.save(MileStone); + + project.getMilestoneIds().add(savedMileStone.getId()); + projectRepository.save(project); + + return savedMileStone; + } + + public MileStone updateMilestoneStatus(Long MileStoneId, MilestoneStatus newStatus, Long userId) { + MileStone MileStone = mileStoneRepository.findById(MileStoneId); + User user = userRepository.findById(userId); + + if (MileStone == null || user == null) { + throw new IllegalArgumentException("MileStone or user not found"); + } + + Project project = projectRepository.findById(MileStone.getProjectId()); + + if (!hasMileStoneManagementRights(user, project)) { + throw new IllegalArgumentException("You don't have permission to update MileStone status"); + } + + validateStatusTransition(MileStone.getStatus(), newStatus); + + if (newStatus == MilestoneStatus.CLOSED) { + List incompleteTickets = ticketRepository.findByStatusAndMilestoneId( + TicketStatus.COMPLETED, MileStoneId); + if (!incompleteTickets.isEmpty()) { + throw new IllegalStateException("Cannot close MileStone with incomplete tickets"); + } + } + + if (newStatus == MilestoneStatus.ACTIVE) { + deactivateOtherMileStones(MileStone.getProjectId()); + } + + MileStone.setStatus(newStatus); + return mileStoneRepository.save(MileStone); + } + + public List getProjectMileStones(Long projectId) { + return mileStoneRepository.findByProjectId(projectId); + } + + public MileStone getCurrentMileStone(Long projectId) { + return mileStoneRepository.findActiveByProjectId(projectId) + .orElse(null); + } + + private boolean hasMileStoneManagementRights(User user, Project project) { + if (user.getSystemRole() == SystemRole.MANAGER) { + return project.getManagerId().equals(user.getId()); + } else if (user.getSystemRole() == SystemRole.TEAM_LEAD) { + return project.getTeamLeadId() != null && + project.getTeamLeadId().equals(user.getId()); + } + return false; + } + + private void validateStatusTransition(MilestoneStatus current, MilestoneStatus next) { + if (current == MilestoneStatus.CLOSED) { + throw new IllegalStateException("Cannot modify closed MileStone"); + } + + if (current == MilestoneStatus.OPEN && next == MilestoneStatus.CLOSED) { + throw new IllegalStateException("Cannot close MileStone without activating it first"); + } + } + + private void deactivateOtherMileStones(Long projectId) { + List projectMileStones = mileStoneRepository.findByProjectId(projectId); + projectMileStones.stream() + .filter(m -> m.getStatus() == MilestoneStatus.ACTIVE) + .forEach(m -> { + m.setStatus(MilestoneStatus.OPEN); + mileStoneRepository.save(m); + }); + } +} diff --git a/src/main/java/org/lab/service/ProjectService.java b/src/main/java/org/lab/service/ProjectService.java new file mode 100644 index 0000000..6ca020b --- /dev/null +++ b/src/main/java/org/lab/service/ProjectService.java @@ -0,0 +1,128 @@ +package org.lab.service; + +import lombok.RequiredArgsConstructor; +import org.lab.entities.Project; +import org.lab.entities.User; +import org.lab.enums.SystemRole; +import org.lab.repository.ProjectRepository; +import org.lab.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class ProjectService { + + private final ProjectRepository projectRepository; + private final UserRepository userRepository; + + public Project createProject(Project project, Long managerId) { + User manager = userRepository.findById(managerId); + if (manager == null || manager.getSystemRole() != SystemRole.MANAGER) { + throw new IllegalArgumentException("Only managers can create projects"); + } + + project.setManagerId(managerId); + Project savedProject = projectRepository.save(project); + + manager.getProjectIds().add(savedProject.getId()); + userRepository.save(manager); + + return savedProject; + } + + public List getProjectsByUser(Long userId) { + return projectRepository.findByUserId(userId); + } + + public Project getProjectById(Long projectId) { + return projectRepository.findById(projectId); + } + + public Project addDeveloper(Long projectId, Long developerId, Long managerId) { + Project project = projectRepository.findById(projectId); + User manager = userRepository.findById(managerId); + User developer = userRepository.findById(developerId); + + validateManagerAccess(project, manager); + + if (developer == null || developer.getSystemRole() != SystemRole.DEVELOPER) { + throw new IllegalArgumentException("User must be a developer"); + } + + project.getDeveloperIds().add(developerId); + developer.getProjectIds().add(projectId); + + userRepository.save(developer); + return projectRepository.save(project); + } + + public Project addTester(Long projectId, Long testerId, Long managerId) { + Project project = projectRepository.findById(projectId); + User manager = userRepository.findById(managerId); + User tester = userRepository.findById(testerId); + + validateManagerAccess(project, manager); + + if (tester == null || tester.getSystemRole() != SystemRole.TESTER) { + throw new IllegalArgumentException("User must be a tester"); + } + + project.getTesterIds().add(testerId); + tester.getProjectIds().add(projectId); + + userRepository.save(tester); + return projectRepository.save(project); + } + + public Project assignTeamLead(Long projectId, Long teamLeadId, Long managerId) { + Project project = projectRepository.findById(projectId); + User manager = userRepository.findById(managerId); + User teamLead = userRepository.findById(teamLeadId); + + validateManagerAccess(project, manager); + + if (teamLead == null || teamLead.getSystemRole() != SystemRole.TEAM_LEAD) { + throw new IllegalArgumentException("User must be a team lead"); + } + + project.setTeamLeadId(teamLeadId); + teamLead.getProjectIds().add(projectId); + + userRepository.save(teamLead); + return projectRepository.save(project); + } + + public List getProjectTeam(Long projectId) { + Project project = projectRepository.findById(projectId); + Set allUserIds = project.getAllUserIds(); + return userRepository.findAll().values().stream() + .filter(user -> allUserIds.contains(user.getId())) + .collect(java.util.stream.Collectors.toList()); + } + + private void validateManagerAccess(Project project, User manager) { + if (project == null) { + throw new IllegalArgumentException("Project not found"); + } + if (manager == null || manager.getSystemRole() != SystemRole.MANAGER) { + throw new IllegalArgumentException("Only project manager can perform this action"); + } + if (!project.getManagerId().equals(manager.getId())) { + throw new IllegalArgumentException("You are not the manager of this project"); + } + } + + private Set getAllUserIds(Project project) { + Set userIds = new java.util.HashSet<>(); + userIds.add(project.getManagerId()); + if (project.getTeamLeadId() != null) { + userIds.add(project.getTeamLeadId()); + } + userIds.addAll(project.getDeveloperIds()); + userIds.addAll(project.getTesterIds()); + return userIds; + } +} diff --git a/src/main/java/org/lab/service/TicketService.java b/src/main/java/org/lab/service/TicketService.java new file mode 100644 index 0000000..3b5c616 --- /dev/null +++ b/src/main/java/org/lab/service/TicketService.java @@ -0,0 +1,155 @@ +package org.lab.service; + +import lombok.RequiredArgsConstructor; +import org.lab.entities.MileStone; +import org.lab.entities.Project; +import org.lab.entities.Ticket; +import org.lab.entities.User; +import org.lab.enums.SystemRole; +import org.lab.enums.TicketStatus; +import org.lab.repository.MilestoneRepository; +import org.lab.repository.ProjectRepository; +import org.lab.repository.TicketRepository; +import org.lab.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TicketService { + + private final TicketRepository ticketRepository; + private final ProjectRepository projectRepository; + private final MilestoneRepository milestoneRepository; + private final UserRepository userRepository; + + public Ticket createTicket(Ticket ticket, Long creatorId) { + User creator = userRepository.findById(creatorId); + Project project = projectRepository.findById(ticket.getProjectId()); + MileStone milestone = milestoneRepository.findById(ticket.getMilestoneId()); + + if (creator == null || project == null || milestone == null) { + throw new IllegalArgumentException("Creator, project or milestone not found"); + } + + if (!hasTicketManagementRights(creator, project)) { + throw new IllegalArgumentException("You don't have permission to create tickets"); + } + + if (!milestone.getProjectId().equals(project.getId())) { + throw new IllegalArgumentException("Milestone does not belong to this project"); + } + + ticket.setStatus(TicketStatus.NEW); + ticket.setCreatedById(creatorId); + ticket.setCreatedAt(LocalDateTime.now()); + ticket.setUpdatedAt(LocalDateTime.now()); + + return ticketRepository.save(ticket); + } + + public Ticket assignTicket(Long ticketId, Long developerId, Long assignerId) { + Ticket ticket = ticketRepository.findById(ticketId); + User assigner = userRepository.findById(assignerId); + User developer = userRepository.findById(developerId); + + if (ticket == null || assigner == null || developer == null) { + throw new IllegalArgumentException("Ticket, assigner or developer not found"); + } + + Project project = projectRepository.findById(ticket.getProjectId()); + + if (!hasTicketManagementRights(assigner, project)) { + throw new IllegalArgumentException("You don't have permission to assign tickets"); + } + + if (!project.getDeveloperIds().contains(developerId)) { + throw new IllegalArgumentException("Developer is not part of this project"); + } + + ticket.setAssignedUserId(developerId); + ticket.setStatus(TicketStatus.ACCEPTED); + ticket.setUpdatedAt(LocalDateTime.now()); + + return ticketRepository.save(ticket); + } + + public Ticket updateTicketStatus(Long ticketId, TicketStatus newStatus, Long userId) { + Ticket ticket = ticketRepository.findById(ticketId); + User user = userRepository.findById(userId); + + if (ticket == null || user == null) { + throw new IllegalArgumentException("Ticket or user not found"); + } + + Project project = projectRepository.findById(ticket.getProjectId()); + + validateStatusUpdatePermission(ticket, user, project, newStatus); + + validateTicketStatusTransition(ticket.getStatus(), newStatus); + + ticket.setStatus(newStatus); + ticket.setUpdatedAt(LocalDateTime.now()); + + return ticketRepository.save(ticket); + } + + public List getMyTickets(Long userId) { + return ticketRepository.findByAssignedUserId(userId); + } + + public List getProjectTickets(Long projectId) { + return ticketRepository.findByProjectId(projectId); + } + + public List getMilestoneTickets(Long milestoneId) { + return ticketRepository.findByMilestoneId(milestoneId); + } + + public Ticket getTicketById(Long ticketId) { + return ticketRepository.findById(ticketId); + } + + private boolean hasTicketManagementRights(User user, Project project) { + if (user.getSystemRole() == SystemRole.MANAGER) { + return project.getManagerId().equals(user.getId()); + } else if (user.getSystemRole() == SystemRole.TEAM_LEAD) { + return project.getTeamLeadId() != null && + project.getTeamLeadId().equals(user.getId()); + } + return false; + } + + private void validateStatusUpdatePermission(Ticket ticket, User user, Project project, TicketStatus newStatus) { + if (user.getSystemRole() == SystemRole.DEVELOPER) { + if (!ticket.getAssignedUserId().equals(user.getId())) { + throw new IllegalArgumentException("You can only update your own tickets"); + } + if (newStatus == TicketStatus.COMPLETED) { + throw new IllegalArgumentException("Only manager or team lead can mark ticket as completed"); + } + } + + if (user.getSystemRole() == SystemRole.MANAGER) { + if (!project.getManagerId().equals(user.getId())) { + throw new IllegalArgumentException("You are not the manager of this project"); + } + } else if (user.getSystemRole() == SystemRole.TEAM_LEAD) { + if (project.getTeamLeadId() == null || !project.getTeamLeadId().equals(user.getId())) { + throw new IllegalArgumentException("You are not the team lead of this project"); + } + } + } + + private void validateTicketStatusTransition(TicketStatus current, TicketStatus next) { + if (current == TicketStatus.COMPLETED) { + throw new IllegalStateException("Cannot modify completed ticket"); + } + + if (current == TicketStatus.NEW && next == TicketStatus.IN_PROGRESS) { + throw new IllegalStateException("Ticket must be accepted before being in progress"); + } + } +} diff --git a/src/main/java/org/lab/service/UserService.java b/src/main/java/org/lab/service/UserService.java new file mode 100644 index 0000000..09aade5 --- /dev/null +++ b/src/main/java/org/lab/service/UserService.java @@ -0,0 +1,56 @@ +package org.lab.service; + +import lombok.RequiredArgsConstructor; +import org.lab.entities.User; +import org.lab.enums.SystemRole; +import org.lab.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public User register(User user) { + if (userRepository.findByUsername(user.getUsername()).isPresent()) { + throw new IllegalArgumentException("Username already exists"); + } + + if (user.getSystemRole() == null) { + user.setSystemRole(SystemRole.DEVELOPER); + } + + return userRepository.save(user); + } + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public User findById(Long id) { + return userRepository.findById(id); + } + + public boolean validateCredentials(String username, String password) { + return userRepository.findByUsername(username) + .map(user -> password.equals(user.getPassword())) + .orElse(false); + } + + public List getUsersByRole(SystemRole role) { + return userRepository.findAll().values().stream() + .filter(user -> user.getSystemRole() == role) + .collect(Collectors.toList()); + } + + public List getUsersByIds(List userIds) { + return userRepository.findAll().values().stream() + .filter(user -> userIds.contains(user.getId())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/lab/util/BugType.java b/src/main/java/org/lab/util/BugType.java new file mode 100644 index 0000000..b315d50 --- /dev/null +++ b/src/main/java/org/lab/util/BugType.java @@ -0,0 +1,62 @@ +package org.lab.util; + +import org.lab.enums.BugStatus; + +public sealed interface BugType permits BugType.CriticalBug, BugType.MajorBug, BugType.MinorBug, BugType.Enhancement { + + String getTitle(); + String getDescription(); + BugStatus getDefaultStatus(); + + record CriticalBug() implements BugType { + @Override + public String getTitle() { return "Критический баг"; } + + @Override + public String getDescription() { + return "Система падает или критическая функциональность не работает"; + } + + @Override + public BugStatus getDefaultStatus() { return BugStatus.NEW; } + } + + record MajorBug() implements BugType { + @Override + public String getTitle() { return "Существенный баг"; } + + @Override + public String getDescription() { + return "Основная функциональность работает некорректно"; + } + + @Override + public BugStatus getDefaultStatus() { return BugStatus.FIXED; } + } + + record MinorBug() implements BugType { + @Override + public String getTitle() { return "Незначительный баг"; } + + @Override + public String getDescription() { + return "Небольшая проблема с UI или косметическая ошибка"; + } + + @Override + public BugStatus getDefaultStatus() { return BugStatus.TESTED; } + } + + record Enhancement() implements BugType { + @Override + public String getTitle() { return "Улучшение"; } + + @Override + public String getDescription() { + return "Предложение по улучшению существующей функциональности"; + } + + @Override + public BugStatus getDefaultStatus() { return BugStatus.CLOSED; } + } +} diff --git a/src/main/java/org/lab/util/MilestoneTemplate.java b/src/main/java/org/lab/util/MilestoneTemplate.java new file mode 100644 index 0000000..dc30e77 --- /dev/null +++ b/src/main/java/org/lab/util/MilestoneTemplate.java @@ -0,0 +1,3 @@ +package org.lab.util; + +public record MilestoneTemplate(String name, String description, int durationDays) {} diff --git a/src/main/java/org/lab/util/TicketType.java b/src/main/java/org/lab/util/TicketType.java new file mode 100644 index 0000000..85607c2 --- /dev/null +++ b/src/main/java/org/lab/util/TicketType.java @@ -0,0 +1,14 @@ +package org.lab.util; + +import java.util.Random; + +public record TicketType(String titlePrefix, String descriptionTemplate) { + + public String generateDescription(String projectName) { + return STR.""" + \{descriptionTemplate} + Проект: \{projectName} + Приоритет: \{new Random().nextInt(5) + 1} + """; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..f62207a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,34 @@ + +spring: + application: + name: project-management-system + + # Профиль для тестовых данных + profiles: + active: dev + +# Настройки инициализации данных +project-management: + data: + initialization: + enabled: true + users-count: 10 + projects-count: 4 + tickets-per-project: 10 + bugs-per-project: 5 + +# Логирование +logging: + level: + com.projectmanagement: INFO + com.projectmanagement.config.DataInitializer: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" + +# Настройки для preview фич Java 21 +jdk: + incubator: + vector: true + preview: true +server: + port: 8080 \ No newline at end of file