From 95aa5228e94cba4cd6fa8ee4f3bb0bc30bb73742 Mon Sep 17 00:00:00 2001 From: mramirezg Date: Wed, 24 Sep 2025 16:17:54 -0600 Subject: [PATCH] Se agrego la funcionalidad para poder exportar los tickets a una hoja de calculo --- .../gob/jumapacelaya/api/RedmineClient.java | 7 +- .../mx/gob/jumapacelaya/models/Ticket.java | 13 +- .../mx/gob/jumapacelaya/ui/ActDiariaView.java | 173 ++++++++++++++---- 3 files changed, 155 insertions(+), 38 deletions(-) diff --git a/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java b/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java index 5fc2c4c..7fb61dd 100644 --- a/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java +++ b/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java @@ -58,7 +58,7 @@ public class RedmineClient { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { String responseBody = response.body(); - log.info(responseBody); + //log.info(responseBody); tickets.addAll(parseTickets(responseBody)); } else { System.err.println("Error en la respuesta: " + response.statusCode()); @@ -144,6 +144,7 @@ public class RedmineClient { int id = issue.has("id") && !issue.get("id").isJsonNull() ? issue.get("id").getAsInt() : 0; String subject = issue.has("subject") && !issue.get("subject").isJsonNull() ? issue.get("subject").getAsString() : ""; String description = issue.has("description") && !issue.get("description").isJsonNull() ? issue.get("description").getAsString() : ""; + Double estimatedHrs = issue.has("estimated_hours") && !issue.get("estimated_hours").isJsonNull() ? issue.get("estimated_hours").getAsDouble() : 0.0; // Status String status = "Unknown"; @@ -188,7 +189,9 @@ public class RedmineClient { dateUpdate != null ? dateUpdate.toString() : "", autor, trackerId, - "Tipo Desconocido" + "Tipo Desconocido", + estimatedHrs + ); // Log para verificar JSON de ticket diff --git a/src/main/java/mx/gob/jumapacelaya/models/Ticket.java b/src/main/java/mx/gob/jumapacelaya/models/Ticket.java index 2cca609..18b1b2a 100644 --- a/src/main/java/mx/gob/jumapacelaya/models/Ticket.java +++ b/src/main/java/mx/gob/jumapacelaya/models/Ticket.java @@ -18,12 +18,12 @@ public class Ticket { private User autor; private Integer trackerId; private String type; - + private Double estimatedHrs; public Ticket(int id, String subject, String description, String status, String dateCreate, String dateClose, String updateOn, - User autor, Integer trackerId, String type) { + User autor, Integer trackerId, String type, Double estimatedHrs) { this.id = id; this.subject = subject; this.description = description; @@ -36,6 +36,7 @@ public class Ticket { this.autor = autor; this.trackerId = trackerId; this.type = type; + this.estimatedHrs = estimatedHrs; } @@ -124,6 +125,14 @@ public class Ticket { return this.dateClose; } + public Double getEstimatedHrs() { + return estimatedHrs; + } + + public void setEstimatedHrs(Double estimatedHrs) { + this.estimatedHrs = estimatedHrs; + } + public static class User { private String username; diff --git a/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java b/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java index 7f873d9..3acf0e9 100644 --- a/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java +++ b/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java @@ -11,6 +11,7 @@ import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Span; @@ -25,8 +26,11 @@ import com.vaadin.flow.component.popover.Popover; import com.vaadin.flow.component.popover.PopoverPosition; import com.vaadin.flow.component.richtexteditor.RichTextEditor; import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.function.SerializableBiConsumer; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.StreamRegistration; +import com.vaadin.flow.server.StreamResource; import jakarta.annotation.security.PermitAll; import mx.gob.jumapacelaya.api.RedmineClient; import mx.gob.jumapacelaya.api.ServerProperties; @@ -36,8 +40,18 @@ import mx.gob.jumapacelaya.models.Ticket; import mx.gob.jumapacelaya.services.LdapService; import mx.gob.jumapacelaya.services.UserService; import org.apache.commons.lang3.StringUtils; - - +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vaadin.lineawesome.LineAwesomeIcon; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.time.LocalDate; @@ -54,11 +68,13 @@ import java.util.stream.Stream; @CssImport("./themes/sistema-mantenimiento/styles.css") public class ActDiariaView extends VerticalLayout { + private static final Logger log = LoggerFactory.getLogger(ActDiariaView.class); private final RedmineClient redmineClient; private final UserService userService; private final Grid grid; private final HorizontalLayout showColumnsLyt; private final Button btnColumns; + private final Button btnExport; private final HorizontalLayout opcionesLyt; private Checkbox chkSoloAbiertos; private DatePicker fechaDesde; @@ -126,20 +142,6 @@ public class ActDiariaView extends VerticalLayout { .setAutoWidth(true) .setKey("tiempoEst"); - grid.addColumn(Ticket::getSubject) - .setHeader("Asunto") - .setWidth("25rem") - .setKey("asunto"); - - grid.addColumn(ticket -> - ticket.getAuthor() != null ? ticket.getAuthor().getUsername() : "") - .setHeader("Autor") - .setAutoWidth(true) - .setKey("autor"); - - grid.addColumn(createStatusRender()).setHeader("Estado") - .setKey("estado"); - grid.addColumn(ticket -> { LocalDateTime fecha = ticket.getDateCreate(); return fecha != null ? fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) : ""; @@ -161,6 +163,29 @@ public class ActDiariaView extends VerticalLayout { .setAutoWidth(true) .setKey("fechaActualizacion"); + grid.addColumn(ticket -> { + if (ticket.getDateCreateLocal() != null && ticket.getDateCloseLocal() != null) { + LocalDateTime inicio = ticket.getDateCreateLocal(); + LocalDateTime fin = ticket.getDateCloseLocal(); + + long horasReales = ChronoUnit.HOURS.between(inicio, fin); + Double horasEstimadas = ticket.getEstimatedHrs(); + + if (horasEstimadas != null) { + return horasReales <= horasEstimadas ? "✅ En tiempo" : "⚠️ Excedido"; + } else { + return "Sin estimación"; + } + } else { + return ""; + } + }).setHeader("Situación").setAutoWidth(true).setKey("cumplimientoHoras"); + + grid.addColumn(createStatusRender()) + .setHeader("Estado") + .setKey("estado"); + + grid.addColumn(ticket -> { if (ticket.getDateCreateLocal() != null) { LocalDateTime fechaInicio = ticket.getDateCreateLocal(); @@ -174,6 +199,20 @@ public class ActDiariaView extends VerticalLayout { } }).setHeader("Duración").setAutoWidth(true).setKey("duracion"); + + grid.addColumn(ticket -> + ticket.getAuthor() != null ? ticket.getAuthor().getUsername() : "") + .setHeader("Autor") + .setAutoWidth(true) + .setKey("autor"); + + + grid.addColumn(Ticket::getSubject) + .setHeader("Asunto") + .setWidth("25rem") + .setKey("asunto"); + + grid.addComponentColumn(ticket -> { Button btnVer = new Button(new Icon(VaadinIcon.EYE)); btnVer.addClickListener(event -> showDescription(ticket)); @@ -187,7 +226,12 @@ public class ActDiariaView extends VerticalLayout { // Botón para mostrar/ocultar columnas btnColumns = new Button(VaadinIcon.GRID_H.create()); - showColumnsLyt = new HorizontalLayout(btnColumns); + // Botón para exportar + btnExport = new Button("Exportar", LineAwesomeIcon.FILE_EXCEL.create()); + btnExport.addClickListener(e -> exportTicketsCerrados()); + showColumnsLyt = new HorizontalLayout(btnExport, btnColumns); + showColumnsLyt.setJustifyContentMode(JustifyContentMode.END); + showColumnsLyt.setWidthFull(); HorizontalLayout columnsSelectorLyt = new HorizontalLayout(); columnsSelectorLyt.setAlignItems(Alignment.END); Popover popover = new Popover(); @@ -217,7 +261,7 @@ public class ActDiariaView extends VerticalLayout { } }); }); - Set defaultColumns = Set.of("tipo","asunto","autor","estado","fechaCreacion","fechaCierre"); + Set defaultColumns = Set.of("tipo","tiempoEst","fechaCreacion","fechaCierre","cumplimientoHoras","estado","duracion","autor","asunto"); chkColumns.setValue(defaultColumns); popover.add(heading, chkColumns); @@ -262,27 +306,21 @@ public class ActDiariaView extends VerticalLayout { // Estilos basados en el estado del ticket switch (ticket.getStatus().toLowerCase()) { - case "análisis": - span.getElement().getStyle().set("color","orange"); - break; - case "desarrollo": - span.getElement().getStyle().set("color","blue"); - break; - case "rechazada": - span.getElement().getStyle().set("color","red"); + case "rechazado": + String theme1 = String.format("badge %s", "error"); + span.getElement().setAttribute("theme",theme1); break; - case "cerrada": - span.getElement().getStyle().set("color","grey"); - break; - case "solicitado": - span.getElement().getStyle().set("color","purple"); + case "resuelto","cerrado": + String theme2 = String.format("badge %s", "success"); + span.getElement().setAttribute("theme",theme2); break; case "qa": - span.getElement().getStyle().set("color","#C21DF2"); + String theme3 = String.format("badge %s", "warning"); + span.getElement().setAttribute("theme",theme3); break; default: - span.getElement().getStyle().set("color","green"); - break; + String theme4 = String.format("badge %s", ""); + span.getElement().setAttribute("theme",theme4); } return span; @@ -348,4 +386,71 @@ public class ActDiariaView extends VerticalLayout { dialog.open(); } + + + // Metodo para exportar los tickets cerrados a un archivo de Excel + private void exportTicketsCerrados() { + try { + RedmineUser user = userService.getRedmineUser(); + + List ticketsCerrados = redmineClient.getTickets(user, true, 0, 10000); + + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Tickets cerrados"); + + String[] headers = {"ID","Tipo","Estado","Autor","Asunto","Fecha Creación","Fecha Cierre","Fecha actualización","Duración","Situación"}; + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + } + + int rowNum = 1; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); + + for (Ticket ticket : ticketsCerrados) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(ticket.getId()); + row.createCell(1).setCellValue(ticket.getType()); + row.createCell(2).setCellValue(ticket.getStatus()); + row.createCell(3).setCellValue(ticket.getAuthor() != null ? ticket.getAuthor().getUsername() : ""); + row.createCell(4).setCellValue(ticket.getSubject()); + row.createCell(5).setCellValue(ticket.getDateCreate() != null ? ticket.getDateCreate().format(formatter) : ""); + row.createCell(6).setCellValue(ticket.getDateClose() != null ? ticket.getDateClose().format(formatter) : ""); + row.createCell(7).setCellValue(ticket.getUpdateOn() != null ? ticket.getUpdateOn().format(formatter) : ""); + + if (ticket.getDateCreateLocal() != null && ticket.getDateCloseLocal() != null) { + long dias = ChronoUnit.DAYS.between(ticket.getDateCreateLocal(), ticket.getDateCloseLocal()); + row.createCell(8).setCellValue(dias + " dias"); + } else { + row.createCell(8).setCellValue(""); + } + + if (ticket.getEstimatedHrs() != null && ticket.getDateCreateLocal() != null && ticket.getDateCloseLocal() != null) { + long horasReales = ChronoUnit.HOURS.between(ticket.getDateCreateLocal(), ticket.getDateCloseLocal()); + row.createCell(9).setCellValue(horasReales <= ticket.getEstimatedHrs() ? "En tiempo" : "Excedido"); + } else { + row.createCell(9).setCellValue("Sin estimación"); + } + } + + for (int i = 0; i < headers.length; i++) { + sheet.autoSizeColumn(i); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + workbook.write(out); + workbook.close(); + + StreamResource resource = new StreamResource("tickets_cerrados.xlsx", () -> new ByteArrayInputStream(out.toByteArray())); + + StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry().registerResource(resource); + UI.getCurrent().getPage().executeJs("window.open('" + registration.getResourceUri().toString() + "','_blank')"); + + } catch (Exception e) { + log.error("Error exportando tickets", e); + Notification.show("Error al exportar los tickets", 5000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + } + } }