diff --git a/backend/project-manager/build.gradle b/backend/project-manager/build.gradle index ad4ed91f..37a84301 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' @@ -68,4 +74,4 @@ tasks.named('test') { jar { enabled = false -} +} \ No newline at end of file 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' 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/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..da341319 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/report/ReportController.java @@ -0,0 +1,51 @@ +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.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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/reports") +@RequiredArgsConstructor +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 downloadReport( + @PathVariable UUID resourceId, + @RequestParam ReportType type, + @AuthenticationPrincipal UserPrincipal 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(type.getContentType())); + + ContentDisposition disposition = ContentDisposition.attachment() + .filename(type.getFilename()) + .build(); + headers.setContentDisposition(disposition); + + return ResponseEntity.ok() + .headers(headers) + .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/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/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/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/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/PortfolioCsvGenerator.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGenerator.java new file mode 100644 index 00000000..86e91777 --- /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.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 new file mode 100644 index 00000000..f194b64f --- /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.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/ReportService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java new file mode 100644 index 00000000..c77c451f --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/report/ReportService.java @@ -0,0 +1,46 @@ +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_risk.ProjectRiskResponse; +import pl.edu.agh.project_manager.security.UserPrincipal; +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; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ReportService { + + private final ProjectService projectService; + private final ProjectRiskService projectRiskService; + private final RiskCsvGenerator riskCsvGenerator; + private final ProjectPdfGenerator projectPdfGenerator; + private final RiskPdfGenerator riskPdfGenerator; + private final PortfolioCsvGenerator portfolioCsvGenerator; + + public byte[] generateProjectRisksCsv(UUID projectId, UserPrincipal user) { + List risks = projectRiskService.getProjectRisks(projectId); + return riskCsvGenerator.generate(risks); + } + + public byte[] generateProjectCardPdf(UUID projectId, UserPrincipal user) { + ProjectResponse project = projectService.getProject(projectId); + return projectPdfGenerator.generate(project, "project-card-template"); + } + + public byte[] generateProjectRisksPdf(UUID projectId, UserPrincipal 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); + } +} 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..445a8f8c --- /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_risk.ProjectRiskResponse; +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 (ProjectRiskResponse 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.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 new file mode 100644 index 00000000..6c5e4a6a --- /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_risk.ProjectRiskResponse; +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.REPORT_GENERATION_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/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; 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..bc3e872d --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/PortfolioCsvGeneratorTest.java @@ -0,0 +1,54 @@ +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/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..34836b2c --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/report/ReportServiceTest.java @@ -0,0 +1,131 @@ +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_risk.ProjectRiskResponse; +import pl.edu.agh.project_manager.domain.enums.UserRole; +import pl.edu.agh.project_manager.security.UserPrincipal; +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; + +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.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ReportServiceTest { + + @Mock + private ProjectService projectService; + + @Mock + private ProjectRiskService projectRiskService; + + @Mock + private RiskCsvGenerator riskCsvGenerator; + + @Mock + private ProjectPdfGenerator projectPdfGenerator; + + @Mock + private RiskPdfGenerator riskPdfGenerator; + + @Mock + private PortfolioCsvGenerator portfolioCsvGenerator; + + @InjectMocks + private ReportService reportService; + + private UUID projectId; + private UserPrincipal normalUser; + + @BeforeEach + void setUp() { + projectId = UUID.randomUUID(); + 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 successfully") + void generateProjectRisksCsv() { + // Given + 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); + + // When + byte[] result = reportService.generateProjectRisksCsv(projectId, normalUser); + + // Then + assertThat(result).isEqualTo(expectedCsv); + verify(projectRiskService).getProjectRisks(projectId); + verify(riskCsvGenerator).generate(risks); + } + + @Test + @DisplayName("Should generate project card PDF successfully") + void generateProjectCardPdf() { + // Given + 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 generate risks PDF successfully") + void generateProjectRisksPdf() { + // Given + 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); + + // 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 successfully") + void generatePortfolioCsv() { + // 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 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..cbc36ef4 --- /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_risk.ProjectRiskResponse; + +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 + 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); + + // 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\""); + } +} diff --git a/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx b/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx index 2230e3cf..5235d305 100644 --- a/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx +++ b/frontend/src/features/dashboard/components/GroupSection/GroupSection.tsx @@ -1,69 +1,83 @@ 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; - 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 [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(); - if (groups.length === 0) return null; + const toggleGroup = (id: string) => { + setOpenGroups(prev => ({ ...prev, [id]: !prev[id] })); + }; - return ( -
-

- - {title} -

- + if (groups.length === 0) return null; -
- {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'} - -
- -
+ 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'} + + + + +
+ + +
+ +
+
+
+ ))} +
+
+ ); }; \ 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 9e521579..9e5ce2fe 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; @@ -12,6 +20,13 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => { const groupName = details.group?.name; const isWallet = details.group?.groupType === ProjectGroupType.WALLET; + const { + isDownloading, + downloadProjectCardPdf, + downloadProjectRisksCsv, + downloadProjectRisksPdf + } = useReportDownload(); + return (
@@ -42,18 +57,56 @@ export const ProjectHeader = ({ details }: ProjectHeaderProps) => {
-
- - - {details.manager.name?.[0] || ''}{details.manager.surname?.[0] || ''} - - - -
- - {details.manager.name} {details.manager.surname} - - Kierownik Projektu +
+ + + + + + + + downloadProjectCardPdf(details.id)} + className="cursor-pointer" + > + + Karta Projektu (PDF) + + + downloadProjectRisksCsv(details.id)} + className="cursor-pointer" + > + + Rejestr Ryzyk (CSV) + + + downloadProjectRisksPdf(details.id)} + className="cursor-pointer" + > + + 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..e5d0b7e9 --- /dev/null +++ b/frontend/src/features/report/report.hooks.ts @@ -0,0 +1,72 @@ +import { useState, useCallback } from 'react'; +import { toast } from 'sonner'; +import { reportService } from './report.service'; + +export const useReportDownload = () => { + const [isDownloading, setIsDownloading] = useState(false); + + const downloadReport = useCallback(async (url: string, filename: string) => { + setIsDownloading(true); + try { + const response = await reportService.getReportFile(url); + + 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); + } + }, []); + + 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]); + + 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 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