From 33d0d659a60a96866d1b28458d6306608019ec13 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Fri, 22 May 2026 23:44:50 +0200 Subject: [PATCH 01/17] Backend for data export --- backend/project-manager/build.gradle | 6 + .../controller/report/ReportController.java | 55 +++++++ .../service/report/CsvGenerator.java | 7 + .../service/report/PdfGenerator.java | 5 + .../service/report/ProjectPdfGenerator.java | 39 +++++ .../service/report/ReportService.java | 50 +++++++ .../service/report/RiskCsvGenerator.java | 48 +++++++ .../report/ProjectPdfGeneratorTest.java | 59 ++++++++ .../service/report/ReportServiceTest.java | 136 ++++++++++++++++++ .../service/report/RiskCsvGeneratorTest.java | 71 +++++++++ 10 files changed, 476 insertions(+) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/CsvGenerator.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PdfGenerator.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java create mode 100644 backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ProjectPdfGeneratorTest.java create mode 100644 backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java create mode 100644 backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java diff --git a/backend/project-manager/build.gradle b/backend/project-manager/build.gradle index ad4ed91f..189b0ece 100644 --- a/backend/project-manager/build.gradle +++ b/backend/project-manager/build.gradle @@ -39,12 +39,18 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2' implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // JSON Web Token (JWT) implementation 'io.jsonwebtoken:jjwt-api:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' + // CSV & PDF + implementation 'com.opencsv:opencsv:5.9' + implementation 'com.openhtmltopdf:openhtmltopdf-core:1.0.10' + implementation 'com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.10' + // TOOLS compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java new file mode 100644 index 00000000..1eac6719 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java @@ -0,0 +1,55 @@ +package pl.edu.agh.project_manager.controller.report; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import pl.edu.agh.project_manager.security.UserPrincipal; +import pl.edu.agh.project_manager.service.report.ReportService; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/projects/{projectId}/reports") +@RequiredArgsConstructor +public class ReportController { + + private final ReportService reportService; + + @GetMapping("/risks/csv") + public ResponseEntity downloadRisksCsv( + @PathVariable UUID projectId, + @AuthenticationPrincipal UserPrincipal user + ) { + byte[] csvData = reportService.generateProjectRisksCsv(projectId, user); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("text/csv")); + headers.setContentDispositionFormData("attachment", "ryzyka_projektu.csv"); + + return ResponseEntity.ok() + .headers(headers) + .body(csvData); + } + + @GetMapping("/card/pdf") + public ResponseEntity downloadProjectCardPdf( + @PathVariable UUID projectId, + @AuthenticationPrincipal UserPrincipal user + ) { + byte[] pdfData = reportService.generateProjectCardPdf(projectId, user); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentDispositionFormData("attachment", "karta_projektu.pdf"); + + return ResponseEntity.ok() + .headers(headers) + .body(pdfData); + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/CsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/CsvGenerator.java new file mode 100644 index 00000000..0905e8e1 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/CsvGenerator.java @@ -0,0 +1,7 @@ +package pl.edu.agh.project_manager.service.report; + +import java.util.List; + +public interface CsvGenerator { + byte[] generate(List data); +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PdfGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PdfGenerator.java new file mode 100644 index 00000000..caacd5f5 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PdfGenerator.java @@ -0,0 +1,5 @@ +package pl.edu.agh.project_manager.service.report; + +public interface PdfGenerator { + byte[] generate(T data, String templateName); +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java new file mode 100644 index 00000000..2e51a25f --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java @@ -0,0 +1,39 @@ +package pl.edu.agh.project_manager.service.report; + +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; + +import java.io.ByteArrayOutputStream; + +@Component +@RequiredArgsConstructor +public class ProjectPdfGenerator implements PdfGenerator { + + private final TemplateEngine templateEngine; + + @Override + public byte[] generate(ProjectResponse data, String templateName) { + Context context = new Context(); + context.setVariable("project", data); + + String htmlContent = templateEngine.process(templateName, context); + + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + PdfRendererBuilder builder = new PdfRendererBuilder(); + builder.useFastMode(); + builder.withHtmlContent(htmlContent, null); + builder.toStream(os); + builder.run(); + + return os.toByteArray(); + } catch (Exception e) { + throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku PDF: " + e.getMessage()); + } + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java new file mode 100644 index 00000000..5b959632 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java @@ -0,0 +1,50 @@ +package pl.edu.agh.project_manager.service.report; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; +import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; +import pl.edu.agh.project_manager.security.UserPrincipal; +import pl.edu.agh.project_manager.security.access.ProjectAccess; +import pl.edu.agh.project_manager.service.project.ProjectRiskService; +import pl.edu.agh.project_manager.service.project.ProjectService; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ReportService { + + private final ProjectService projectService; + private final ProjectRiskService projectRiskService; + private final ProjectAccess projectAccess; + private final RiskCsvGenerator riskCsvGenerator; + private final ProjectPdfGenerator projectPdfGenerator; + + public byte[] generateProjectRisksCsv(UUID projectId, UserPrincipal user) { + checkAccess(projectId, user); + + List risks = projectRiskService.getProjectRisks(projectId); + return riskCsvGenerator.generate(risks); + } + + public byte[] generateProjectCardPdf(UUID projectId, UserPrincipal user) { + checkAccess(projectId, user); + + ProjectResponse project = projectService.getProject(projectId); + return projectPdfGenerator.generate(project, "project-card-template"); + } + + private void checkAccess(UUID projectId, UserPrincipal user) { + boolean hasAccess = user.userRole().name().equals("ADMINISTRATOR") || + user.userRole().name().equals("AUTHORITY") || + projectAccess.canAccessProject(projectId, user); + + if (!hasAccess) { + throw new ApplicationException(ApiErrorCode.ACCESS_DENIED, "Brak dostępu do projektu."); + } + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java new file mode 100644 index 00000000..ee45f62e --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java @@ -0,0 +1,48 @@ +package pl.edu.agh.project_manager.service.report; + +import com.opencsv.CSVWriter; +import org.springframework.stereotype.Component; +import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Component +public class RiskCsvGenerator implements CsvGenerator { + + private static final byte[] BOM = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + private static final String[] HEADERS = {"Nazwa", "Opis", "Prawdopodobieństwo"}; + + @Override + public byte[] generate(List data) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + baos.write(BOM); + + try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8); + CSVWriter csvWriter = new CSVWriter(writer)) { + + csvWriter.writeNext(HEADERS); + + for (RiskResponse risk : data) { + String[] row = { + risk.name(), + risk.description(), + String.valueOf(risk.probability()) + }; + csvWriter.writeNext(row); + } + + csvWriter.flush(); + } + + return baos.toByteArray(); + } catch (IOException e) { + throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku CSV: " + e.getMessage()); + } + } +} diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ProjectPdfGeneratorTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ProjectPdfGeneratorTest.java new file mode 100644 index 00000000..4b1ce01f --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ProjectPdfGeneratorTest.java @@ -0,0 +1,59 @@ +package pl.edu.agh.project_manager.service.report; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; +import pl.edu.agh.project_manager.controller.dto.user.UserResponse; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ProjectPdfGeneratorTest { + + @Mock + private TemplateEngine templateEngine; + + @InjectMocks + private ProjectPdfGenerator projectPdfGenerator; + + private ProjectResponse projectData; + + @BeforeEach + void setUp() { + UserResponse manager = new UserResponse(UUID.randomUUID(),"john.doe@example.com", "John", "Doe", null,null, null, List.of() + ); + projectData = new ProjectResponse(UUID.randomUUID(), "Test Project", "Description", LocalDate.now(), LocalDate.now().plusMonths(6), true, manager, null); + } + + @Test + @DisplayName("Should generate PDF from HTML content") + void generatePdf_Success() { + // Given + String templateName = "test-template"; + String htmlContent = "

Test Project

"; + when(templateEngine.process(eq(templateName), any(Context.class))).thenReturn(htmlContent); + + // When + byte[] pdfBytes = projectPdfGenerator.generate(projectData, templateName); + + // Then + assertThat(pdfBytes).isNotNull(); + assertThat(pdfBytes.length).isGreaterThan(0); + + assertThat(new String(pdfBytes, 0, 4)).isEqualTo("%PDF"); + } +} diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java new file mode 100644 index 00000000..b2152075 --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java @@ -0,0 +1,136 @@ +package pl.edu.agh.project_manager.service.report; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; +import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.domain.enums.UserRole; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; +import pl.edu.agh.project_manager.security.UserPrincipal; +import pl.edu.agh.project_manager.security.access.ProjectAccess; +import pl.edu.agh.project_manager.service.project.ProjectRiskService; +import pl.edu.agh.project_manager.service.project.ProjectService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ReportServiceTest { + + @Mock + private ProjectService projectService; + + @Mock + private ProjectRiskService projectRiskService; + + @Mock + private ProjectAccess projectAccess; + + @Mock + private RiskCsvGenerator riskCsvGenerator; + + @Mock + private ProjectPdfGenerator projectPdfGenerator; + + @InjectMocks + private ReportService reportService; + + private UUID projectId; + private UserPrincipal adminUser; + private UserPrincipal normalUser; + + @BeforeEach + void setUp() { + projectId = UUID.randomUUID(); + adminUser = new UserPrincipal(UUID.randomUUID(), "admin@test.com", "pass", "A", "A", List.of(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR")), UserRole.ADMINISTRATOR); + normalUser = new UserPrincipal(UUID.randomUUID(), "user@test.com", "pass", "U", "U", List.of(new SimpleGrantedAuthority("ROLE_COMMON")), UserRole.COMMON); + } + + @Test + @DisplayName("Should generate risks CSV when user has access") + void generateProjectRisksCsv_HasAccess() { + // Given + when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); + List risks = List.of(new RiskResponse(UUID.randomUUID(), "R1", "D1", 50)); + when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); + byte[] expectedCsv = "test-csv".getBytes(); + when(riskCsvGenerator.generate(risks)).thenReturn(expectedCsv); + + // When + byte[] result = reportService.generateProjectRisksCsv(projectId, normalUser); + + // Then + assertThat(result).isEqualTo(expectedCsv); + verify(projectRiskService).getProjectRisks(projectId); + verify(riskCsvGenerator).generate(risks); + } + + @Test + @DisplayName("Should generate risks CSV for admin without explicit project access") + void generateProjectRisksCsv_Admin() { + // Given + List risks = List.of(); + when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); + when(riskCsvGenerator.generate(risks)).thenReturn(new byte[0]); + + // When + reportService.generateProjectRisksCsv(projectId, adminUser); + + // Then + verify(projectAccess, never()).canAccessProject(any(), any()); + verify(projectRiskService).getProjectRisks(projectId); + } + + @Test + @DisplayName("Should throw exception when generating CSV and no access") + void generateProjectRisksCsv_NoAccess() { + // Given + when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(false); + + // When & Then + assertThatThrownBy(() -> reportService.generateProjectRisksCsv(projectId, normalUser)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining("Brak dostępu"); + } + + @Test + @DisplayName("Should generate project card PDF when user has access") + void generateProjectCardPdf_HasAccess() { + // Given + when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); + ProjectResponse project = new ProjectResponse(projectId, "Test", "Desc", null, null, true, null, null); + when(projectService.getProject(projectId)).thenReturn(project); + byte[] expectedPdf = "test-pdf".getBytes(); + when(projectPdfGenerator.generate(project, "project-card-template")).thenReturn(expectedPdf); + + // When + byte[] result = reportService.generateProjectCardPdf(projectId, normalUser); + + // Then + assertThat(result).isEqualTo(expectedPdf); + verify(projectService).getProject(projectId); + verify(projectPdfGenerator).generate(project, "project-card-template"); + } + + @Test + @DisplayName("Should throw exception when generating PDF and no access") + void generateProjectCardPdf_NoAccess() { + // Given + when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(false); + + // When & Then + assertThatThrownBy(() -> reportService.generateProjectCardPdf(projectId, normalUser)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining("Brak dostępu"); + } +} diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java new file mode 100644 index 00000000..d8676302 --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java @@ -0,0 +1,71 @@ +package pl.edu.agh.project_manager.service.report; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class RiskCsvGeneratorTest { + + private RiskCsvGenerator riskCsvGenerator; + + @BeforeEach + void setUp() { + riskCsvGenerator = new RiskCsvGenerator(); + } + + @Test + @DisplayName("Should generate CSV content correctly") + void generateCsv_Success() { + // Given + RiskResponse risk1 = new RiskResponse(UUID.randomUUID(), "Risk 1", "Description 1", 50); + RiskResponse risk2 = new RiskResponse(UUID.randomUUID(), "Risk 2", "Description 2", 80); + List risks = List.of(risk1, risk2); + + // When + byte[] resultBytes = riskCsvGenerator.generate(risks); + + // Then + assertThat(resultBytes).isNotNull(); + assertThat(resultBytes.length).isGreaterThan(0); + + // Verify BOM + assertThat(resultBytes[0]).isEqualTo((byte) 0xEF); + assertThat(resultBytes[1]).isEqualTo((byte) 0xBB); + assertThat(resultBytes[2]).isEqualTo((byte) 0xBF); + + // Verify content + String content = new String(resultBytes, 3, resultBytes.length - 3, StandardCharsets.UTF_8); + String[] lines = content.split("\n"); + + assertThat(lines).hasSize(3); // Headers + 2 rows + assertThat(lines[0]).contains("\"Nazwa\",\"Opis\",\"Prawdopodobieństwo\""); + assertThat(lines[1]).contains("\"Risk 1\",\"Description 1\",\"50\""); + assertThat(lines[2]).contains("\"Risk 2\",\"Description 2\",\"80\""); + } + + @Test + @DisplayName("Should generate empty CSV with headers when list is empty") + void generateCsv_EmptyList() { + // Given + List risks = List.of(); + + // When + byte[] resultBytes = riskCsvGenerator.generate(risks); + + // Then + assertThat(resultBytes).isNotNull(); + + String content = new String(resultBytes, 3, resultBytes.length - 3, StandardCharsets.UTF_8); + String[] lines = content.split("\n"); + + assertThat(lines).hasSize(1); + assertThat(lines[0]).contains("\"Nazwa\",\"Opis\",\"Prawdopodobieństwo\""); + } +} From 21490b2e7dcd2658a48c4f349105ba01d2d6cf08 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Sat, 23 May 2026 00:14:49 +0200 Subject: [PATCH 02/17] Backend complete --- .../controller/report/ReportController.java | 38 +++++++- .../service/report/PortfolioCsvGenerator.java | 50 +++++++++++ .../service/report/ReportService.java | 16 ++++ .../service/report/RiskPdfGenerator.java | 40 +++++++++ .../templates/project-card-template.html | 37 ++++++++ .../templates/project-risks-template.html | 87 +++++++++++++++++++ .../report/PortfolioCsvGeneratorTest.java | 53 +++++++++++ .../service/report/ReportServiceTest.java | 50 +++++++++-- 8 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java create mode 100644 backend/project-manager/src/main/resources/templates/project-card-template.html create mode 100644 backend/project-manager/src/main/resources/templates/project-risks-template.html create mode 100644 backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java index 1eac6719..f9758411 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java @@ -15,13 +15,13 @@ import java.util.UUID; @RestController -@RequestMapping("/api/projects/{projectId}/reports") +@RequestMapping("/api") @RequiredArgsConstructor public class ReportController { private final ReportService reportService; - @GetMapping("/risks/csv") + @GetMapping("/projects/{projectId}/reports/risks/csv") public ResponseEntity downloadRisksCsv( @PathVariable UUID projectId, @AuthenticationPrincipal UserPrincipal user @@ -37,7 +37,7 @@ public ResponseEntity downloadRisksCsv( .body(csvData); } - @GetMapping("/card/pdf") + @GetMapping("/projects/{projectId}/reports/card/pdf") public ResponseEntity downloadProjectCardPdf( @PathVariable UUID projectId, @AuthenticationPrincipal UserPrincipal user @@ -52,4 +52,36 @@ public ResponseEntity downloadProjectCardPdf( .headers(headers) .body(pdfData); } + + @GetMapping("/projects/{projectId}/reports/risks/pdf") + public ResponseEntity downloadRisksPdf( + @PathVariable UUID projectId, + @AuthenticationPrincipal UserPrincipal user + ) { + byte[] pdfData = reportService.generateProjectRisksPdf(projectId, user); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentDispositionFormData("attachment", "rejestr_ryzyk.pdf"); + + return ResponseEntity.ok() + .headers(headers) + .body(pdfData); + } + + @GetMapping("/groups/{groupId}/reports/projects/csv") + public ResponseEntity downloadPortfolioCsv( + @PathVariable UUID groupId, + @AuthenticationPrincipal UserPrincipal user + ) { + byte[] csvData = reportService.generatePortfolioCsv(groupId, user); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("text/csv")); + headers.setContentDispositionFormData("attachment", "lista_projektow_grupy.csv"); + + return ResponseEntity.ok() + .headers(headers) + .body(csvData); + } } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java new file mode 100644 index 00000000..480569a1 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java @@ -0,0 +1,50 @@ +package pl.edu.agh.project_manager.service.report; + +import com.opencsv.CSVWriter; +import org.springframework.stereotype.Component; +import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Component +public class PortfolioCsvGenerator implements CsvGenerator { + + private static final byte[] BOM = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + private static final String[] HEADERS = {"Tytuł Projektu", "Opis", "Data rozpoczęcia", "Data zakończenia", "Status"}; + + @Override + public byte[] generate(List data) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + baos.write(BOM); + + try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8); + CSVWriter csvWriter = new CSVWriter(writer)) { + + csvWriter.writeNext(HEADERS); + + for (ProjectResponse project : data) { + String[] row = { + project.title(), + project.description(), + project.startDate().toString(), + project.endDate().toString(), + project.isActive() ? "Aktywny" : "Zakończony" + }; + csvWriter.writeNext(row); + } + + csvWriter.flush(); + } + + return baos.toByteArray(); + } catch (IOException e) { + throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku CSV portfela: " + e.getMessage()); + } + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java index 5b959632..5f3dcada 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java @@ -8,6 +8,7 @@ import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.security.UserPrincipal; import pl.edu.agh.project_manager.security.access.ProjectAccess; +import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand; import pl.edu.agh.project_manager.service.project.ProjectRiskService; import pl.edu.agh.project_manager.service.project.ProjectService; @@ -23,6 +24,8 @@ public class ReportService { private final ProjectAccess projectAccess; private final RiskCsvGenerator riskCsvGenerator; private final ProjectPdfGenerator projectPdfGenerator; + private final RiskPdfGenerator riskPdfGenerator; + private final PortfolioCsvGenerator portfolioCsvGenerator; public byte[] generateProjectRisksCsv(UUID projectId, UserPrincipal user) { checkAccess(projectId, user); @@ -38,6 +41,19 @@ public byte[] generateProjectCardPdf(UUID projectId, UserPrincipal user) { return projectPdfGenerator.generate(project, "project-card-template"); } + public byte[] generateProjectRisksPdf(UUID projectId, UserPrincipal user) { + checkAccess(projectId, user); + + List risks = projectRiskService.getProjectRisks(projectId); + return riskPdfGenerator.generate(risks, "project-risks-template"); + } + + public byte[] generatePortfolioCsv(UUID groupId, UserPrincipal user) { + SearchProjectCommand command = new SearchProjectCommand(user, null, groupId, null, false); + List projects = projectService.searchProjects(command); + return portfolioCsvGenerator.generate(projects); + } + private void checkAccess(UUID projectId, UserPrincipal user) { boolean hasAccess = user.userRole().name().equals("ADMINISTRATOR") || user.userRole().name().equals("AUTHORITY") || diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java new file mode 100644 index 00000000..bd3915b9 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java @@ -0,0 +1,40 @@ +package pl.edu.agh.project_manager.service.report; + +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; + +import java.io.ByteArrayOutputStream; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class RiskPdfGenerator implements PdfGenerator> { + + private final TemplateEngine templateEngine; + + @Override + public byte[] generate(List data, String templateName) { + Context context = new Context(); + context.setVariable("risks", data); + + String htmlContent = templateEngine.process(templateName, context); + + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + PdfRendererBuilder builder = new PdfRendererBuilder(); + builder.useFastMode(); + builder.withHtmlContent(htmlContent, null); + builder.toStream(os); + builder.run(); + + return os.toByteArray(); + } catch (Exception e) { + throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku PDF z ryzykami: " + e.getMessage()); + } + } +} diff --git a/backend/project-manager/src/main/resources/templates/project-card-template.html b/backend/project-manager/src/main/resources/templates/project-card-template.html new file mode 100644 index 00000000..458cf006 --- /dev/null +++ b/backend/project-manager/src/main/resources/templates/project-card-template.html @@ -0,0 +1,37 @@ + + + + + Karta Projektu + + + + +

Nazwa projektu (placeholder)

+ +
+

Opis: ...

+

+ Status: + ... +

+

Data rozpoczęcia: ...

+

Data zakończenia: ...

+
+ +
+

Kierownik Projektu

+

Imię i nazwisko: ...

+

Email: ...

+
+ + + \ No newline at end of file diff --git a/backend/project-manager/src/main/resources/templates/project-risks-template.html b/backend/project-manager/src/main/resources/templates/project-risks-template.html new file mode 100644 index 00000000..e754d584 --- /dev/null +++ b/backend/project-manager/src/main/resources/templates/project-risks-template.html @@ -0,0 +1,87 @@ + + + + + Rejestr Ryzyk Projektu + + + +
+

Rejestr Ryzyk Projektu

+ + + + + + + + + + + + + + + + + + + +
NazwaOpisPrawdopodobieństwo (%)
Ryzyko techniczneProblem z integracją zewnętrznego API.75
Brak zdefiniowanych ryzyk dla tego projektu.
+ + +
+ + diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java new file mode 100644 index 00000000..72c54b71 --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java @@ -0,0 +1,53 @@ +package pl.edu.agh.project_manager.service.report; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class PortfolioCsvGeneratorTest { + + private PortfolioCsvGenerator portfolioCsvGenerator; + + @BeforeEach + void setUp() { + portfolioCsvGenerator = new PortfolioCsvGenerator(); + } + + @Test + @DisplayName("Should generate CSV for portfolio correctly") + void generatePortfolioCsv_Success() { + // Given + ProjectResponse project1 = new ProjectResponse(UUID.randomUUID(), "Projekt A", "Opis A", LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31), true, null, null); + ProjectResponse project2 = new ProjectResponse(UUID.randomUUID(), "Projekt B", "Opis B", LocalDate.of(2023, 5, 10), LocalDate.of(2025, 5, 9), false, null, null); + List projects = List.of(project1, project2); + + // When + byte[] resultBytes = portfolioCsvGenerator.generate(projects); + + // Then + assertThat(resultBytes).isNotNull(); + assertThat(resultBytes.length).isGreaterThan(0); + + // Verify BOM + assertThat(resultBytes[0]).isEqualTo((byte) 0xEF); + assertThat(resultBytes[1]).isEqualTo((byte) 0xBB); + assertThat(resultBytes[2]).isEqualTo((byte) 0xBF); + + // Verify content + String content = new String(resultBytes, 3, resultBytes.length - 3, StandardCharsets.UTF_8); + String[] lines = content.split("\n"); + + assertThat(lines).hasSize(3); // Headers + 2 rows + assertThat(lines[0]).contains("\"Tytuł Projektu\",\"Opis\",\"Data rozpoczęcia\",\"Data zakończenia\",\"Status\""); + assertThat(lines[1]).contains("\"Projekt A\",\"Opis A\",\"2024-01-01\",\"2024-12-31\",\"Aktywny\""); + assertThat(lines[2]).contains("\"Projekt B\",\"Opis B\",\"2023-05-10\",\"2025-05-09\",\"Zakończony\""); + } +} diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java index b2152075..006f375e 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java @@ -14,6 +14,7 @@ import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.security.UserPrincipal; import pl.edu.agh.project_manager.security.access.ProjectAccess; +import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand; import pl.edu.agh.project_manager.service.project.ProjectRiskService; import pl.edu.agh.project_manager.service.project.ProjectService; @@ -22,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -42,6 +44,12 @@ class ReportServiceTest { @Mock private ProjectPdfGenerator projectPdfGenerator; + @Mock + private RiskPdfGenerator riskPdfGenerator; + + @Mock + private PortfolioCsvGenerator portfolioCsvGenerator; + @InjectMocks private ReportService reportService; @@ -123,14 +131,40 @@ void generateProjectCardPdf_HasAccess() { } @Test - @DisplayName("Should throw exception when generating PDF and no access") - void generateProjectCardPdf_NoAccess() { + @DisplayName("Should generate risks PDF when user has access") + void generateProjectRisksPdf_HasAccess() { // Given - when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(false); + when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); + List risks = List.of(new RiskResponse(UUID.randomUUID(), "R1", "D1", 50)); + when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); + byte[] expectedPdf = "test-pdf".getBytes(); + when(riskPdfGenerator.generate(risks, "project-risks-template")).thenReturn(expectedPdf); - // When & Then - assertThatThrownBy(() -> reportService.generateProjectCardPdf(projectId, normalUser)) - .isInstanceOf(ApplicationException.class) - .hasMessageContaining("Brak dostępu"); + // When + byte[] result = reportService.generateProjectRisksPdf(projectId, normalUser); + + // Then + assertThat(result).isEqualTo(expectedPdf); + verify(projectRiskService).getProjectRisks(projectId); + verify(riskPdfGenerator).generate(risks, "project-risks-template"); + } + + @Test + @DisplayName("Should generate portfolio CSV") + void generatePortfolioCsv_Success() { + // Given + UUID groupId = UUID.randomUUID(); + List projects = List.of(new ProjectResponse(projectId, "Test", "Desc", null, null, true, null, null)); + when(projectService.searchProjects(any(SearchProjectCommand.class))).thenReturn(projects); + byte[] expectedCsv = "test-csv".getBytes(); + when(portfolioCsvGenerator.generate(projects)).thenReturn(expectedCsv); + + // When + byte[] result = reportService.generatePortfolioCsv(groupId, normalUser); + + // Then + assertThat(result).isEqualTo(expectedCsv); + verify(projectService).searchProjects(any(SearchProjectCommand.class)); + verify(portfolioCsvGenerator).generate(projects); } -} +} \ No newline at end of file From 5431f09e1736a2cba5cead3073355e2fe80e525e Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 01:45:38 +0200 Subject: [PATCH 03/17] Frontend --- .../components/GroupSection/GroupSection.tsx | 30 ++++++-- .../project/components/ProjectHeader.tsx | 75 +++++++++++++++---- frontend/src/features/report/report.hooks.ts | 51 +++++++++++++ 3 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 frontend/src/features/report/report.hooks.ts diff --git a/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx b/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx index 2230e3cf..953fecef 100644 --- a/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx +++ b/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx @@ -1,10 +1,12 @@ import { useState } from 'react'; -import { ChevronDown, FolderOpen } from 'lucide-react'; +import { ChevronDown, FolderOpen, FileSpreadsheet, Loader2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import type { ProjectGroupResponse } from '@/features/project_group/project_group.types'; import { ProjectGrid } from "@/features/dashboard/components/ProjectGrid/ProjectGrid.tsx"; +import { Button } from '@/components/ui/button'; +import { useReportDownload } from '@/features/report/report.hooks'; interface GroupSectionProps { title: string; @@ -22,11 +24,17 @@ export const GroupSection = ({ const [openGroups, setOpenGroups] = useState>( () => groups.reduce((acc, g) => ({ ...acc, [g.id]: true }), {}) ); + const { downloadReport, isDownloading } = useReportDownload(); const toggleGroup = (id: string) => { setOpenGroups(prev => ({ ...prev, [id]: !prev[id] })); }; + const handleDownload = (groupId: string, groupName: string) => { + const filename = `zestawienie_${groupName.toLowerCase().replace(/\s+/g, '_')}.csv`; + downloadReport(`/groups/${groupId}/reports/projects/csv`, filename); + }; + if (groups.length === 0) return null; return ( @@ -45,16 +53,26 @@ export const GroupSection = ({ onOpenChange={() => toggleGroup(group.id)} className="border rounded-lg bg-white shadow-sm overflow-hidden" > - -
+
+ {group.name} {group.projects.length} {group.projects.length === 1 ? 'projekt' : (group.projects.length > 1 && group.projects.length < 5) ? 'projekty' : 'projektów'} -
- - + + + +
diff --git a/frontend/src/features/project/components/ProjectHeader.tsx b/frontend/src/features/project/components/ProjectHeader.tsx index 9e521579..898febfa 100644 --- a/frontend/src/features/project/components/ProjectHeader.tsx +++ b/frontend/src/features/project/components/ProjectHeader.tsx @@ -1,8 +1,16 @@ import type { ProjectDetailsResponse } from "@/features/project"; -import { CalendarIcon, Folder, Briefcase } from "lucide-react"; +import { CalendarIcon, Folder, Briefcase, Download, FileText, FileSpreadsheet, Loader2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import {ProjectGroupType} from "@/features/project_group/project_group.types.ts"; +import { ProjectGroupType } from "@/features/project_group/project_group.types.ts"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useReportDownload } from "@/features/report/report.hooks"; interface ProjectHeaderProps { details: ProjectDetailsResponse; @@ -11,6 +19,20 @@ interface ProjectHeaderProps { export const ProjectHeader = ({ details }: ProjectHeaderProps) => { const groupName = details.group?.name; const isWallet = details.group?.groupType === ProjectGroupType.WALLET; + + const { downloadReport, isDownloading } = useReportDownload(); + + const handleDownloadCardPdf = () => { + downloadReport(`/projects/${details.id}/reports/card/pdf`, 'karta_projektu.pdf'); + }; + + const handleDownloadRisksCsv = () => { + downloadReport(`/projects/${details.id}/reports/risks/csv`, 'ryzyka.csv'); + }; + + const handleDownloadRisksPdf = () => { + downloadReport(`/projects/${details.id}/reports/risks/pdf`, 'ryzyka.pdf'); + }; return (
@@ -42,18 +64,43 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => {
-
- - - {details.manager.name?.[0] || ''}{details.manager.surname?.[0] || ''} - - - -
- - {details.manager.name} {details.manager.surname} - - Kierownik Projektu +
+ + + + + + + + Karta Projektu (PDF) + + + + Rejestr Ryzyk (CSV) + + + + Rejestr Ryzyk (PDF) + + + + +
+ + + {details.manager.name?.[0] || ''}{details.manager.surname?.[0] || ''} + + + +
+ + {details.manager.name} {details.manager.surname} + + Kierownik Projektu +
diff --git a/frontend/src/features/report/report.hooks.ts b/frontend/src/features/report/report.hooks.ts new file mode 100644 index 00000000..6b7a4456 --- /dev/null +++ b/frontend/src/features/report/report.hooks.ts @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import { toast } from 'sonner'; +import api from '@/api/client'; + +export const useReportDownload = () => { + const [isDownloading, setIsDownloading] = useState(false); + + const downloadReport = async (url: string, filename: string) => { + setIsDownloading(true); + try { + const response = await api.get(url, { + responseType: 'blob', + }); + + const disposition = response.headers['content-disposition'] as string | undefined; + let finalFilename = filename; + + if (disposition && typeof disposition === 'string' && disposition.indexOf('attachment') !== -1) { + const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + const matches = filenameRegex.exec(disposition); + if (matches != null && matches[1]) { + finalFilename = matches[1].replace(/['"]/g, ''); + } + } + + const contentType = (response.headers['content-type'] as string) || 'application/octet-stream'; + const blob = new Blob([response.data], { type: contentType }); + + const downloadUrl = window.URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = downloadUrl; + link.setAttribute('download', finalFilename); + + document.body.appendChild(link); + link.click(); + + link.parentNode?.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + + toast.success('Pobieranie pliku rozpoczęto.'); + } catch (error) { + console.error('Błąd podczas pobierania raportu:', error); + toast.error('Wystąpił błąd podczas generowania lub pobierania raportu.'); + } finally { + setIsDownloading(false); + } + }; + + return { downloadReport, isDownloading }; +}; \ No newline at end of file From 879abe386ab58170f142cb8fc894181afd601e63 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 12:57:53 +0200 Subject: [PATCH 04/17] Desperate fix attempt --- backend/project-manager/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/project-manager/build.gradle b/backend/project-manager/build.gradle index 189b0ece..2b47b66d 100644 --- a/backend/project-manager/build.gradle +++ b/backend/project-manager/build.gradle @@ -75,3 +75,8 @@ tasks.named('test') { jar { enabled = false } + + +repositories { + mavenCentral() +} \ No newline at end of file From 56182a010f3dbcbbc0fdb45e4165dae13bc82576 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 13:33:42 +0200 Subject: [PATCH 05/17] Fix attempt --- backend/project-manager/gradle.properties | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/project-manager/gradle.properties diff --git a/backend/project-manager/gradle.properties b/backend/project-manager/gradle.properties new file mode 100644 index 00000000..854cd5b7 --- /dev/null +++ b/backend/project-manager/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.java.installations.auto-download=true +org.gradle.java.installations.auto-detect=true \ No newline at end of file From 11143f7d0f1219ed46eec7e2365433e69b32fde3 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 14:16:02 +0200 Subject: [PATCH 06/17] Fix attempt 3 --- backend/project-manager/build.gradle | 5 ----- backend/project-manager/gradle.properties | 2 -- 2 files changed, 7 deletions(-) delete mode 100644 backend/project-manager/gradle.properties diff --git a/backend/project-manager/build.gradle b/backend/project-manager/build.gradle index 2b47b66d..37a84301 100644 --- a/backend/project-manager/build.gradle +++ b/backend/project-manager/build.gradle @@ -74,9 +74,4 @@ tasks.named('test') { jar { enabled = false -} - - -repositories { - mavenCentral() } \ No newline at end of file diff --git a/backend/project-manager/gradle.properties b/backend/project-manager/gradle.properties deleted file mode 100644 index 854cd5b7..00000000 --- a/backend/project-manager/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.java.installations.auto-download=true -org.gradle.java.installations.auto-detect=true \ No newline at end of file From 420c3dfe1d667f69bec8f286d9a34e5d776c0981 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 15:16:52 +0200 Subject: [PATCH 07/17] Trigger tests --- .../service/report/PortfolioCsvGeneratorTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java index 72c54b71..bc3e872d 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java @@ -50,4 +50,5 @@ void generatePortfolioCsv_Success() { assertThat(lines[1]).contains("\"Projekt A\",\"Opis A\",\"2024-01-01\",\"2024-12-31\",\"Aktywny\""); assertThat(lines[2]).contains("\"Projekt B\",\"Opis B\",\"2023-05-10\",\"2025-05-09\",\"Zakończony\""); } + } From 7107e44d3c9b7cf0e89a970914ba1551d57f92aa Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 16:23:35 +0200 Subject: [PATCH 08/17] Ultimate fix --- backend/project-manager/gradle.properties | 1 + backend/project-manager/settings.gradle | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 backend/project-manager/gradle.properties diff --git a/backend/project-manager/gradle.properties b/backend/project-manager/gradle.properties new file mode 100644 index 00000000..2f97dafb --- /dev/null +++ b/backend/project-manager/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.installations.auto-download=true \ No newline at end of file diff --git a/backend/project-manager/settings.gradle b/backend/project-manager/settings.gradle index 0a9425cc..f4c17051 100644 --- a/backend/project-manager/settings.gradle +++ b/backend/project-manager/settings.gradle @@ -1 +1,5 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + rootProject.name = 'project-manager' From 1f7670aa905adf709d298cf0edfcf617b3c75829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Rogalski?= <80521810+rrogalski@users.noreply.github.com> Date: Mon, 25 May 2026 18:34:33 +0200 Subject: [PATCH 09/17] Update CI workflow to clean before building --- .github/workflows/backend-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index b0e0aaa4..e429a173 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -33,4 +33,4 @@ jobs: run: chmod +x gradlew - name: Build and Test with Gradle - run: ./gradlew build + run: ./gradlew clean build From a6bbc82a3c129a748f5280dc930cd76611c956de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Rogalski?= <80521810+rrogalski@users.noreply.github.com> Date: Mon, 25 May 2026 18:41:14 +0200 Subject: [PATCH 10/17] Add debug step for Risk file path in CI workflow Add a step to debug file path and casing for Risk files. --- .github/workflows/backend-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index e429a173..9abefcbf 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -32,5 +32,8 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Debug file path and casing + run: find src/main/java -name "*Risk*" + - name: Build and Test with Gradle run: ./gradlew clean build From 5be158a22eb35dc71a80e1f8c80750a01326d216 Mon Sep 17 00:00:00 2001 From: radek Date: Mon, 25 May 2026 18:49:55 +0200 Subject: [PATCH 11/17] fixy --- .github/workflows/backend-ci.yml | 5 +---- .../controller/dto/project/ProjectCreationRequest.java | 2 +- .../projectrisk => project_risk}/ProjectRiskRequest.java | 2 +- .../projectrisk => project_risk}/ProjectRiskResponse.java | 2 +- .../controller/project/ProjectRiskController.java | 4 ++-- .../service/project/ProjectRiskService.java | 2 +- .../agh/project_manager/service/report/ReportService.java | 6 +++--- .../project_manager/service/report/RiskCsvGenerator.java | 8 ++++---- .../project_manager/service/report/RiskPdfGenerator.java | 6 +++--- .../service/project/ProjectRiskServiceTest.java | 2 +- 10 files changed, 18 insertions(+), 21 deletions(-) rename backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/{project/projectrisk => project_risk}/ProjectRiskRequest.java (93%) rename backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/{project/projectrisk => project_risk}/ProjectRiskResponse.java (89%) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 9abefcbf..c2a535bc 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -32,8 +32,5 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Debug file path and casing - run: find src/main/java -name "*Risk*" - - name: Build and Test with Gradle - run: ./gradlew clean build + run: ./gradlew gbuild diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectCreationRequest.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectCreationRequest.java index aab2b214..660c7e89 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectCreationRequest.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectCreationRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneRequest; -import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskRequest; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskRequest; import pl.edu.agh.project_manager.service.command.project.ProjectCreationCommand; import java.time.LocalDate; diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/projectrisk/ProjectRiskRequest.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project_risk/ProjectRiskRequest.java similarity index 93% rename from backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/projectrisk/ProjectRiskRequest.java rename to backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project_risk/ProjectRiskRequest.java index 0ab48d9d..89d591d3 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/projectrisk/ProjectRiskRequest.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project_risk/ProjectRiskRequest.java @@ -1,4 +1,4 @@ -package pl.edu.agh.project_manager.controller.dto.project.projectrisk; +package pl.edu.agh.project_manager.controller.dto.project_risk; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/projectrisk/ProjectRiskResponse.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project_risk/ProjectRiskResponse.java similarity index 89% rename from backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/projectrisk/ProjectRiskResponse.java rename to backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project_risk/ProjectRiskResponse.java index 2a32fcb0..a823c7a6 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/projectrisk/ProjectRiskResponse.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project_risk/ProjectRiskResponse.java @@ -1,4 +1,4 @@ -package pl.edu.agh.project_manager.controller.dto.project.projectrisk; +package pl.edu.agh.project_manager.controller.dto.project_risk; import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk; diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectRiskController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectRiskController.java index 80403751..dfa2d65b 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectRiskController.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectRiskController.java @@ -6,8 +6,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskRequest; -import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskRequest; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.service.project.ProjectRiskService; import java.util.List; diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectRiskService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectRiskService.java index a431a6d1..9c9d6450 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectRiskService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectRiskService.java @@ -3,7 +3,7 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.entity.project.Project; import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java index 5f3dcada..b49e7734 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; -import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.security.UserPrincipal; @@ -30,7 +30,7 @@ public class ReportService { public byte[] generateProjectRisksCsv(UUID projectId, UserPrincipal user) { checkAccess(projectId, user); - List risks = projectRiskService.getProjectRisks(projectId); + List risks = projectRiskService.getProjectRisks(projectId); return riskCsvGenerator.generate(risks); } @@ -44,7 +44,7 @@ public byte[] generateProjectCardPdf(UUID projectId, UserPrincipal user) { public byte[] generateProjectRisksPdf(UUID projectId, UserPrincipal user) { checkAccess(projectId, user); - List risks = projectRiskService.getProjectRisks(projectId); + List risks = projectRiskService.getProjectRisks(projectId); return riskPdfGenerator.generate(risks, "project-risks-template"); } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java index ee45f62e..4d0b9851 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java @@ -2,7 +2,7 @@ import com.opencsv.CSVWriter; import org.springframework.stereotype.Component; -import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; @@ -13,13 +13,13 @@ import java.util.List; @Component -public class RiskCsvGenerator implements CsvGenerator { +public class RiskCsvGenerator implements CsvGenerator { private static final byte[] BOM = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; private static final String[] HEADERS = {"Nazwa", "Opis", "Prawdopodobieństwo"}; @Override - public byte[] generate(List data) { + public byte[] generate(List data) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { baos.write(BOM); @@ -28,7 +28,7 @@ public byte[] generate(List data) { csvWriter.writeNext(HEADERS); - for (RiskResponse risk : data) { + for (ProjectRiskResponse risk : data) { String[] row = { risk.name(), risk.description(), diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java index bd3915b9..276e5f9d 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; @@ -14,12 +14,12 @@ @Component @RequiredArgsConstructor -public class RiskPdfGenerator implements PdfGenerator> { +public class RiskPdfGenerator implements PdfGenerator> { private final TemplateEngine templateEngine; @Override - public byte[] generate(List data, String templateName) { + public byte[] generate(List data, String templateName) { Context context = new Context(); context.setVariable("risks", data); diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/project/ProjectRiskServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/project/ProjectRiskServiceTest.java index 78765b28..d3771932 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/project/ProjectRiskServiceTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/project/ProjectRiskServiceTest.java @@ -6,7 +6,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import pl.edu.agh.project_manager.controller.dto.project.projectrisk.ProjectRiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.entity.project.Project; import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk; import pl.edu.agh.project_manager.repository.project.RiskRepository; From a4a76ee35d380facb6e52d2c5d1c91e71347bbbe Mon Sep 17 00:00:00 2001 From: radek Date: Mon, 25 May 2026 18:51:04 +0200 Subject: [PATCH 12/17] literowka --- .github/workflows/backend-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index c2a535bc..b0e0aaa4 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -33,4 +33,4 @@ jobs: run: chmod +x gradlew - name: Build and Test with Gradle - run: ./gradlew gbuild + run: ./gradlew build From 4aa292e8552a294cb769beadef09a36a4b761e91 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 20:09:26 +0200 Subject: [PATCH 13/17] Tests fix --- .../service/report/ReportServiceTest.java | 8 ++++---- .../service/report/RiskCsvGeneratorTest.java | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java index 006f375e..d393c258 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java @@ -9,7 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.authority.SimpleGrantedAuthority; import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; -import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.enums.UserRole; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.security.UserPrincipal; @@ -69,7 +69,7 @@ void setUp() { void generateProjectRisksCsv_HasAccess() { // Given when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); - List risks = List.of(new RiskResponse(UUID.randomUUID(), "R1", "D1", 50)); + List risks = List.of(new ProjectRiskResponse(UUID.randomUUID(), "R1", "D1", 50,21,2)); when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); byte[] expectedCsv = "test-csv".getBytes(); when(riskCsvGenerator.generate(risks)).thenReturn(expectedCsv); @@ -87,7 +87,7 @@ void generateProjectRisksCsv_HasAccess() { @DisplayName("Should generate risks CSV for admin without explicit project access") void generateProjectRisksCsv_Admin() { // Given - List risks = List.of(); + List risks = List.of(); when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); when(riskCsvGenerator.generate(risks)).thenReturn(new byte[0]); @@ -135,7 +135,7 @@ void generateProjectCardPdf_HasAccess() { void generateProjectRisksPdf_HasAccess() { // Given when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); - List risks = List.of(new RiskResponse(UUID.randomUUID(), "R1", "D1", 50)); + List risks = List.of(new ProjectRiskResponse(UUID.randomUUID(), "R1", "D1", 50,27,2)); when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); byte[] expectedPdf = "test-pdf".getBytes(); when(riskPdfGenerator.generate(risks, "project-risks-template")).thenReturn(expectedPdf); diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java index d8676302..cbc36ef4 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/RiskCsvGeneratorTest.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import pl.edu.agh.project_manager.controller.dto.project.RiskResponse; +import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import java.nio.charset.StandardCharsets; import java.util.List; @@ -24,9 +24,9 @@ void setUp() { @DisplayName("Should generate CSV content correctly") void generateCsv_Success() { // Given - RiskResponse risk1 = new RiskResponse(UUID.randomUUID(), "Risk 1", "Description 1", 50); - RiskResponse risk2 = new RiskResponse(UUID.randomUUID(), "Risk 2", "Description 2", 80); - List risks = List.of(risk1, risk2); + ProjectRiskResponse risk1 = new ProjectRiskResponse(UUID.randomUUID(), "Risk 1", "Description 1", 50,21,2); + ProjectRiskResponse risk2 = new ProjectRiskResponse(UUID.randomUUID(), "Risk 2", "Description 2", 80,21,3); + List risks = List.of(risk1, risk2); // When byte[] resultBytes = riskCsvGenerator.generate(risks); @@ -54,7 +54,7 @@ void generateCsv_Success() { @DisplayName("Should generate empty CSV with headers when list is empty") void generateCsv_EmptyList() { // Given - List risks = List.of(); + List risks = List.of(); // When byte[] resultBytes = riskCsvGenerator.generate(risks); From c6cc8bb4ab0c887ed824f151aa1332601aef7446 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 20:20:24 +0200 Subject: [PATCH 14/17] Clean error codes --- .../agh/project_manager/domain/exception/ApiErrorCode.java | 2 ++ .../project_manager/service/report/PortfolioCsvGenerator.java | 2 +- .../project_manager/service/report/ProjectPdfGenerator.java | 2 +- .../agh/project_manager/service/report/RiskCsvGenerator.java | 4 ++-- .../agh/project_manager/service/report/RiskPdfGenerator.java | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java index 37d419f9..e77fbe7f 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java @@ -47,6 +47,8 @@ public enum ApiErrorCode { INVALID_CURRENT_PASSWORD("USR_003", HttpStatus.UNAUTHORIZED, "Current password is incorrect"), PASSWORD_SAME_AS_CURRENT("USR_004", HttpStatus.BAD_REQUEST, "New password must be different from current password"), + REPORT_GENERATION_ERROR("REP_001", HttpStatus.INTERNAL_SERVER_ERROR, "Wystąpił błąd podczas generowania raportu"), + ACCESS_DENIED("GEN_002", HttpStatus.FORBIDDEN, "Access denied"), VALIDATION_FAILED("GEN_998", HttpStatus.BAD_REQUEST, "Validation failed"), INTERNAL_SERVER_ERROR("GEN_999", HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected internal server error"); diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java index 480569a1..86e91777 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java @@ -44,7 +44,7 @@ public byte[] generate(List data) { return baos.toByteArray(); } catch (IOException e) { - throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku CSV portfela: " + e.getMessage()); + throw new ApplicationException(ApiErrorCode.REPORT_GENERATION_ERROR, "Błąd podczas generowania pliku CSV portfela: " + e.getMessage()); } } } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java index 2e51a25f..f194b64f 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ProjectPdfGenerator.java @@ -33,7 +33,7 @@ public byte[] generate(ProjectResponse data, String templateName) { return os.toByteArray(); } catch (Exception e) { - throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku PDF: " + e.getMessage()); + throw new ApplicationException(ApiErrorCode.REPORT_GENERATION_ERROR, "Błąd podczas generowania pliku PDF: " + e.getMessage()); } } } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java index 4d0b9851..445a8f8c 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskCsvGenerator.java @@ -42,7 +42,7 @@ public byte[] generate(List data) { return baos.toByteArray(); } catch (IOException e) { - throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku CSV: " + e.getMessage()); + throw new ApplicationException(ApiErrorCode.REPORT_GENERATION_ERROR, "Błąd podczas generowania pliku CSV: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java index 276e5f9d..6c5e4a6a 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/RiskPdfGenerator.java @@ -34,7 +34,7 @@ public byte[] generate(List data, String templateName) { return os.toByteArray(); } catch (Exception e) { - throw new ApplicationException(ApiErrorCode.INTERNAL_SERVER_ERROR, "Błąd podczas generowania pliku PDF z ryzykami: " + e.getMessage()); + throw new ApplicationException(ApiErrorCode.REPORT_GENERATION_ERROR, "Błąd podczas generowania pliku PDF z ryzykami: " + e.getMessage()); } } } From 65c3b539806e1bdc6fc3a0d5b9bb5ff350081a0e Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Mon, 25 May 2026 20:47:49 +0200 Subject: [PATCH 15/17] Code Rewiev changes --- .../controller/report/ReportController.java | 78 +++++-------------- .../controller/report/ReportType.java | 24 ++++++ .../project/components/ProjectHeader.tsx | 41 +++++----- frontend/src/features/report/report.hooks.ts | 6 +- .../src/features/report/report.service.ts | 10 +++ 5 files changed, 76 insertions(+), 83 deletions(-) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportType.java create mode 100644 frontend/src/features/report/report.service.ts diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java index f9758411..82098852 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java @@ -1,87 +1,47 @@ package pl.edu.agh.project_manager.controller.report; import lombok.RequiredArgsConstructor; +import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import pl.edu.agh.project_manager.security.UserPrincipal; import pl.edu.agh.project_manager.service.report.ReportService; import java.util.UUID; @RestController -@RequestMapping("/api") +@RequestMapping("/api/reports") @RequiredArgsConstructor public class ReportController { private final ReportService reportService; - @GetMapping("/projects/{projectId}/reports/risks/csv") + @GetMapping("/{resourceId}") public ResponseEntity downloadRisksCsv( - @PathVariable UUID projectId, + @PathVariable UUID resourceId, + @RequestParam ReportType type, @AuthenticationPrincipal UserPrincipal user ) { - byte[] csvData = reportService.generateProjectRisksCsv(projectId, user); + byte[] reportData = switch (type) { + case PROJECT_CARD_PDF -> reportService.generateProjectCardPdf(resourceId, user); + case PROJECT_RISKS_PDF -> reportService.generateProjectRisksPdf(resourceId, user); + case PROJECT_RISKS_CSV -> reportService.generateProjectRisksCsv(resourceId, user); + case GROUP_PROJECTS_CSV -> reportService.generatePortfolioCsv(resourceId, user); + }; HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType("text/csv")); - headers.setContentDispositionFormData("attachment", "ryzyka_projektu.csv"); + headers.setContentType(MediaType.parseMediaType(type.getContentType())); - return ResponseEntity.ok() - .headers(headers) - .body(csvData); - } - - @GetMapping("/projects/{projectId}/reports/card/pdf") - public ResponseEntity downloadProjectCardPdf( - @PathVariable UUID projectId, - @AuthenticationPrincipal UserPrincipal user - ) { - byte[] pdfData = reportService.generateProjectCardPdf(projectId, user); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_PDF); - headers.setContentDispositionFormData("attachment", "karta_projektu.pdf"); - - return ResponseEntity.ok() - .headers(headers) - .body(pdfData); - } - - @GetMapping("/projects/{projectId}/reports/risks/pdf") - public ResponseEntity downloadRisksPdf( - @PathVariable UUID projectId, - @AuthenticationPrincipal UserPrincipal user - ) { - byte[] pdfData = reportService.generateProjectRisksPdf(projectId, user); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_PDF); - headers.setContentDispositionFormData("attachment", "rejestr_ryzyk.pdf"); - - return ResponseEntity.ok() - .headers(headers) - .body(pdfData); - } - - @GetMapping("/groups/{groupId}/reports/projects/csv") - public ResponseEntity downloadPortfolioCsv( - @PathVariable UUID groupId, - @AuthenticationPrincipal UserPrincipal user - ) { - byte[] csvData = reportService.generatePortfolioCsv(groupId, user); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType("text/csv")); - headers.setContentDispositionFormData("attachment", "lista_projektow_grupy.csv"); + ContentDisposition disposition = ContentDisposition.attachment() + .filename(type.getFilename()) + .build(); + headers.setContentDisposition(disposition); return ResponseEntity.ok() .headers(headers) - .body(csvData); + .body(reportData); } -} +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportType.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportType.java new file mode 100644 index 00000000..89852c4f --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportType.java @@ -0,0 +1,24 @@ +package pl.edu.agh.project_manager.controller.report; + +public enum ReportType { + PROJECT_CARD_PDF("karta_projektu.pdf", "application/pdf"), + PROJECT_RISKS_PDF("rejestr_ryzyk.pdf", "application/pdf"), + PROJECT_RISKS_CSV("rejestr_ryzyk.csv", "text/csv"), + GROUP_PROJECTS_CSV("lista_projektow_grupy.csv", "text/csv"); + + private final String filename; + private final String contentType; + + ReportType(String filename, String contentType) { + this.filename = filename; + this.contentType = contentType; + } + + public String getFilename() { + return filename; + } + + public String getContentType() { + return contentType; + } +} \ No newline at end of file diff --git a/frontend/src/features/project/components/ProjectHeader.tsx b/frontend/src/features/project/components/ProjectHeader.tsx index 898febfa..05b9ceac 100644 --- a/frontend/src/features/project/components/ProjectHeader.tsx +++ b/frontend/src/features/project/components/ProjectHeader.tsx @@ -5,10 +5,10 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { ProjectGroupType } from "@/features/project_group/project_group.types.ts"; import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useReportDownload } from "@/features/report/report.hooks"; @@ -19,20 +19,8 @@ interface ProjectHeaderProps { export const ProjectHeader = ({ details }: ProjectHeaderProps) => { const groupName = details.group?.name; const isWallet = details.group?.groupType === ProjectGroupType.WALLET; - - const { downloadReport, isDownloading } = useReportDownload(); - - const handleDownloadCardPdf = () => { - downloadReport(`/projects/${details.id}/reports/card/pdf`, 'karta_projektu.pdf'); - }; - const handleDownloadRisksCsv = () => { - downloadReport(`/projects/${details.id}/reports/risks/csv`, 'ryzyka.csv'); - }; - - const handleDownloadRisksPdf = () => { - downloadReport(`/projects/${details.id}/reports/risks/pdf`, 'ryzyka.pdf'); - }; + const { downloadReport, isDownloading } = useReportDownload(); return (
@@ -65,6 +53,7 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => {
+ + - + downloadReport(`/reports/${details.id}?type=PROJECT_CARD_PDF`, 'karta_projektu.pdf')} + className="cursor-pointer" + > Karta Projektu (PDF) - + + downloadReport(`/reports/${details.id}?type=PROJECT_RISKS_CSV`, 'ryzyka.csv')} + className="cursor-pointer" + > Rejestr Ryzyk (CSV) - + + downloadReport(`/reports/${details.id}?type=PROJECT_RISKS_PDF`, 'ryzyka.pdf')} + className="cursor-pointer" + > Rejestr Ryzyk (PDF) diff --git a/frontend/src/features/report/report.hooks.ts b/frontend/src/features/report/report.hooks.ts index 6b7a4456..118fb579 100644 --- a/frontend/src/features/report/report.hooks.ts +++ b/frontend/src/features/report/report.hooks.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; import { toast } from 'sonner'; -import api from '@/api/client'; +import { reportService } from './report.service'; export const useReportDownload = () => { const [isDownloading, setIsDownloading] = useState(false); @@ -8,9 +8,7 @@ export const useReportDownload = () => { const downloadReport = async (url: string, filename: string) => { setIsDownloading(true); try { - const response = await api.get(url, { - responseType: 'blob', - }); + const response = await reportService.getReportFile(url); const disposition = response.headers['content-disposition'] as string | undefined; let finalFilename = filename; diff --git a/frontend/src/features/report/report.service.ts b/frontend/src/features/report/report.service.ts new file mode 100644 index 00000000..01e05265 --- /dev/null +++ b/frontend/src/features/report/report.service.ts @@ -0,0 +1,10 @@ +import api from '@/api/client'; +import type { AxiosResponse } from 'axios'; + +export const reportService = { + getReportFile: async (url: string): Promise> => { + return await api.get(url, { + responseType: 'blob', + }); + } +}; \ No newline at end of file From d7bae96c233699d884a01a416b7d5d7237b1c0a4 Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Tue, 26 May 2026 01:22:21 +0200 Subject: [PATCH 16/17] Rewiev fixes --- .../controller/report/ReportController.java | 6 +- .../security/access/GroupAccess.java | 33 +++++ .../service/report/ReportService.java | 20 --- .../components/GroupSection/GroupSection.tsx | 126 +++++++++--------- .../project/components/ProjectHeader.tsx | 13 +- frontend/src/features/report/report.hooks.ts | 31 ++++- 6 files changed, 135 insertions(+), 94 deletions(-) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/GroupAccess.java diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java index 82098852..da341319 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import pl.edu.agh.project_manager.security.UserPrincipal; @@ -19,8 +20,11 @@ public class ReportController { private final ReportService reportService; + @PreAuthorize("hasAnyAuthority('ADMINISTRATOR', 'AUTHORITY') or " + + "(#type.name().startsWith('PROJECT_') and @projectAccess.canAccessProject(#resourceId, principal)) or " + + "(#type.name().startsWith('GROUP_') and @groupAccess.canAccessGroup(#resourceId, principal))") @GetMapping("/{resourceId}") - public ResponseEntity downloadRisksCsv( + public ResponseEntity downloadReport( @PathVariable UUID resourceId, @RequestParam ReportType type, @AuthenticationPrincipal UserPrincipal user diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/GroupAccess.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/GroupAccess.java new file mode 100644 index 00000000..28bcc162 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/GroupAccess.java @@ -0,0 +1,33 @@ +package pl.edu.agh.project_manager.security.access; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import pl.edu.agh.project_manager.repository.projectgroup.ProjectGroupRepository; +import pl.edu.agh.project_manager.security.UserPrincipal; + +import java.util.UUID; + +@Component("groupAccess") +@RequiredArgsConstructor +public class GroupAccess { + + private final ProjectGroupRepository projectGroupRepository; + private final ProjectAccess projectAccess; + + public boolean canAccessGroup(UUID groupId, UserPrincipal currentUser) { + if (currentUser == null) { + return false; + } + + return projectGroupRepository.findById(groupId) + .map(group -> { + if (group.getOwner().getId().equals(currentUser.userId())) { + return true; + } + + return group.getProjects().stream() + .anyMatch(project -> projectAccess.canAccessProject(project.getId(), currentUser)); + }) + .orElse(false); + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java index b49e7734..c77c451f 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java @@ -4,10 +4,7 @@ import org.springframework.stereotype.Service; import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; -import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; -import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.security.UserPrincipal; -import pl.edu.agh.project_manager.security.access.ProjectAccess; import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand; import pl.edu.agh.project_manager.service.project.ProjectRiskService; import pl.edu.agh.project_manager.service.project.ProjectService; @@ -21,29 +18,22 @@ public class ReportService { private final ProjectService projectService; private final ProjectRiskService projectRiskService; - private final ProjectAccess projectAccess; private final RiskCsvGenerator riskCsvGenerator; private final ProjectPdfGenerator projectPdfGenerator; private final RiskPdfGenerator riskPdfGenerator; private final PortfolioCsvGenerator portfolioCsvGenerator; public byte[] generateProjectRisksCsv(UUID projectId, UserPrincipal user) { - checkAccess(projectId, user); - List risks = projectRiskService.getProjectRisks(projectId); return riskCsvGenerator.generate(risks); } public byte[] generateProjectCardPdf(UUID projectId, UserPrincipal user) { - checkAccess(projectId, user); - ProjectResponse project = projectService.getProject(projectId); return projectPdfGenerator.generate(project, "project-card-template"); } public byte[] generateProjectRisksPdf(UUID projectId, UserPrincipal user) { - checkAccess(projectId, user); - List risks = projectRiskService.getProjectRisks(projectId); return riskPdfGenerator.generate(risks, "project-risks-template"); } @@ -53,14 +43,4 @@ public byte[] generatePortfolioCsv(UUID groupId, UserPrincipal user) { List projects = projectService.searchProjects(command); return portfolioCsvGenerator.generate(projects); } - - private void checkAccess(UUID projectId, UserPrincipal user) { - boolean hasAccess = user.userRole().name().equals("ADMINISTRATOR") || - user.userRole().name().equals("AUTHORITY") || - projectAccess.canAccessProject(projectId, user); - - if (!hasAccess) { - throw new ApplicationException(ApiErrorCode.ACCESS_DENIED, "Brak dostępu do projektu."); - } - } } diff --git a/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx b/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx index 953fecef..5235d305 100644 --- a/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx +++ b/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx @@ -9,79 +9,75 @@ import { Button } from '@/components/ui/button'; import { useReportDownload } from '@/features/report/report.hooks'; interface GroupSectionProps { - title: string; - groups: ProjectGroupResponse[]; - icon: React.ElementType; - badgeVariant?: "default" | "secondary" | "destructive" | "outline"; + title: string; + groups: ProjectGroupResponse[]; + icon: React.ElementType; + badgeVariant?: "default" | "secondary" | "destructive" | "outline"; } export const GroupSection = ({ - title, - groups, - icon: Icon, - badgeVariant = "default" + title, + groups, + icon: Icon, + badgeVariant = "default" }: GroupSectionProps) => { - const [openGroups, setOpenGroups] = useState>( - () => groups.reduce((acc, g) => ({ ...acc, [g.id]: true }), {}) - ); - const { downloadReport, isDownloading } = useReportDownload(); + const [openGroups, setOpenGroups] = useState>( + () => groups.reduce((acc, g) => ({ ...acc, [g.id]: true }), {}) + ); - const toggleGroup = (id: string) => { - setOpenGroups(prev => ({ ...prev, [id]: !prev[id] })); - }; + const { downloadGroupProjectsCsv, isDownloading } = useReportDownload(); - const handleDownload = (groupId: string, groupName: string) => { - const filename = `zestawienie_${groupName.toLowerCase().replace(/\s+/g, '_')}.csv`; - downloadReport(`/groups/${groupId}/reports/projects/csv`, filename); - }; + const toggleGroup = (id: string) => { + setOpenGroups(prev => ({ ...prev, [id]: !prev[id] })); + }; - if (groups.length === 0) return null; + if (groups.length === 0) return null; - return ( -
-

- - {title} -

- + return ( +
+

+ + {title} +

+ -
- {groups.map(group => ( - toggleGroup(group.id)} - className="border rounded-lg bg-white shadow-sm overflow-hidden" - > -
- - - {group.name} - - {group.projects.length} {group.projects.length === 1 ? 'projekt' : (group.projects.length > 1 && group.projects.length < 5) ? 'projekty' : 'projektów'} - - - - -
+
+ {groups.map(group => ( + toggleGroup(group.id)} + className="border rounded-lg bg-white shadow-sm overflow-hidden" + > +
+ + + {group.name} + + {group.projects.length} {group.projects.length === 1 ? 'projekt' : (group.projects.length > 1 && group.projects.length < 5) ? 'projekty' : 'projektów'} + + + + +
- -
- -
-
-
- ))} -
-
- ); + +
+ +
+
+ + ))} +
+
+ ); }; \ No newline at end of file diff --git a/frontend/src/features/project/components/ProjectHeader.tsx b/frontend/src/features/project/components/ProjectHeader.tsx index 05b9ceac..9e5ce2fe 100644 --- a/frontend/src/features/project/components/ProjectHeader.tsx +++ b/frontend/src/features/project/components/ProjectHeader.tsx @@ -20,7 +20,12 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => { const groupName = details.group?.name; const isWallet = details.group?.groupType === ProjectGroupType.WALLET; - const { downloadReport, isDownloading } = useReportDownload(); + const { + isDownloading, + downloadProjectCardPdf, + downloadProjectRisksCsv, + downloadProjectRisksPdf + } = useReportDownload(); return (
@@ -64,7 +69,7 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => { downloadReport(`/reports/${details.id}?type=PROJECT_CARD_PDF`, 'karta_projektu.pdf')} + onClick={() => downloadProjectCardPdf(details.id)} className="cursor-pointer" > @@ -72,7 +77,7 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => { downloadReport(`/reports/${details.id}?type=PROJECT_RISKS_CSV`, 'ryzyka.csv')} + onClick={() => downloadProjectRisksCsv(details.id)} className="cursor-pointer" > @@ -80,7 +85,7 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => { downloadReport(`/reports/${details.id}?type=PROJECT_RISKS_PDF`, 'ryzyka.pdf')} + onClick={() => downloadProjectRisksPdf(details.id)} className="cursor-pointer" > diff --git a/frontend/src/features/report/report.hooks.ts b/frontend/src/features/report/report.hooks.ts index 118fb579..e5d0b7e9 100644 --- a/frontend/src/features/report/report.hooks.ts +++ b/frontend/src/features/report/report.hooks.ts @@ -1,11 +1,11 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { toast } from 'sonner'; import { reportService } from './report.service'; export const useReportDownload = () => { const [isDownloading, setIsDownloading] = useState(false); - const downloadReport = async (url: string, filename: string) => { + const downloadReport = useCallback(async (url: string, filename: string) => { setIsDownloading(true); try { const response = await reportService.getReportFile(url); @@ -43,7 +43,30 @@ export const useReportDownload = () => { } finally { setIsDownloading(false); } - }; + }, []); + + const downloadProjectCardPdf = useCallback((projectId: string) => { + downloadReport(`/reports/${projectId}?type=PROJECT_CARD_PDF`, 'karta_projektu.pdf'); + }, [downloadReport]); + + const downloadProjectRisksCsv = useCallback((projectId: string) => { + downloadReport(`/reports/${projectId}?type=PROJECT_RISKS_CSV`, 'ryzyka.csv'); + }, [downloadReport]); - return { downloadReport, isDownloading }; + const downloadProjectRisksPdf = useCallback((projectId: string) => { + downloadReport(`/reports/${projectId}?type=PROJECT_RISKS_PDF`, 'ryzyka.pdf'); + }, [downloadReport]); + + const downloadGroupProjectsCsv = useCallback((groupId: string, groupName: string) => { + const filename = `zestawienie_${groupName.toLowerCase().replace(/\s+/g, '_')}.csv`; + downloadReport(`/reports/${groupId}?type=GROUP_PROJECTS_CSV`, filename); + }, [downloadReport]); + + return { + isDownloading, + downloadProjectCardPdf, + downloadProjectRisksCsv, + downloadProjectRisksPdf, + downloadGroupProjectsCsv + }; }; \ No newline at end of file From 9b0b08db57a670f4c6ebfc6ac4fbed99d0dbf3fd Mon Sep 17 00:00:00 2001 From: PatrykBlacha Date: Tue, 26 May 2026 01:29:05 +0200 Subject: [PATCH 17/17] Tests update --- .../service/report/ReportServiceTest.java | 65 ++++--------------- 1 file changed, 13 insertions(+), 52 deletions(-) diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java index d393c258..34836b2c 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java @@ -11,9 +11,7 @@ import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskResponse; import pl.edu.agh.project_manager.domain.enums.UserRole; -import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.security.UserPrincipal; -import pl.edu.agh.project_manager.security.access.ProjectAccess; import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand; import pl.edu.agh.project_manager.service.project.ProjectRiskService; import pl.edu.agh.project_manager.service.project.ProjectService; @@ -22,7 +20,6 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -31,16 +28,13 @@ class ReportServiceTest { @Mock private ProjectService projectService; - + @Mock private ProjectRiskService projectRiskService; - - @Mock - private ProjectAccess projectAccess; - + @Mock private RiskCsvGenerator riskCsvGenerator; - + @Mock private ProjectPdfGenerator projectPdfGenerator; @@ -54,22 +48,19 @@ class ReportServiceTest { private ReportService reportService; private UUID projectId; - private UserPrincipal adminUser; private UserPrincipal normalUser; @BeforeEach void setUp() { projectId = UUID.randomUUID(); - adminUser = new UserPrincipal(UUID.randomUUID(), "admin@test.com", "pass", "A", "A", List.of(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR")), UserRole.ADMINISTRATOR); normalUser = new UserPrincipal(UUID.randomUUID(), "user@test.com", "pass", "U", "U", List.of(new SimpleGrantedAuthority("ROLE_COMMON")), UserRole.COMMON); } @Test - @DisplayName("Should generate risks CSV when user has access") - void generateProjectRisksCsv_HasAccess() { + @DisplayName("Should generate risks CSV successfully") + void generateProjectRisksCsv() { // Given - when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); - List risks = List.of(new ProjectRiskResponse(UUID.randomUUID(), "R1", "D1", 50,21,2)); + List risks = List.of(new ProjectRiskResponse(UUID.randomUUID(), "R1", "D1", 50, 21, 2)); when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); byte[] expectedCsv = "test-csv".getBytes(); when(riskCsvGenerator.generate(risks)).thenReturn(expectedCsv); @@ -84,38 +75,9 @@ void generateProjectRisksCsv_HasAccess() { } @Test - @DisplayName("Should generate risks CSV for admin without explicit project access") - void generateProjectRisksCsv_Admin() { - // Given - List risks = List.of(); - when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); - when(riskCsvGenerator.generate(risks)).thenReturn(new byte[0]); - - // When - reportService.generateProjectRisksCsv(projectId, adminUser); - - // Then - verify(projectAccess, never()).canAccessProject(any(), any()); - verify(projectRiskService).getProjectRisks(projectId); - } - - @Test - @DisplayName("Should throw exception when generating CSV and no access") - void generateProjectRisksCsv_NoAccess() { - // Given - when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(false); - - // When & Then - assertThatThrownBy(() -> reportService.generateProjectRisksCsv(projectId, normalUser)) - .isInstanceOf(ApplicationException.class) - .hasMessageContaining("Brak dostępu"); - } - - @Test - @DisplayName("Should generate project card PDF when user has access") - void generateProjectCardPdf_HasAccess() { + @DisplayName("Should generate project card PDF successfully") + void generateProjectCardPdf() { // Given - when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); ProjectResponse project = new ProjectResponse(projectId, "Test", "Desc", null, null, true, null, null); when(projectService.getProject(projectId)).thenReturn(project); byte[] expectedPdf = "test-pdf".getBytes(); @@ -131,11 +93,10 @@ void generateProjectCardPdf_HasAccess() { } @Test - @DisplayName("Should generate risks PDF when user has access") - void generateProjectRisksPdf_HasAccess() { + @DisplayName("Should generate risks PDF successfully") + void generateProjectRisksPdf() { // Given - when(projectAccess.canAccessProject(projectId, normalUser)).thenReturn(true); - List risks = List.of(new ProjectRiskResponse(UUID.randomUUID(), "R1", "D1", 50,27,2)); + List risks = List.of(new ProjectRiskResponse(UUID.randomUUID(), "R1", "D1", 50, 27, 2)); when(projectRiskService.getProjectRisks(projectId)).thenReturn(risks); byte[] expectedPdf = "test-pdf".getBytes(); when(riskPdfGenerator.generate(risks, "project-risks-template")).thenReturn(expectedPdf); @@ -150,8 +111,8 @@ void generateProjectRisksPdf_HasAccess() { } @Test - @DisplayName("Should generate portfolio CSV") - void generatePortfolioCsv_Success() { + @DisplayName("Should generate portfolio CSV successfully") + void generatePortfolioCsv() { // Given UUID groupId = UUID.randomUUID(); List projects = List.of(new ProjectResponse(projectId, "Test", "Desc", null, null, true, null, null));