diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index bd01910..1c0b2fe 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java b/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java index 5ab93f0..5fc2c4c 100644 --- a/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java +++ b/src/main/java/mx/gob/jumapacelaya/api/RedmineClient.java @@ -3,6 +3,8 @@ package mx.gob.jumapacelaya.api; import com.google.gson.*; import mx.gob.jumapacelaya.models.RedmineUser; import mx.gob.jumapacelaya.models.Ticket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -12,6 +14,9 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; @@ -21,11 +26,11 @@ import java.util.Map; @Component public class RedmineClient { + private static final Logger log = LoggerFactory.getLogger(RedmineClient.class); - private static final int PAGE_SIZE = 25; + //private static final int PAGE_SIZE = 25; static String REDMINE_URL; static String API_KEY; - public static final Gson GSON = new Gson(); public RedmineClient(@Value("${redmine.url}") String redmineUrl, @Value("${redmine.api_key}") String apiKey) { REDMINE_URL = redmineUrl; @@ -52,7 +57,9 @@ public class RedmineClient { try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { - tickets.addAll(parseTickets(response.body())); + String responseBody = response.body(); + log.info(responseBody); + tickets.addAll(parseTickets(responseBody)); } else { System.err.println("Error en la respuesta: " + response.statusCode()); } @@ -133,16 +140,12 @@ public class RedmineClient { for (JsonElement issueElement : issues) { JsonObject issue = issueElement.getAsJsonObject(); - // Verifica y obtiene el ID + // ID, subject, descripción int id = issue.has("id") && !issue.get("id").isJsonNull() ? issue.get("id").getAsInt() : 0; - - // Verifica y obtiene el subject String subject = issue.has("subject") && !issue.get("subject").isJsonNull() ? issue.get("subject").getAsString() : ""; - - // Verifica y obtiene la descripción String description = issue.has("description") && !issue.get("description").isJsonNull() ? issue.get("description").getAsString() : ""; - // Verifica y obtiene el status + // Status String status = "Unknown"; if (issue.has("status") && !issue.get("status").isJsonNull()) { JsonObject statusObject = issue.getAsJsonObject("status"); @@ -151,57 +154,21 @@ public class RedmineClient { } } - // Verifica y obtiene la fecha de creación - String dateString = issue.has("created_on") && !issue.get("created_on").isJsonNull() ? issue.get("created_on").getAsString() : ""; - LocalDate dateCreate = null; - if (!dateString.isEmpty()) { - try { - DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; - dateCreate = LocalDate.parse(dateString, formatter); - } catch (DateTimeParseException e) { - System.err.println("Error al parsear la fecha: " + dateString); - e.printStackTrace(); - } - } - - // Verifica y obtiene la fecha de cierre - String closeDateString = issue.has("closed_on") && !issue.get("closed_on").isJsonNull() ? issue.get("closed_on").getAsString() : ""; - LocalDate dateClose = null; - if (!closeDateString.isEmpty()) { - try { - DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; - dateClose = LocalDate.parse(closeDateString, formatter); - } catch (DateTimeParseException e) { - System.err.println("Error al parsear la fecha de cierre: " + closeDateString); - e.printStackTrace(); - } - } - - // Verifica y obtiene la fecha de actualizacion - String updateDateString = issue.has("updated_on") && !issue.get("updated_on").isJsonNull() ? issue.get("updated_on").getAsString() : ""; - LocalDate dateUpdate = null; - if (!updateDateString.isEmpty()) { - try { - DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; - dateUpdate = LocalDate.parse(updateDateString, formatter); - } catch (DateTimeParseException e) { - System.err.println("Error al parsear la fecha de actualización: " + updateDateString); - e.printStackTrace(); - } - } - + // Parse fechas correctamente + LocalDateTime dateCreate = parseDateTime(issue, "created_on"); + LocalDateTime dateUpdate = parseDateTime(issue, "updated_on"); + LocalDateTime dateClose = parseDateTime(issue, "closed_on"); + // Autor Ticket.User autor = null; if (issue.has("author") && !issue.get("author").isJsonNull()) { JsonObject authorObj = issue.getAsJsonObject("author"); if (authorObj.has("name") && !authorObj.get("name").isJsonNull()) { - String authorName = authorObj.get("name").getAsString(); - autor = new Ticket.User(authorName); + autor = new Ticket.User(authorObj.get("name").getAsString()); } } - - //Verifica y obtiene el ID del tipo de ticket + // Tracker ID Integer trackerId = null; if (issue.has("tracker") && !issue.get("tracker").isJsonNull()) { JsonObject trackerObject = issue.getAsJsonObject("tracker"); @@ -210,14 +177,24 @@ public class RedmineClient { } } - - // Agrega el ticket a la lista - tickets.add(new Ticket(id, subject, description, status, + // Crear ticket + Ticket ticketObj = new Ticket( + id, + subject, + description, + status, dateCreate != null ? dateCreate.toString() : "", dateClose != null ? dateClose.toString() : "", dateUpdate != null ? dateUpdate.toString() : "", autor, - trackerId, "Tipo Desconocido")); + trackerId, + "Tipo Desconocido" + ); + + // Log para verificar JSON de ticket + //System.out.println(GSON.toJson(ticketObj)); + + tickets.add(ticketObj); } } else { System.out.println("La respuesta JSON no contiene la clave 'issues'"); @@ -229,6 +206,7 @@ public class RedmineClient { return tickets; } + public RedmineUser getMyAccount(String username) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(REDMINE_URL + "/my/account.json")) @@ -306,7 +284,7 @@ public class RedmineClient { public RedmineUser getUserByUsername(String username) { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(REDMINE_URL + "/users.json?name=" + username )) + .uri(URI.create(REDMINE_URL + "/users.json?name=" + username)) .header("Content-Type", "application/json") .header("X-Redmine-API-Key", API_KEY) .build(); @@ -369,4 +347,37 @@ public class RedmineClient { System.err.println(response.body()); } } + + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, + (JsonSerializer) (src, typeOfSrc, context) -> + new JsonPrimitive(src.toString())) + .registerTypeAdapter(LocalDateTime.class, + (JsonDeserializer) (json, typeOf, context) -> + LocalDateTime.parse(json.getAsString())) + .setPrettyPrinting() + .create(); + + + // Método auxiliar para parsear echas con hora + private LocalDateTime parseDateTime(JsonObject issue, String fieldName) { + if (!issue.has(fieldName) || issue.get(fieldName).isJsonNull()) { + return null; + } + + String dateStr = issue.get(fieldName).getAsString(); + try { + // Si viene con 'Z' al final (UTC), convertir a LocalDateTime + if (dateStr.endsWith("Z")) { + return OffsetDateTime.parse(dateStr) + .atZoneSameInstant(ZoneId.systemDefault()) + .toLocalDateTime(); + } else { + return LocalDateTime.parse(dateStr); + } + } catch (DateTimeParseException ex) { + System.err.println("Error al parsear fecha '" + fieldName + "': " + dateStr); + return null; + } + } } diff --git a/src/main/java/mx/gob/jumapacelaya/models/Ticket.java b/src/main/java/mx/gob/jumapacelaya/models/Ticket.java index 3367010..2cca609 100644 --- a/src/main/java/mx/gob/jumapacelaya/models/Ticket.java +++ b/src/main/java/mx/gob/jumapacelaya/models/Ticket.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.format.DateTimeFormatter; public class Ticket { private final int id; @@ -49,16 +50,16 @@ public class Ticket { return null; } try { - OffsetDateTime odt = OffsetDateTime.parse(dateStr); - return odt.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); - } catch (Exception e) { - try { - LocalDate ld = LocalDate.parse(dateStr); - return ld.atStartOfDay(); - } catch (Exception ex) { - System.err.println("Error al parsear fecha: " + dateStr); - return null; + if (dateStr.endsWith("Z")) { + return OffsetDateTime.parse(dateStr) + .atZoneSameInstant(ZoneId.systemDefault()) + .toLocalDateTime(); + } else { + return LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); } + } catch (Exception e) { + System.err.println("Error al parsear fecha: " + dateStr); + return null; } } diff --git a/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java b/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java index d7d9ce3..7f873d9 100644 --- a/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java +++ b/src/main/java/mx/gob/jumapacelaya/ui/ActDiariaView.java @@ -60,7 +60,7 @@ public class ActDiariaView extends VerticalLayout { private final HorizontalLayout showColumnsLyt; private final Button btnColumns; private final HorizontalLayout opcionesLyt; - private final Checkbox chkVerCerrados; + private Checkbox chkSoloAbiertos; private DatePicker fechaDesde; private DatePicker fechaHasta; private Button btnBuscar; @@ -72,7 +72,7 @@ public class ActDiariaView extends VerticalLayout { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); - // Configuracion de las opciones de arriba + // Layout de opciones opcionesLyt = new HorizontalLayout(); opcionesLyt.setWidthFull(); opcionesLyt.setMargin(false); @@ -83,26 +83,33 @@ public class ActDiariaView extends VerticalLayout { .set("padding", "1rem") .set("margin", "1rem auto"); + // Checkbox para ver solo abiertos + chkSoloAbiertos = new Checkbox("Ver solo abiertos"); + chkSoloAbiertos.setValue(false); // inicial: mostrar cerrados - chkVerCerrados = new Checkbox(); - chkVerCerrados.setLabel("Ver solo abiertos"); - chkVerCerrados.addValueChangeListener(e -> { - boolean soloAbiertos = e.getValue().equals(false); + fechaDesde = new DatePicker("Fecha desde:"); + fechaHasta = new DatePicker("Fecha hasta:"); + btnBuscar = new Button("Buscar"); + + // Listener del checkbox + chkSoloAbiertos.addValueChangeListener(e -> { + boolean soloAbiertos = e.getValue(); // marcado = abiertos loadTickets(soloAbiertos); - fechaDesde.setEnabled(false); - fechaHasta.setEnabled(false); - btnBuscar.setEnabled(false); + + fechaDesde.setEnabled(!soloAbiertos); + fechaHasta.setEnabled(!soloAbiertos); + btnBuscar.setEnabled(!soloAbiertos); }); - fechaDesde = new DatePicker("Fecha desde:"); + // Estado inicial fechaDesde.setEnabled(true); - - fechaHasta = new DatePicker("Fecha hasta:"); fechaHasta.setEnabled(true); + btnBuscar.setEnabled(true); - btnBuscar = new Button("Buscar"); - opcionesLyt.add(chkVerCerrados,fechaDesde,fechaHasta,btnBuscar); + // Cargar tickets iniciales (cerrados) + loadTickets(false); + opcionesLyt.add(chkSoloAbiertos, fechaDesde, fechaHasta, btnBuscar); // Configuración de columnas del grid grid.addColumn(Ticket::getId).setHeader("No.") @@ -114,19 +121,16 @@ public class ActDiariaView extends VerticalLayout { .setAutoWidth(true) .setKey("tipo"); - grid.addColumn(ticket -> ticket.tiempoEst(ticket.getTrackerId())) .setHeader("Tiempo estimado") .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") @@ -138,26 +142,25 @@ public class ActDiariaView extends VerticalLayout { grid.addColumn(ticket -> { LocalDateTime fecha = ticket.getDateCreate(); - return fecha != null ? fecha.format(formatter) : ""; + return fecha != null ? fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) : ""; }).setHeader("Fecha creación") .setAutoWidth(true) .setKey("fechaCreacion"); grid.addColumn(ticket -> { LocalDateTime fecha = ticket.getDateClose(); - return fecha != null ? fecha.format(formatter) : ""; + return fecha != null ? fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) : ""; }).setHeader("Fecha cierre") .setAutoWidth(true) .setKey("fechaCierre"); grid.addColumn(ticket -> { LocalDateTime fecha = ticket.getUpdateOn(); - return fecha != null ? fecha.format(formatter) : ""; + return fecha != null ? fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) : ""; }).setHeader("Fecha actualización") .setAutoWidth(true) .setKey("fechaActualizacion"); - grid.addColumn(ticket -> { if (ticket.getDateCreateLocal() != null) { LocalDateTime fechaInicio = ticket.getDateCreateLocal(); @@ -171,7 +174,6 @@ public class ActDiariaView extends VerticalLayout { } }).setHeader("Duración").setAutoWidth(true).setKey("duracion"); - grid.addComponentColumn(ticket -> { Button btnVer = new Button(new Icon(VaadinIcon.EYE)); btnVer.addClickListener(event -> showDescription(ticket)); @@ -179,18 +181,13 @@ public class ActDiariaView extends VerticalLayout { return btnVer; }); - //grid.addColumn(buttonTicketComponentRenderer()).setAutoWidth(true); - grid.addThemeVariants(GridVariant.LUMO_WRAP_CELL_CONTENT); grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES); - //grid.getStyle().set("opacity", "0.8"); - //grid.setAllRowsVisible(false); grid.setSizeFull(); - - // Mostrar boton para elegir que columnas mostrar - /**/btnColumns = new Button(VaadinIcon.GRID_H.create()); - /**/showColumnsLyt = new HorizontalLayout(btnColumns); + // Botón para mostrar/ocultar columnas + btnColumns = new Button(VaadinIcon.GRID_H.create()); + showColumnsLyt = new HorizontalLayout(btnColumns); HorizontalLayout columnsSelectorLyt = new HorizontalLayout(); columnsSelectorLyt.setAlignItems(Alignment.END); Popover popover = new Popover(); @@ -223,43 +220,41 @@ public class ActDiariaView extends VerticalLayout { Set defaultColumns = Set.of("tipo","asunto","autor","estado","fechaCreacion","fechaCierre"); chkColumns.setValue(defaultColumns); popover.add(heading, chkColumns); - /****/ - - // Ajustar tamaño del Grid y Layout + // Ajustar tamaños grid.setSizeFull(); setSizeFull(); add(opcionesLyt, showColumnsLyt, grid); - //expand(grid); setMargin(false); - - loadTickets(true); } + // NOTA: Ajuste en la carga de tickets para que ahora haga LazyLoad al cargar todos los ticktes cerrados private void loadTickets(boolean soloAbiertos) { RedmineUser user = userService.getRedmineUser(); + // Invertir parámetro para que coincida con RedmineClient + boolean paramCliente = !soloAbiertos; + grid.setItems( query -> { int offset = query.getOffset(); int limit = query.getLimit(); try { - List ticketsPage = redmineClient.getTickets(user, soloAbiertos, offset, limit); + List ticketsPage = redmineClient.getTickets(user, paramCliente, offset, limit); return ticketsPage.stream(); } catch (Exception e) { e.printStackTrace(); - return Stream.empty(); + return Stream.empty(); } }, - query -> { - return redmineClient.getTotalTickets(user, soloAbiertos); - } + query -> redmineClient.getTotalTickets(user, paramCliente) ); } + private ComponentRenderer createStatusRender() { return new ComponentRenderer<>(ticket -> { // Creamos un Span para mostrar el estado