diff --git a/stratify/pom.xml b/stratify/pom.xml
index bc2a1b6..01a4ffe 100644
--- a/stratify/pom.xml
+++ b/stratify/pom.xml
@@ -135,7 +135,11 @@
org.springframework.boot
spring-boot-starter-validation
-
+
+ com.opencsv
+ opencsv
+ 5.7.1
+
org.springframework.boot
spring-boot-starter-mail
diff --git a/stratify/src/main/java/com/quantum/stratify/config/SpringSecurityConfig.java b/stratify/src/main/java/com/quantum/stratify/config/SpringSecurityConfig.java
index 341c43b..397e03d 100644
--- a/stratify/src/main/java/com/quantum/stratify/config/SpringSecurityConfig.java
+++ b/stratify/src/main/java/com/quantum/stratify/config/SpringSecurityConfig.java
@@ -1,8 +1,6 @@
package com.quantum.stratify.config;
-import com.quantum.stratify.config.jwt.JwtAuthenticationEntryPoint;
-import com.quantum.stratify.config.jwt.JwtAuthorizationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -17,6 +15,9 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import com.quantum.stratify.config.jwt.JwtAuthenticationEntryPoint;
+import com.quantum.stratify.config.jwt.JwtAuthorizationFilter;
+
@EnableMethodSecurity
@EnableWebMvc
diff --git a/stratify/src/main/java/com/quantum/stratify/services/CsvDownloadService.java b/stratify/src/main/java/com/quantum/stratify/services/CsvDownloadService.java
new file mode 100644
index 0000000..8294997
--- /dev/null
+++ b/stratify/src/main/java/com/quantum/stratify/services/CsvDownloadService.java
@@ -0,0 +1,117 @@
+package com.quantum.stratify.services;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.stereotype.Service;
+
+import com.opencsv.CSVWriter;
+import com.quantum.stratify.web.dtos.PercentualStatusUsuarioDTO;
+import com.quantum.stratify.web.dtos.QuantidadeCardsPorSprintDTO;
+import com.quantum.stratify.web.dtos.QuantidadeCardsPorTagDTO;
+import com.quantum.stratify.web.dtos.ResponseQuantidadeCardsByPeriodo;
+import com.quantum.stratify.web.dtos.TempoMedioPorProjetoDTO;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class CsvDownloadService {
+
+ @Autowired
+ private UserStoryService userStoryService;
+
+ @Autowired
+ private FatoUserStoryTemporaisService fatoUserStoryTemporaisService;
+
+ @Autowired
+ private UserStoryTagService userStoryTagService;
+
+ @Autowired
+ private UserStoryStatusService userStoryStatusService;
+
+ @Autowired
+ private FatoEficienciaUserStoryService fatoEficienciaUserStoryService;
+ public InputStreamResource generateCsv(Long projectId, Long userId, String exportType) {
+ List csvData;
+
+ switch (exportType.toLowerCase()) {
+ case "cardsporsprint": {
+ List lista = userStoryService.getQuantidadeUserStoriesBySprint(projectId, userId);
+ csvData = new ArrayList<>();
+ csvData.add(new String[]{"Sprint", "Quantidade"});
+ for (QuantidadeCardsPorSprintDTO dto : lista) {
+ csvData.add(new String[]{dto.getSprint(), String.valueOf(dto.getQuantidade())});
+ }
+ log.info("ExportType=cardsporsprint → Encontrados {} registros para projeto={} usuário={}", lista.size(), projectId, userId);
+ break;
+ }
+ case "cardsporperiodo": {
+ List lista = fatoUserStoryTemporaisService.getUserStoriesByPeriodoAndUser(projectId, userId);
+ csvData = new ArrayList<>();
+ csvData.add(new String[]{"Período", "Criadas", "Finalizadas"});
+ for (ResponseQuantidadeCardsByPeriodo dto : lista) {
+ csvData.add(new String[]{
+ dto.periodo(),
+ String.valueOf(dto.quantidadeCriadas()),
+ String.valueOf(dto.quantidadeFinalizadas())
+ });
+ }
+ break;
+ }
+ case "cardsporetiqueta": {
+ List lista = userStoryTagService.getQuantidadeUserStoriesByTag(projectId, userId);
+ csvData = new ArrayList<>();
+ csvData.add(new String[]{"Tag", "Quantidade"});
+ for (QuantidadeCardsPorTagDTO dto : lista) {
+ csvData.add(new String[]{dto.getNomeTag(), String.valueOf(dto.getQuantidade())});
+ }
+ break;
+ }
+ case "cardsporstatus": {
+ List lista = userStoryStatusService.getPercentualUserStoriesPorStatus(projectId, userId);
+ csvData = new ArrayList<>();
+ csvData.add(new String[]{"Status", "Percentual"});
+ for (PercentualStatusUsuarioDTO dto : lista) {
+ csvData.add(new String[]{dto.getNomeStatus(), String.valueOf(dto.getPercentual())});
+ }
+ break;
+ }
+ case "tempomedio": {
+ List lista = fatoEficienciaUserStoryService.getTempoMedioFiltrado(projectId, userId);
+ csvData = new ArrayList<>();
+ csvData.add(new String[]{"ID User Story", "Descrição", "Tempo Médio", "Quantidade Retrabalho"});
+ for (TempoMedioPorProjetoDTO dto : lista) {
+ csvData.add(new String[]{
+ String.valueOf(dto.idUserStory()),
+ dto.descricao(),
+ String.valueOf(dto.tempoMedio()),
+ String.valueOf(dto.quantidadeRetrabalhos())
+ });
+ }
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("Tipo de exportação inválido: " + exportType);
+ }
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (CSVWriter writer = new CSVWriter(new OutputStreamWriter(out))) {
+ for (String[] line : csvData) {
+ writer.writeNext(line);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Erro ao gerar CSV", e);
+ }
+
+ return new InputStreamResource(new ByteArrayInputStream(out.toByteArray()));
+ }
+}
diff --git a/stratify/src/main/java/com/quantum/stratify/web/controllers/CsvDownloadController.java b/stratify/src/main/java/com/quantum/stratify/web/controllers/CsvDownloadController.java
new file mode 100644
index 0000000..676a3ed
--- /dev/null
+++ b/stratify/src/main/java/com/quantum/stratify/web/controllers/CsvDownloadController.java
@@ -0,0 +1,62 @@
+package com.quantum.stratify.web.controllers;
+
+import java.io.IOException;
+
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.quantum.stratify.services.CsvDownloadService;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/download")
+@CrossOrigin("*")
+@Slf4j
+@Tag(name = "CSV Download", description = "Exportação de relatórios em formato CSV")
+public class CsvDownloadController {
+
+ private final CsvDownloadService csvDownloadService;
+
+ @Operation(summary = "Exporta dados filtrados em formato CSV")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Arquivo CSV gerado com sucesso"),
+ @ApiResponse(responseCode = "400", description = "Parâmetros inválidos"),
+ @ApiResponse(responseCode = "404", description = "Dados não encontrados"),
+ @ApiResponse(responseCode = "500", description = "Erro interno ao gerar o CSV")
+ })
+ @GetMapping("/csv")
+ public ResponseEntity downloadCsv(
+ @Parameter(description = "Tipo de exportação (ex: cardsporsprint, cardsporperiodo, etc.)", required = true)
+ @RequestParam String exportType,
+
+ @Parameter(description = "ID do projeto (opcional)")
+ @RequestParam(required = false) Long projectId,
+
+ @Parameter(description = "ID do usuário (opcional)")
+ @RequestParam(required = false) Long userId
+ ) throws IOException {
+
+ String filename = exportType + ".csv";
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
+ headers.add(HttpHeaders.CONTENT_TYPE, "text/csv");
+
+ InputStreamResource resource = csvDownloadService.generateCsv(projectId, userId, exportType);
+ return new ResponseEntity<>(resource, headers, HttpStatus.OK);
+ }
+}
diff --git a/stratify/src/test/java/com/quantum/stratify/services/CsvDownloadTest.java b/stratify/src/test/java/com/quantum/stratify/services/CsvDownloadTest.java
new file mode 100644
index 0000000..9a00b4f
--- /dev/null
+++ b/stratify/src/test/java/com/quantum/stratify/services/CsvDownloadTest.java
@@ -0,0 +1,73 @@
+package com.quantum.stratify.services;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.core.io.InputStreamResource;
+
+import com.quantum.stratify.web.dtos.QuantidadeCardsPorSprintDTO;
+import com.quantum.stratify.web.dtos.ResponseQuantidadeCardsByPeriodo;
+
+class CsvDownloadServiceTest {
+
+ @Mock
+ private UserStoryService userStoryService;
+ @Mock
+ private FatoUserStoryTemporaisService fatoUserStoryTemporaisService;
+ @Mock
+ private UserStoryTagService userStoryTagService;
+ @Mock
+ private UserStoryStatusService userStoryStatusService;
+ @Mock
+ private FatoEficienciaUserStoryService fatoEficienciaUserStoryService;
+
+ @InjectMocks
+ private CsvDownloadService csvDownloadService;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ void shouldGenerateCsvForCardsporsprint() throws Exception {
+ when(userStoryService.getQuantidadeUserStoriesBySprint(1L, 1L)).thenReturn(
+ List.of(new QuantidadeCardsPorSprintDTO("Sprint 1", 5L))
+ );
+
+ InputStreamResource result = csvDownloadService.generateCsv(1L, 1L, "cardsporsprint");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(result.getInputStream()));
+
+ assertEquals("\"Sprint\",\"Quantidade\"", reader.readLine());
+ assertEquals("\"Sprint 1\",\"5\"", reader.readLine());
+ }
+
+ @Test
+ void shouldGenerateCsvForCardsporperiodo() throws Exception {
+ when(fatoUserStoryTemporaisService.getUserStoriesByPeriodoAndUser(1L, 1L)).thenReturn(
+ List.of(new ResponseQuantidadeCardsByPeriodo("2024-01", 3, 2))
+ );
+
+ InputStreamResource result = csvDownloadService.generateCsv(1L, 1L, "cardsporperiodo");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(result.getInputStream()));
+
+ assertEquals("\"Período\",\"Criadas\",\"Finalizadas\"", reader.readLine());
+ assertEquals("\"2024-01\",\"3\",\"2\"", reader.readLine());
+ }
+
+ @Test
+ void shouldThrowForInvalidExportType() {
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> csvDownloadService.generateCsv(1L, 1L, "invalido"));
+ assertEquals("Tipo de exportação inválido: invalido", ex.getMessage());
+ }
+}