diff --git a/javafx-client/.gitignore b/javafx-client/.gitignore new file mode 100644 index 0000000..80b9b40 --- /dev/null +++ b/javafx-client/.gitignore @@ -0,0 +1,7 @@ +target/ +*.class +*.jar +*.log +.idea/ +*.iml +.DS_Store diff --git a/javafx-client/pom.xml b/javafx-client/pom.xml index 6b7b70a..8065e4b 100644 --- a/javafx-client/pom.xml +++ b/javafx-client/pom.xml @@ -20,17 +20,12 @@ - + org.openjfx javafx-controls ${javafx.version} - - org.openjfx - javafx-fxml - ${javafx.version} - org.openjfx javafx-graphics @@ -41,13 +36,8 @@ javafx-base ${javafx.version} - - org.openjfx - javafx-swing - ${javafx.version} - - + com.fasterxml.jackson.core jackson-databind @@ -107,7 +97,7 @@ - + org.openjfx javafx-maven-plugin @@ -116,30 +106,6 @@ com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication - - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.0 - - - package - - shade - - - true - fat - - - com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication - - - - - - - \ No newline at end of file + diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.java index b5c78ae..015f937 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.java @@ -1,7 +1,8 @@ package com.teterialosjuanjos.tfg.javafxclient.api; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.teterialosjuanjos.tfg.javafxclient.model.*; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @@ -10,12 +11,12 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Base64; +import java.util.List; /** - * Cliente HTTP tipado para el sync-service en Railway. - * - *

Maneja autenticación Basic Auth para todos los endpoints.

+ * Cliente HTTP tipado para el sync-service. + * Usa java.net.http.HttpClient (Java 11+) con Basic Auth. */ @Slf4j public class SyncServiceClient { @@ -26,133 +27,81 @@ public class SyncServiceClient { private final ObjectMapper objectMapper; public SyncServiceClient(String baseUrl, String username, String password) { - this.baseUrl = baseUrl.replaceAll("/$", ""); // Remove trailing slash + this.baseUrl = baseUrl.replaceAll("/$", ""); this.httpClient = HttpClient.newHttpClient(); this.authHeader = buildBasicAuthHeader(username, password); - this.objectMapper = new ObjectMapper(); - this.objectMapper.registerModule(new JavaTimeModule()); + this.objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } - // ── Health Check ──────────────────────────────────────────────────────── + // ── Health ────────────────────────────────────────────────────────────── - public Map checkHealth() throws IOException, InterruptedException { - String url = baseUrl + "/actuator/health"; - return getJson(url, Map.class); + /** Lanza IOException si el servidor no responde 200. */ + public void checkHealth() throws IOException, InterruptedException { + HttpResponse response = sendRequest("GET", baseUrl + "/actuator/health", null); + if (response.statusCode() != 200) { + throw new IOException("Health check failed: HTTP " + response.statusCode()); + } } // ── Logs ───────────────────────────────────────────────────────────────── - /** - * Obtiene todos los logs de sincronización. - * - * @param type opcional, filtra por tipo (PRODUCT_PUSH, STOCK_PUSH, ORDER_PULL) - */ - public List> getLogs(String type) throws IOException, InterruptedException { - String url = baseUrl + "/api/logs"; - if (type != null && !type.isEmpty()) { - url += "?type=" + type; - } - - HttpResponse response = sendRequest("GET", url, null); - if (response.statusCode() == 200) { - return objectMapper.readValue( - response.body(), - objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class) - ); - } - throw new IOException("Failed to fetch logs: " + response.statusCode()); + public List getLogs(String type) throws IOException, InterruptedException { + String url = baseUrl + "/api/logs" + (type != null && !type.isEmpty() ? "?type=" + type : ""); + return getList(url, SyncLogResponse.class); } - /** - * Obtiene un log específico por ID. - */ - public Map getLog(Long id) throws IOException, InterruptedException { - String url = baseUrl + "/api/logs/" + id; - return getJson(url, Map.class); + public SyncLogResponse getLog(Long id) throws IOException, InterruptedException { + return get(baseUrl + "/api/logs/" + id, SyncLogResponse.class); } // ── Mappings ───────────────────────────────────────────────────────────── - /** - * Obtiene todos los mapeos de productos. - * - * @param status opcional, filtra por status (PENDING, SYNCED, ERROR) - */ - public List> getProductMappings(String status) - throws IOException, InterruptedException { - String url = baseUrl + "/api/mappings/products"; - if (status != null && !status.isEmpty()) { - url += "?status=" + status; - } - - HttpResponse response = sendRequest("GET", url, null); - if (response.statusCode() == 200) { - return objectMapper.readValue( - response.body(), - objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class) - ); - } - throw new IOException("Failed to fetch product mappings: " + response.statusCode()); + public List getProductMappings(String status) throws IOException, InterruptedException { + String url = baseUrl + "/api/mappings/products" + + (status != null && !status.isEmpty() ? "?status=" + status : ""); + return getList(url, ProductMappingResponse.class); } - /** - * Obtiene todos los mapeos de pedidos. - */ - public List> getOrderMappings() throws IOException, InterruptedException { - String url = baseUrl + "/api/mappings/orders"; - - HttpResponse response = sendRequest("GET", url, null); - if (response.statusCode() == 200) { - return objectMapper.readValue( - response.body(), - objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class) - ); - } - throw new IOException("Failed to fetch order mappings: " + response.statusCode()); + public List getOrderMappings() throws IOException, InterruptedException { + return getList(baseUrl + "/api/mappings/orders", OrderMappingResponse.class); } - // ── Sync Triggers ─────────────────────────────────────────────────────── + // ── Sync Triggers ──────────────────────────────────────────────────────── - /** - * Dispara sincronización de productos. - */ - public Map triggerProductSync() throws IOException, InterruptedException { - String url = baseUrl + "/api/sync/products"; - return postJson(url, new HashMap<>(), Map.class); + public SyncTriggerResponse triggerProductSync() throws IOException, InterruptedException { + return post(baseUrl + "/api/sync/products", SyncTriggerResponse.class); } - /** - * Dispara sincronización de stock. - */ - public Map triggerStockSync() throws IOException, InterruptedException { - String url = baseUrl + "/api/sync/stock"; - return postJson(url, new HashMap<>(), Map.class); + public SyncTriggerResponse triggerStockSync() throws IOException, InterruptedException { + return post(baseUrl + "/api/sync/stock", SyncTriggerResponse.class); } - /** - * Dispara sincronización de pedidos. - */ - public Map triggerOrderSync() throws IOException, InterruptedException { - String url = baseUrl + "/api/sync/orders"; - return postJson(url, new HashMap<>(), Map.class); + public SyncTriggerResponse triggerOrderSync() throws IOException, InterruptedException { + return post(baseUrl + "/api/sync/orders", SyncTriggerResponse.class); } // ── Helpers ────────────────────────────────────────────────────────────── - private T getJson(String url, Class responseType) throws IOException, InterruptedException { + private T get(String url, Class type) throws IOException, InterruptedException { + HttpResponse response = sendRequest("GET", url, null); + if (response.statusCode() == 200) return objectMapper.readValue(response.body(), type); + throw new IOException("HTTP " + response.statusCode() + ": " + response.body()); + } + + private List getList(String url, Class elementType) throws IOException, InterruptedException { HttpResponse response = sendRequest("GET", url, null); if (response.statusCode() == 200) { - return objectMapper.readValue(response.body(), responseType); + return objectMapper.readValue(response.body(), + objectMapper.getTypeFactory().constructCollectionType(List.class, elementType)); } throw new IOException("HTTP " + response.statusCode() + ": " + response.body()); } - private T postJson(String url, Object body, Class responseType) - throws IOException, InterruptedException { - String jsonBody = objectMapper.writeValueAsString(body); - HttpResponse response = sendRequest("POST", url, jsonBody); + private T post(String url, Class type) throws IOException, InterruptedException { + HttpResponse response = sendRequest("POST", url, "{}"); if (response.statusCode() == 200 || response.statusCode() == 201) { - return objectMapper.readValue(response.body(), responseType); + return objectMapper.readValue(response.body(), type); } throw new IOException("HTTP " + response.statusCode() + ": " + response.body()); } @@ -166,9 +115,7 @@ public class SyncServiceClient { .method(method, body != null ? HttpRequest.BodyPublishers.ofString(body) : HttpRequest.BodyPublishers.noBody()); - - HttpRequest request = builder.build(); - return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); } private String buildBasicAuthHeader(String username, String password) { @@ -176,4 +123,4 @@ public class SyncServiceClient { String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); return "Basic " + encoded; } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/AppSettings.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/AppSettings.java new file mode 100644 index 0000000..6a36304 --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/AppSettings.java @@ -0,0 +1,24 @@ +package com.teterialosjuanjos.tfg.javafxclient.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** Configuración de conexión al sync-service. */ +@Getter +@Setter +@AllArgsConstructor +public class AppSettings { + + private String baseUrl; + private String username; + private String password; + + public static AppSettings defaults() { + return new AppSettings( + "https://proyectointermodular-production-a9c3.up.railway.app", + "admin", + "admin123" + ); + } +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/OrderMappingResponse.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/OrderMappingResponse.java new file mode 100644 index 0000000..fa01206 --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/OrderMappingResponse.java @@ -0,0 +1,20 @@ +package com.teterialosjuanjos.tfg.javafxclient.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** Respuesta de la API para un mapeo de pedido. */ +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class OrderMappingResponse { + private Long id; + private Integer prestashopOrderId; + private Integer dolibarrOrderId; + private Integer dolibarrInvoiceId; + private String importedAt; // ISO-8601 + private String status; // IMPORTED | INVOICED | ERROR +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/ProductMappingResponse.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/ProductMappingResponse.java new file mode 100644 index 0000000..bc2d2db --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/ProductMappingResponse.java @@ -0,0 +1,21 @@ +package com.teterialosjuanjos.tfg.javafxclient.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** Respuesta de la API para un mapeo de producto. */ +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProductMappingResponse { + private Long id; + private String sku; + private Integer dolibarrId; + private Integer prestashopId; + private String lastSyncedAt; // ISO-8601, nullable + private String syncStatus; // SYNCED | PENDING | ERROR + private String errorMessage; +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.java index 9625931..2f0bf9b 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.java @@ -1,12 +1,14 @@ package com.teterialosjuanjos.tfg.javafxclient.model; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; +import com.teterialosjuanjos.tfg.javafxclient.util.SettingsStore; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** - * Gestiona la sesión actual: credenciales, cliente HTTP, estado de autenticación. + * Gestiona la sesión actual: credenciales, cliente HTTP y persistencia de ajustes. + * Carga los ajustes guardados en el constructor. */ @Slf4j @Getter @@ -20,16 +22,16 @@ public class SessionManager { private boolean authenticated; public SessionManager() { + AppSettings saved = SettingsStore.load(); + this.baseUrl = saved.getBaseUrl(); + this.username = saved.getUsername(); + this.password = saved.getPassword(); this.authenticated = false; - // Default: Railway production - this.baseUrl = "https://proyectointermodular-production-a9c3.up.railway.app"; } /** - * Intenta autenticarse contra el sync-service. - * Si es exitoso, inicializa el cliente HTTP. - * - * @return true si auth fue exitosa + * Autentica contra el sync-service verificando el health endpoint. + * Si tiene éxito, guarda los ajustes en SettingsStore. */ public boolean authenticate(String baseUrl, String username, String password) { this.baseUrl = baseUrl; @@ -38,9 +40,9 @@ public class SessionManager { try { this.syncServiceClient = new SyncServiceClient(baseUrl, username, password); - // Prueba conectividad syncServiceClient.checkHealth(); this.authenticated = true; + SettingsStore.save(new AppSettings(baseUrl, username, password)); log.info("Authentication successful for user: {}", username); return true; } catch (Exception e) { @@ -50,6 +52,30 @@ public class SessionManager { } } + /** + * Actualiza los ajustes y reinicializa el cliente HTTP sin health check. + * Usado desde la pantalla de ajustes para cambiar la configuración en caliente. + */ + public void updateSettings(String baseUrl, String username, String password) { + this.baseUrl = baseUrl; + this.username = username; + this.password = password; + this.syncServiceClient = new SyncServiceClient(baseUrl, username, password); + SettingsStore.save(new AppSettings(baseUrl, username, password)); + log.info("Settings updated, new base URL: {}", baseUrl); + } + + /** Comprueba conectividad con los ajustes dados sin modificar el estado. */ + public boolean testConnection(String baseUrl, String username, String password) { + try { + new SyncServiceClient(baseUrl, username, password).checkHealth(); + return true; + } catch (Exception e) { + log.debug("Connection test failed: {}", e.getMessage()); + return false; + } + } + public void logout() { this.authenticated = false; this.syncServiceClient = null; @@ -57,4 +83,4 @@ public class SessionManager { this.password = null; log.info("Logged out"); } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SyncLogResponse.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SyncLogResponse.java new file mode 100644 index 0000000..8a4c1a5 --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SyncLogResponse.java @@ -0,0 +1,21 @@ +package com.teterialosjuanjos.tfg.javafxclient.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** Respuesta de la API para un registro de sincronización. */ +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class SyncLogResponse { + private Long id; + private String syncType; + private String startedAt; // ISO-8601 + private String finishedAt; // ISO-8601, null si en curso + private Integer itemsProcessed; + private Integer itemsFailed; + private String errorDetails; +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SyncTriggerResponse.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SyncTriggerResponse.java new file mode 100644 index 0000000..e3136cf --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SyncTriggerResponse.java @@ -0,0 +1,21 @@ +package com.teterialosjuanjos.tfg.javafxclient.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +/** Respuesta de la API al disparar una sincronización manual. */ +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class SyncTriggerResponse { + private String syncType; + private int itemsProcessed; + private int itemsFailed; + private boolean hasErrors; + private List errors; +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.java index ce7517a..adccb96 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.java @@ -2,40 +2,37 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; +import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse; +import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse; import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; +import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.scene.control.Label; -import javafx.scene.paint.Color; +import javafx.scene.control.*; +import javafx.scene.layout.*; import javafx.util.Duration; import lombok.extern.slf4j.Slf4j; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; +import java.util.HashMap; import java.util.List; import java.util.Map; /** - * Dashboard con estado de sincronizaciones y estadísticas. + * Tab Dashboard: botón "Sincronizar todo", 3 tarjetas de sync con datos del último log. + * Equivalente a DashboardScreen de la app Android. */ @Slf4j public class DashboardController { private final SessionManager sessionManager; - private Label lastProductSyncLabel; - private Label lastStockSyncLabel; - private Label lastOrderSyncLabel; - private Label successRateLabel; - private Label totalSyncsLabel; + + private final Map lastSyncInfoLabels = new HashMap<>(); + private Button syncAllButton; + private Label syncAllStatusLabel; public DashboardController(SessionManager sessionManager) { this.sessionManager = sessionManager; @@ -43,190 +40,222 @@ public class DashboardController { public VBox createView() { VBox root = new VBox(20); - root.setPadding(new Insets(20)); - root.setStyle("-fx-background-color: #fafafa;"); + root.setPadding(new Insets(24)); + root.setStyle("-fx-background-color: #FAFAFA;"); - // Title - Label title = new Label("Synchronization Dashboard"); - title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); + // Header + Label appTitle = new Label("Sync Manager"); + appTitle.setStyle("-fx-font-size: 22; -fx-font-weight: bold; -fx-text-fill: #1565C0;"); + Label appSubtitle = new Label("Dolibarr · PrestaShop"); + appSubtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;"); + VBox headerBox = new VBox(2, appTitle, appSubtitle); - // Sync stats cards - GridPane statsGrid = createStatsGrid(); - - // Action buttons - HBox actionsBox = createActionsBox(); - - root.getChildren().addAll( - title, - statsGrid, - actionsBox, - new Label() // spacer + // Sync All button + syncAllButton = new Button("Sincronizar todo"); + syncAllButton.setMaxWidth(Double.MAX_VALUE); + syncAllButton.setPrefHeight(46); + syncAllButton.setStyle( + "-fx-background-color: #1565C0; -fx-text-fill: white; -fx-font-size: 14; " + + "-fx-font-weight: bold; -fx-background-radius: 10; -fx-cursor: hand;" ); + syncAllButton.setOnAction(e -> handleSyncAll()); - // Auto-refresh cada 10 segundos - startAutoRefresh(); + syncAllStatusLabel = new Label(""); + syncAllStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565C0;"); + syncAllStatusLabel.setVisible(false); + + // 3 sync cards + HBox cardsBox = new HBox(16); + VBox productCard = createSyncCard("📦", "Productos", "Dolibarr → PrestaShop", "PRODUCT_PUSH", "#1565C0"); + VBox stockCard = createSyncCard("📊", "Stock", "Dolibarr → PrestaShop", "STOCK_PUSH", "#2E7D32"); + VBox ordersCard = createSyncCard("🛒", "Pedidos", "PrestaShop → Dolibarr", "ORDER_PULL", "#E65100"); + + HBox.setHgrow(productCard, Priority.ALWAYS); + HBox.setHgrow(stockCard, Priority.ALWAYS); + HBox.setHgrow(ordersCard, Priority.ALWAYS); + cardsBox.getChildren().addAll(productCard, stockCard, ordersCard); + + root.getChildren().addAll(headerBox, syncAllButton, syncAllStatusLabel, cardsBox); - // Carga inicial refreshDashboard(); + startAutoRefresh(); return root; } - private GridPane createStatsGrid() { - GridPane grid = new GridPane(); - grid.setHgap(15); - grid.setVgap(15); - grid.setPadding(new Insets(10)); - grid.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0; -fx-border-radius: 4;"); - - // Card: Last Product Sync - VBox productCard = createStatCard( - "📦 Last Product Sync", - "Never", - "#1976d2" - ); - lastProductSyncLabel = (Label) productCard.getChildren().get(1); - GridPane.setConstraints(productCard, 0, 0); - - // Card: Last Stock Sync - VBox stockCard = createStatCard( - "📊 Last Stock Sync", - "Never", - "#388e3c" - ); - lastStockSyncLabel = (Label) stockCard.getChildren().get(1); - GridPane.setConstraints(stockCard, 1, 0); - - // Card: Last Order Sync - VBox orderCard = createStatCard( - "🛒 Last Order Sync", - "Never", - "#f57c00" - ); - lastOrderSyncLabel = (Label) orderCard.getChildren().get(1); - GridPane.setConstraints(orderCard, 2, 0); - - // Card: Total Syncs - VBox totalCard = createStatCard( - "📈 Total Syncs", - "0", - "#6a1b9a" - ); - totalSyncsLabel = (Label) totalCard.getChildren().get(1); - GridPane.setConstraints(totalCard, 0, 1); - - // Card: Success Rate - VBox rateCard = createStatCard( - "✓ Success Rate", - "0%", - "#00796b" - ); - successRateLabel = (Label) rateCard.getChildren().get(1); - GridPane.setConstraints(rateCard, 1, 1); - - grid.getChildren().addAll(productCard, stockCard, orderCard, totalCard, rateCard); - - return grid; - } - - private VBox createStatCard(String title, String value, String color) { + private VBox createSyncCard(String icon, String title, String direction, + String type, String color) { VBox card = new VBox(10); - card.setPadding(new Insets(15)); + card.setPadding(new Insets(16)); card.setStyle( - "-fx-background-color: white; " + - "-fx-border-color: " + color + "; " + - "-fx-border-width: 3 0 0 0; " + - "-fx-border-radius: 0;" + "-fx-background-color: white;" + + "-fx-background-radius: 12;" + + "-fx-border-radius: 12;" + + "-fx-border-color: #E0E0E0 #E0E0E0 #E0E0E0 " + color + ";" + + "-fx-border-width: 1 1 1 4;" + + "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.06), 6, 0, 0, 2);" ); - card.setMinWidth(250); - card.setAlignment(Pos.TOP_LEFT); - Label titleLabel = new Label(title); - titleLabel.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: " + color + ";"); + // Title row + button + HBox headerRow = new HBox(10); + headerRow.setAlignment(Pos.CENTER_LEFT); - Label valueLabel = new Label(value); - valueLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold; -fx-text-fill: #333333;"); + Label titleLabel = new Label(icon + " " + title); + titleLabel.setStyle("-fx-font-size: 14; -fx-font-weight: bold; -fx-text-fill: " + color + ";"); - card.getChildren().addAll(titleLabel, valueLabel); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + Button syncBtn = new Button("Sincronizar"); + syncBtn.setStyle( + "-fx-background-color: " + color + "; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 6 14; -fx-font-size: 11; -fx-cursor: hand;" + ); + syncBtn.setOnAction(e -> handleSingleSync(type, syncBtn)); + + headerRow.getChildren().addAll(titleLabel, spacer, syncBtn); + + Label dirLabel = new Label(direction); + dirLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #9E9E9E;"); + + Label lastSyncLabel = new Label("Sin sincronizaciones previas"); + lastSyncLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #BDBDBD;"); + lastSyncInfoLabels.put(type, lastSyncLabel); + + card.getChildren().addAll(headerRow, dirLabel, lastSyncLabel); return card; } - private HBox createActionsBox() { - HBox box = new HBox(10); - box.setPadding(new Insets(15)); - box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); - box.setAlignment(Pos.CENTER_LEFT); - - Label label = new Label("Quick Actions:"); - label.setStyle("-fx-font-weight: bold;"); - - // Buttons - javafx.scene.control.Button refreshButton = new javafx.scene.control.Button("🔄 Refresh Now"); - refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;"); - refreshButton.setOnAction(e -> refreshDashboard()); - - box.getChildren().addAll(label, refreshButton); - - return box; - } - public void refreshDashboard() { - Thread thread = new Thread(() -> { + Thread t = new Thread(() -> { try { SyncServiceClient client = sessionManager.getSyncServiceClient(); - List> logs = client.getLogs(null); - - Platform.runLater(() -> updateDashboard(logs)); + List logs = client.getLogs(null); + Platform.runLater(() -> updateCards(logs)); } catch (Exception e) { - log.error("Error refreshing dashboard: {}", e.getMessage()); + log.error("Dashboard refresh failed: {}", e.getMessage()); } }); - thread.setDaemon(true); - thread.start(); + t.setDaemon(true); + t.start(); } - private void updateDashboard(List> logs) { - if (logs.isEmpty()) { - totalSyncsLabel.setText("0"); - successRateLabel.setText("0%"); - return; - } - - totalSyncsLabel.setText(String.valueOf(logs.size())); - - // Busca último sync de cada tipo - for (Map log : logs) { - String syncType = (String) log.get("syncType"); - Instant startedAt = Instant.parse((String) log.get("startedAt")); - String timeStr = formatInstant(startedAt); - - switch (syncType) { - case "PRODUCT_PUSH" -> lastProductSyncLabel.setText(timeStr); - case "STOCK_PUSH" -> lastStockSyncLabel.setText(timeStr); - case "ORDER_PULL" -> lastOrderSyncLabel.setText(timeStr); + private void updateCards(List logs) { + // Toma el más reciente de cada tipo (el primero encontrado en la lista, que viene newest-first) + Map latestPerType = new HashMap<>(); + for (SyncLogResponse entry : logs) { + if (entry.getSyncType() != null) { + latestPerType.putIfAbsent(entry.getSyncType(), entry); } } - // Success rate - long successCount = logs.stream() - .filter(l -> ((Number) l.get("itemsFailed")).longValue() == 0) - .count(); - long successRate = (successCount * 100) / logs.size(); - successRateLabel.setText(successRate + "%"); + latestPerType.forEach((type, entry) -> { + Label label = lastSyncInfoLabels.get(type); + if (label == null) return; + + String time = DateUtils.formatRelative(entry.getStartedAt()); + int processed = entry.getItemsProcessed() != null ? entry.getItemsProcessed() : 0; + int failed = entry.getItemsFailed() != null ? entry.getItemsFailed() : 0; + String text = time + " · " + processed + " procesados"; + if (failed > 0) text += ", " + failed + " fallidos"; + + label.setText(text); + label.setStyle("-fx-font-size: 11; -fx-text-fill: " + (failed > 0 ? "#C62828" : "#757575") + ";"); + }); + } + + private void handleSingleSync(String type, Button btn) { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmar sincronización"); + confirm.setHeaderText("¿Ejecutar sincronización de " + DateUtils.syncTypeLabel(type) + "?"); + confirm.setContentText(DateUtils.syncTypeDirection(type)); + if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return; + + btn.setDisable(true); + Thread t = new Thread(() -> { + try { + SyncServiceClient client = sessionManager.getSyncServiceClient(); + SyncTriggerResponse result = switch (type) { + case "PRODUCT_PUSH" -> client.triggerProductSync(); + case "STOCK_PUSH" -> client.triggerStockSync(); + case "ORDER_PULL" -> client.triggerOrderSync(); + default -> throw new IllegalArgumentException("Unknown type: " + type); + }; + Platform.runLater(() -> { + btn.setDisable(false); + if (result.getItemsFailed() == 0) { + AlertUtil.showInfo("Completado", + DateUtils.syncTypeLabel(type) + ": " + result.getItemsProcessed() + " procesados."); + } else { + AlertUtil.showWarn("Completado con errores", + result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos."); + } + refreshDashboard(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + btn.setDisable(false); + AlertUtil.showError("Error de sincronización", e.getMessage()); + }); + } + }); + t.setDaemon(true); + t.start(); + } + + private void handleSyncAll() { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Sincronizar todo"); + confirm.setHeaderText("Ejecutará productos, stock y pedidos en secuencia."); + confirm.setContentText("¿Continuar?"); + if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return; + + syncAllButton.setDisable(true); + syncAllStatusLabel.setVisible(true); + + Thread t = new Thread(() -> { + try { + SyncServiceClient client = sessionManager.getSyncServiceClient(); + + Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando productos (1/3)...")); + SyncTriggerResponse r1 = client.triggerProductSync(); + + Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando stock (2/3)...")); + SyncTriggerResponse r2 = client.triggerStockSync(); + + Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando pedidos (3/3)...")); + SyncTriggerResponse r3 = client.triggerOrderSync(); + + int totalProcessed = r1.getItemsProcessed() + r2.getItemsProcessed() + r3.getItemsProcessed(); + int totalFailed = r1.getItemsFailed() + r2.getItemsFailed() + r3.getItemsFailed(); + + Platform.runLater(() -> { + syncAllButton.setDisable(false); + syncAllStatusLabel.setVisible(false); + if (totalFailed == 0) { + AlertUtil.showInfo("Sincronización completada", + "Total procesados: " + totalProcessed); + } else { + AlertUtil.showWarn("Completado con errores", + "Procesados: " + totalProcessed + ", Fallidos: " + totalFailed); + } + refreshDashboard(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + syncAllButton.setDisable(false); + syncAllStatusLabel.setVisible(false); + AlertUtil.showError("Error en sincronización", e.getMessage()); + }); + } + }); + t.setDaemon(true); + t.start(); } private void startAutoRefresh() { - Timeline timeline = new Timeline( - new KeyFrame(Duration.seconds(10), e -> refreshDashboard()) - ); + Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(30), e -> refreshDashboard())); timeline.setCycleCount(Animation.INDEFINITE); timeline.play(); } - - private String formatInstant(Instant instant) { - LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - return ldt.format(formatter); - } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.java index 89ac883..5607b83 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.java @@ -2,221 +2,187 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; +import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse; +import com.teterialosjuanjos.tfg.javafxclient.ui.dialogs.LogDetailDialog; import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; +import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; +import javafx.scene.layout.*; import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.Map; /** - * Tab para logs de sincronización. + * Tab de historial de sincronizaciones. + * Click en una fila abre LogDetailDialog. + * Equivalente a LogsScreen de la app Android. */ @Slf4j public class LogsController { private final SessionManager sessionManager; - private TableView> logsTable; - private TextArea errorDetailsArea; + private TableView logsTable; public LogsController(SessionManager sessionManager) { this.sessionManager = sessionManager; } public VBox createView() { - VBox root = new VBox(15); + VBox root = new VBox(12); root.setPadding(new Insets(20)); - root.setStyle("-fx-background-color: #fafafa;"); + root.setStyle("-fx-background-color: #FAFAFA;"); - // Title - Label title = new Label("Synchronization Logs"); - title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); + // Header + HBox headerRow = new HBox(10); + headerRow.setAlignment(Pos.CENTER_LEFT); + VBox titleBox = new VBox(2); + Label title = new Label("Historial de sincronización"); + title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;"); + Label subtitle = new Label("Registros de cada ejecución"); + subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;"); + titleBox.getChildren().addAll(title, subtitle); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + Button refreshBtn = new Button("🔄 Actualizar"); + refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 14;"); + refreshBtn.setOnAction(e -> loadLogs(null)); + headerRow.getChildren().addAll(titleBox, spacer, refreshBtn); + + // Filter chips + HBox chipsBox = createFilterChips(); // Table - logsTable = createLogsTable(); - VBox.setVgrow(logsTable, javafx.scene.layout.Priority.ALWAYS); - logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> { - if (newVal != null) { - showLogDetails(newVal); + logsTable = createTable(); + VBox.setVgrow(logsTable, Priority.ALWAYS); + + // Click row → open detail dialog + logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, log) -> { + if (log != null) { + new LogDetailDialog(log).show(); + Platform.runLater(() -> logsTable.getSelectionModel().clearSelection()); } }); - // Details panel - VBox detailsPanel = createDetailsPanel(); - - // Filter & Actions - HBox filterBox = createFilterBox(); - - root.getChildren().addAll(title, filterBox, logsTable, detailsPanel); - - // Load initial data + root.getChildren().addAll(headerRow, chipsBox, logsTable); loadLogs(null); - return root; } - private TableView> createLogsTable() { - TableView> table = new TableView<>(); - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); - table.setStyle("-fx-font-size: 11;"); - table.setPrefHeight(300); + private HBox createFilterChips() { + ToggleGroup group = new ToggleGroup(); - // ID column - TableColumn, String> idCol = new TableColumn<>("ID"); - idCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("id"); - return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value)); - }); - idCol.setPrefWidth(50); + ToggleButton allBtn = chip("Todos", null, group, true); + ToggleButton productsBtn = chip("Productos", "PRODUCT_PUSH", group, false); + ToggleButton stockBtn = chip("Stock", "STOCK_PUSH", group, false); + ToggleButton ordersBtn = chip("Pedidos", "ORDER_PULL", group, false); - // Type column - TableColumn, String> typeCol = new TableColumn<>("Type"); - typeCol.setCellValueFactory(cf -> { - String value = (String) cf.getValue().get("syncType"); - return new javafx.beans.property.SimpleObjectProperty<>(value); - }); - typeCol.setPrefWidth(120); - - // Started At column - TableColumn, String> startedCol = new TableColumn<>("Started At"); - startedCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("startedAt"); - return new javafx.beans.property.SimpleObjectProperty<>( - value != null ? String.valueOf(value).substring(0, 19) : "—" - ); - }); - startedCol.setPrefWidth(150); - - // Finished At column - TableColumn, String> finishedCol = new TableColumn<>("Finished At"); - finishedCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("finishedAt"); - return new javafx.beans.property.SimpleObjectProperty<>( - value != null ? String.valueOf(value).substring(0, 19) : "—" - ); - }); - finishedCol.setPrefWidth(150); - - // Processed column - TableColumn, String> processedCol = new TableColumn<>("Processed"); - processedCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("itemsProcessed"); - return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value)); - }); - processedCol.setPrefWidth(80); - - // Failed column - TableColumn, String> failedCol = new TableColumn<>("Failed"); - failedCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("itemsFailed"); - return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value)); - }); - failedCol.setPrefWidth(80); - failedCol.setCellFactory(col -> new TableCell, String>() { - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null || "0".equals(item)) { - setText(item); - setStyle(""); - } else { - setText(item); - setStyle("-fx-text-fill: #c62828; -fx-font-weight: bold;"); - } - } + group.selectedToggleProperty().addListener((obs, old, newToggle) -> { + if (newToggle == null) { allBtn.setSelected(true); return; } + loadLogs((String) newToggle.getUserData()); }); - table.getColumns().addAll(idCol, typeCol, startedCol, finishedCol, processedCol, failedCol); - - return table; - } - - private VBox createDetailsPanel() { - VBox panel = new VBox(10); - panel.setPadding(new Insets(10)); - panel.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); - - Label label = new Label("Error Details (click a row to see details):"); - label.setStyle("-fx-font-weight: bold;"); - - errorDetailsArea = new TextArea(); - errorDetailsArea.setEditable(false); - errorDetailsArea.setWrapText(true); - errorDetailsArea.setPrefHeight(120); - errorDetailsArea.setStyle("-fx-font-size: 10; -fx-control-inner-background: #f5f5f5;"); - - panel.getChildren().addAll(label, errorDetailsArea); - - return panel; - } - - private HBox createFilterBox() { - HBox box = new HBox(10); - box.setPadding(new Insets(10)); - box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); + HBox box = new HBox(8, allBtn, productsBtn, stockBtn, ordersBtn); box.setAlignment(Pos.CENTER_LEFT); - - Label filterLabel = new Label("Filter by type:"); - - ComboBox typeCombo = new ComboBox<>(); - typeCombo.setItems(FXCollections.observableArrayList( - "All", "PRODUCT_PUSH", "STOCK_PUSH", "ORDER_PULL" - )); - typeCombo.setValue("All"); - typeCombo.setPrefWidth(140); - - typeCombo.setOnAction(e -> { - String selected = typeCombo.getValue(); - loadLogs("All".equals(selected) ? null : selected); - }); - - Region spacer = new Region(); - HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); - - Button refreshButton = new Button("🔄 Refresh"); - refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;"); - refreshButton.setOnAction(e -> loadLogs(null)); - - box.getChildren().addAll(filterLabel, typeCombo, spacer, refreshButton); - return box; } - private void loadLogs(String type) { - Thread thread = new Thread(() -> { - try { - SyncServiceClient client = sessionManager.getSyncServiceClient(); - List> logs = client.getLogs(type); + private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) { + ToggleButton btn = new ToggleButton(text); + btn.setToggleGroup(group); + btn.setSelected(selected); + btn.setUserData(value); + btn.getStyleClass().add("filter-chip"); + return btn; + } - ObservableList> items = FXCollections.observableArrayList(logs); - Platform.runLater(() -> logsTable.setItems(items)); - } catch (Exception e) { - log.error("Error loading logs: {}", e.getMessage()); - Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load logs: " + e.getMessage())); + private TableView createTable() { + TableView table = new TableView<>(); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); + table.setStyle("-fx-cursor: hand;"); + + // Status icon column + TableColumn iconCol = new TableColumn<>(""); + iconCol.setCellValueFactory(cf -> new javafx.beans.property.SimpleObjectProperty<>(cf.getValue())); + iconCol.setPrefWidth(36); + iconCol.setCellFactory(c -> new TableCell<>() { + @Override + protected void updateItem(SyncLogResponse item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { setGraphic(null); return; } + int failed = item.getItemsFailed() != null ? item.getItemsFailed() : 0; + boolean inP = item.getFinishedAt() == null; + String icon = inP ? "⏱" : (failed > 0 ? "✗" : "✓"); + String color = inP ? "#1565C0" : (failed > 0 ? "#C62828" : "#2E7D32"); + Label lbl = new Label(icon); + lbl.setStyle("-fx-font-size: 14; -fx-text-fill: " + color + ";"); + setGraphic(lbl); + setText(null); } }); - thread.setDaemon(true); - thread.start(); + + // Type + TableColumn typeCol = new TableColumn<>("Tipo"); + typeCol.setCellValueFactory(cf -> new SimpleStringProperty(DateUtils.syncTypeLabel(cf.getValue().getSyncType()))); + typeCol.setPrefWidth(100); + + // Started (relative) + TableColumn startedCol = new TableColumn<>("Inicio"); + startedCol.setCellValueFactory(cf -> new SimpleStringProperty(DateUtils.formatRelative(cf.getValue().getStartedAt()))); + startedCol.setPrefWidth(130); + + // Duration + TableColumn durationCol = new TableColumn<>("Duración"); + durationCol.setCellValueFactory(cf -> + new SimpleStringProperty(DateUtils.formatDuration(cf.getValue().getStartedAt(), cf.getValue().getFinishedAt()))); + durationCol.setPrefWidth(80); + + // Processed + TableColumn processedCol = new TableColumn<>("Procesados"); + processedCol.setCellValueFactory(cf -> { + Integer v = cf.getValue().getItemsProcessed(); + return new SimpleStringProperty(v != null ? String.valueOf(v) : "—"); + }); + processedCol.setPrefWidth(90); + + // Failed (red if > 0) + TableColumn failedCol = new TableColumn<>("Fallidos"); + failedCol.setCellValueFactory(cf -> { + Integer v = cf.getValue().getItemsFailed(); + return new SimpleStringProperty(v != null ? String.valueOf(v) : "—"); + }); + failedCol.setPrefWidth(80); + failedCol.setCellFactory(c -> new TableCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { setText(null); setStyle(""); return; } + setText(item); + boolean isFailed = !"—".equals(item) && !"0".equals(item); + setStyle(isFailed ? "-fx-text-fill: #C62828; -fx-font-weight: bold;" : ""); + } + }); + + table.getColumns().addAll(iconCol, typeCol, startedCol, durationCol, processedCol, failedCol); + return table; } - private void showLogDetails(Map log) { - String errorDetails = (String) log.get("errorDetails"); - if (errorDetails != null && !errorDetails.isEmpty()) { - errorDetailsArea.setText(errorDetails); - } else { - int processed = ((Number) log.get("itemsProcessed")).intValue(); - int failed = ((Number) log.get("itemsFailed")).intValue(); - errorDetailsArea.setText("✓ Sync completed successfully\n" + - "Processed: " + processed + "\n" + - "Failed: " + failed); - } + private void loadLogs(String type) { + Thread t = new Thread(() -> { + try { + SyncServiceClient client = sessionManager.getSyncServiceClient(); + List logs = client.getLogs(type); + Platform.runLater(() -> logsTable.setItems(FXCollections.observableArrayList(logs))); + } catch (Exception e) { + log.error("Error loading logs: {}", e.getMessage()); + Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los logs: " + e.getMessage())); + } + }); + t.setDaemon(true); + t.start(); } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.java index 1f17b0d..2140312 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.java @@ -1,30 +1,32 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; +import com.teterialosjuanjos.tfg.javafxclient.model.OrderMappingResponse; import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; +import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse; import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; +import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; +import javafx.scene.layout.*; import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.Map; /** - * Tab para pedidos: ver mapeos de PrestaShop → Dolibarr y disparar sync. + * Tab de mapeos de pedidos: PrestaShop → Dolibarr. + * Equivalente a MappingsScreen (tab Pedidos) de la app Android. */ @Slf4j public class OrdersController { private final SessionManager sessionManager; private final DashboardController dashboardController; - private TableView> ordersTable; + private TableView ordersTable; public OrdersController(SessionManager sessionManager, DashboardController dashboardController) { this.sessionManager = sessionManager; @@ -32,194 +34,145 @@ public class OrdersController { } public VBox createView() { - VBox root = new VBox(15); + VBox root = new VBox(12); root.setPadding(new Insets(20)); - root.setStyle("-fx-background-color: #fafafa;"); + root.setStyle("-fx-background-color: #FAFAFA;"); - // Title - Label title = new Label("Order Mappings (PrestaShop → Dolibarr)"); - title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); + Label title = new Label("Mappings"); + title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;"); + Label subtitle = new Label("Pedidos — PrestaShop → Dolibarr"); + subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;"); - // Info box - HBox infoBox = createInfoBox(); + // Info banner + HBox infoBanner = new HBox(8); + infoBanner.getStyleClass().add("banner-info"); + Label infoLabel = new Label("ℹ Los pedidos de PrestaShop se importan en Dolibarr como comandas y facturas."); + infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565C0;"); + infoLabel.setWrapText(true); + infoBanner.getChildren().add(infoLabel); - // Table - ordersTable = createOrdersTable(); - VBox.setVgrow(ordersTable, javafx.scene.layout.Priority.ALWAYS); + ordersTable = createTable(); + VBox.setVgrow(ordersTable, Priority.ALWAYS); - // Action buttons HBox actionBox = createActionBox(); - root.getChildren().addAll(title, infoBox, ordersTable, actionBox); - - // Load initial data + root.getChildren().addAll(title, subtitle, infoBanner, ordersTable, actionBox); loadOrders(); - return root; } - private HBox createInfoBox() { - HBox box = new HBox(10); - box.setPadding(new Insets(10)); - box.setStyle("-fx-background-color: #e3f2fd; -fx-border-color: #1976d2; -fx-border-width: 1;"); - box.setAlignment(Pos.CENTER_LEFT); - - Label infoLabel = new Label( - "ℹ Orders from PrestaShop are automatically imported into Dolibarr as sales orders and invoices." - ); - infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565c0;"); - infoLabel.setWrapText(true); - - box.getChildren().add(infoLabel); - - return box; - } - - private TableView> createOrdersTable() { - TableView> table = new TableView<>(); + private TableView createTable() { + TableView table = new TableView<>(); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); - table.setStyle("-fx-font-size: 11;"); - // PrestaShop Order ID column - TableColumn, String> psOrderIdCol = new TableColumn<>("PS Order ID"); - psOrderIdCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("prestashopOrderId"); - return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value)); - }); - psOrderIdCol.setPrefWidth(120); + TableColumn psIdCol = new TableColumn<>("Pedido PS #"); + psIdCol.setCellValueFactory(cf -> + new SimpleStringProperty(String.valueOf(cf.getValue().getPrestashopOrderId()))); + psIdCol.setPrefWidth(100); - // Dolibarr Order ID column - TableColumn, String> dolOrderIdCol = new TableColumn<>("Dolibarr Order ID"); - dolOrderIdCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("dolibarrOrderId"); - return new javafx.beans.property.SimpleObjectProperty<>( - value != null ? String.valueOf(value) : "—" - ); + TableColumn dolOrderCol = new TableColumn<>("Comanda Dolibarr"); + dolOrderCol.setCellValueFactory(cf -> { + Integer v = cf.getValue().getDolibarrOrderId(); + return new SimpleStringProperty(v != null ? String.valueOf(v) : "—"); }); - dolOrderIdCol.setPrefWidth(120); + dolOrderCol.setPrefWidth(130); - // Dolibarr Invoice ID column - TableColumn, String> dolInvoiceIdCol = new TableColumn<>("Invoice ID"); - dolInvoiceIdCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("dolibarrInvoiceId"); - return new javafx.beans.property.SimpleObjectProperty<>( - value != null ? String.valueOf(value) : "—" - ); + TableColumn dolInvoiceCol = new TableColumn<>("Factura"); + dolInvoiceCol.setCellValueFactory(cf -> { + Integer v = cf.getValue().getDolibarrInvoiceId(); + return new SimpleStringProperty(v != null ? String.valueOf(v) : "—"); }); - dolInvoiceIdCol.setPrefWidth(100); + dolInvoiceCol.setPrefWidth(80); - // Status column - TableColumn, String> statusCol = new TableColumn<>("Status"); - statusCol.setCellValueFactory(cf -> { - String value = (String) cf.getValue().get("status"); - return new javafx.beans.property.SimpleObjectProperty<>(value); - }); - statusCol.setPrefWidth(100); - statusCol.setCellFactory(col -> new TableCell, String>() { + // Status chip column + TableColumn statusCol = new TableColumn<>("Estado"); + statusCol.setCellValueFactory(cf -> new SimpleStringProperty(cf.getValue().getStatus())); + statusCol.setPrefWidth(110); + statusCol.setCellFactory(c -> new TableCell<>() { @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - setStyle(""); - } else { - setText(item); - if ("INVOICED".equals(item)) { - setStyle("-fx-text-fill: #2e7d32; -fx-font-weight: bold;"); - } - } + protected void updateItem(String status, boolean empty) { + super.updateItem(status, empty); + if (empty || status == null) { setGraphic(null); return; } + Label chip = new Label(DateUtils.orderStatusLabel(status)); + chip.getStyleClass().add(DateUtils.orderStatusCssClass(status)); + setGraphic(chip); + setText(null); } }); - // Imported At column - TableColumn, String> importedAtCol = new TableColumn<>("Imported At"); - importedAtCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("importedAt"); - return new javafx.beans.property.SimpleObjectProperty<>( - value != null ? String.valueOf(value).substring(0, 19) : "—" - ); - }); - importedAtCol.setPrefWidth(150); - - table.getColumns().addAll(psOrderIdCol, dolOrderIdCol, dolInvoiceIdCol, statusCol, importedAtCol); + TableColumn importedCol = new TableColumn<>("Importado"); + importedCol.setCellValueFactory(cf -> + new SimpleStringProperty(DateUtils.formatDateTime(cf.getValue().getImportedAt()))); + importedCol.setPrefWidth(150); + table.getColumns().addAll(psIdCol, dolOrderCol, dolInvoiceCol, statusCol, importedCol); return table; } private HBox createActionBox() { HBox box = new HBox(10); - box.setPadding(new Insets(15)); - box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); - box.setAlignment(Pos.CENTER_LEFT); + box.setPadding(new Insets(10, 0, 0, 0)); + box.setAlignment(Pos.CENTER_RIGHT); - Label label = new Label("Actions:"); - label.setStyle("-fx-font-weight: bold;"); + Button refreshBtn = new Button("🔄 Actualizar"); + refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 16;"); + refreshBtn.setOnAction(e -> loadOrders()); - Button syncButton = new Button("🔄 Sync Orders from PrestaShop"); - syncButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #f57c00; -fx-text-fill: white;"); - syncButton.setOnAction(e -> triggerOrderSync()); - - Button refreshButton = new Button("🔄 Refresh"); - refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;"); - refreshButton.setOnAction(e -> loadOrders()); - - box.getChildren().addAll(label, syncButton, refreshButton); + Button syncBtn = new Button("Sincronizar pedidos"); + syncBtn.setStyle("-fx-background-color: #E65100; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 10 20; -fx-font-weight: bold; -fx-cursor: hand;"); + syncBtn.setOnAction(e -> triggerOrderSync(syncBtn)); + box.getChildren().addAll(refreshBtn, syncBtn); return box; } private void loadOrders() { - Thread thread = new Thread(() -> { + Thread t = new Thread(() -> { try { SyncServiceClient client = sessionManager.getSyncServiceClient(); - List> mappings = client.getOrderMappings(); - - ObservableList> items = FXCollections.observableArrayList(mappings); - Platform.runLater(() -> ordersTable.setItems(items)); + List orders = client.getOrderMappings(); + Platform.runLater(() -> ordersTable.setItems(FXCollections.observableArrayList(orders))); } catch (Exception e) { log.error("Error loading orders: {}", e.getMessage()); - Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load orders: " + e.getMessage())); + Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los pedidos: " + e.getMessage())); } }); - thread.setDaemon(true); - thread.start(); + t.setDaemon(true); + t.start(); } - private void triggerOrderSync() { - Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION); - confirmAlert.setTitle("Confirm Sync"); - confirmAlert.setHeaderText("Sync Orders from PrestaShop to Dolibarr?"); - confirmAlert.setContentText("This will import new orders from PrestaShop as sales orders and invoices in Dolibarr."); + private void triggerOrderSync(Button btn) { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmar sincronización"); + confirm.setHeaderText("¿Sincronizar pedidos de PrestaShop a Dolibarr?"); + confirm.setContentText("Se importarán los pedidos nuevos como comandas y facturas en Dolibarr."); + if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return; - if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { - Thread thread = new Thread(() -> { - try { - SyncServiceClient client = sessionManager.getSyncServiceClient(); - Map result = client.triggerOrderSync(); - - Platform.runLater(() -> { - int processed = ((Number) result.get("processed")).intValue(); - int failed = ((Number) result.get("failed")).intValue(); - - if (failed == 0) { - AlertUtil.showInfo("Success", - "Orders synced: " + processed + " orders imported"); - } else { - AlertUtil.showWarn("Partial Success", - "Processed: " + processed + ", Failed: " + failed); - } - - loadOrders(); - dashboardController.refreshDashboard(); - }); - } catch (Exception e) { - Platform.runLater(() -> - AlertUtil.showError("Sync Error", e.getMessage()) - ); - } - }); - thread.setDaemon(true); - thread.start(); - } + btn.setDisable(true); + Thread t = new Thread(() -> { + try { + SyncServiceClient client = sessionManager.getSyncServiceClient(); + SyncTriggerResponse result = client.triggerOrderSync(); + Platform.runLater(() -> { + btn.setDisable(false); + if (result.getItemsFailed() == 0) { + AlertUtil.showInfo("Completado", "Pedidos importados: " + result.getItemsProcessed()); + } else { + AlertUtil.showWarn("Completado con errores", + result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos."); + } + loadOrders(); + dashboardController.refreshDashboard(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + btn.setDisable(false); + AlertUtil.showError("Error de sincronización", e.getMessage()); + }); + } + }); + t.setDaemon(true); + t.start(); } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.java index 02df466..c3d107c 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.java @@ -1,30 +1,37 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; +import com.teterialosjuanjos.tfg.javafxclient.model.ProductMappingResponse; import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; +import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse; import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; +import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; +import javafx.scene.layout.*; import lombok.extern.slf4j.Slf4j; -import java.util.*; +import java.util.List; +import java.util.stream.Collectors; /** - * Tab para productos: ver mapeos y disparar sync. + * Tab de mapeos de productos con búsqueda por SKU y filtros de estado. + * Equivalente a MappingsScreen (tab Productos) de la app Android. */ @Slf4j public class ProductsController { private final SessionManager sessionManager; private final DashboardController dashboardController; - private TableView> productsTable; + + private TableView productsTable; + private List allProducts = List.of(); + private String activeStatusFilter = null; public ProductsController(SessionManager sessionManager, DashboardController dashboardController) { this.sessionManager = sessionManager; @@ -32,240 +39,197 @@ public class ProductsController { } public VBox createView() { - VBox root = new VBox(15); + VBox root = new VBox(12); root.setPadding(new Insets(20)); - root.setStyle("-fx-background-color: #fafafa;"); + root.setStyle("-fx-background-color: #FAFAFA;"); - // Title - Label title = new Label("Product Mappings"); - title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); + // Header + Label title = new Label("Mappings"); + title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;"); + Label subtitle = new Label("Productos — Dolibarr ↔ PrestaShop"); + subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;"); - // Filter & Actions - HBox filterBox = createFilterBox(); + // Search bar + TextField searchField = new TextField(); + searchField.setPromptText("Buscar por SKU..."); + searchField.setMaxWidth(Double.MAX_VALUE); + searchField.textProperty().addListener((obs, old, val) -> applyFilters(searchField, val)); + + // Filter chips + HBox chipsBox = createFilterChips(searchField); // Table - productsTable = createProductsTable(); - VBox.setVgrow(productsTable, javafx.scene.layout.Priority.ALWAYS); + productsTable = createTable(); + VBox.setVgrow(productsTable, Priority.ALWAYS); // Action buttons HBox actionBox = createActionBox(); - root.getChildren().addAll(title, filterBox, productsTable, actionBox); - - // Load initial data - loadProducts(null); + root.getChildren().addAll(title, subtitle, searchField, chipsBox, productsTable, actionBox); + loadProducts(); return root; } - private HBox createFilterBox() { - HBox box = new HBox(10); - box.setPadding(new Insets(10)); - box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); - box.setAlignment(Pos.CENTER_LEFT); + private HBox createFilterChips(TextField searchField) { + ToggleGroup group = new ToggleGroup(); - Label filterLabel = new Label("Filter by status:"); + ToggleButton allBtn = chip("Todos", null, group, true); + ToggleButton syncedBtn = chip("Sincronizados", "SYNCED", group, false); + ToggleButton pendingBtn = chip("Pendientes", "PENDING", group, false); + ToggleButton errorBtn = chip("Con error", "ERROR", group, false); - ComboBox statusCombo = new ComboBox<>(); - statusCombo.setItems(FXCollections.observableArrayList( - "All", "SYNCED", "PENDING", "ERROR" - )); - statusCombo.setValue("All"); - statusCombo.setPrefWidth(120); - - statusCombo.setOnAction(e -> { - String selected = statusCombo.getValue(); - loadProducts("All".equals(selected) ? null : selected); + group.selectedToggleProperty().addListener((obs, old, newToggle) -> { + if (newToggle == null) { allBtn.setSelected(true); return; } + activeStatusFilter = (String) newToggle.getUserData(); + loadProducts(); }); - Region spacer = new Region(); - HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); - - Button refreshButton = new Button("🔄 Refresh"); - refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;"); - refreshButton.setOnAction(e -> loadProducts(null)); - - box.getChildren().addAll(filterLabel, statusCombo, spacer, refreshButton); - + HBox box = new HBox(8, allBtn, syncedBtn, pendingBtn, errorBtn); + box.setAlignment(Pos.CENTER_LEFT); return box; } - private TableView> createProductsTable() { - TableView> table = new TableView<>(); + private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) { + ToggleButton btn = new ToggleButton(text); + btn.setToggleGroup(group); + btn.setSelected(selected); + btn.setUserData(value); + btn.getStyleClass().add("filter-chip"); + return btn; + } + + private TableView createTable() { + TableView table = new TableView<>(); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); - table.setStyle("-fx-font-size: 11;"); - // SKU column - TableColumn, String> skuCol = new TableColumn<>("SKU"); - skuCol.setCellValueFactory(cf -> { - String value = (String) cf.getValue().get("sku"); - return new javafx.beans.property.SimpleObjectProperty<>(value); - }); - skuCol.setPrefWidth(120); + TableColumn skuCol = col("SKU", p -> p.getSku()); + skuCol.setPrefWidth(140); - // Dolibarr ID column - TableColumn, String> dolIdCol = new TableColumn<>("Dolibarr ID"); - dolIdCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("dolibarrId"); - return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value)); - }); + TableColumn dolIdCol = col("Dolibarr ID", + p -> p.getDolibarrId() != null ? String.valueOf(p.getDolibarrId()) : "—"); dolIdCol.setPrefWidth(100); - // PrestaShop ID column - TableColumn, String> psIdCol = new TableColumn<>("PrestaShop ID"); - psIdCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("prestashopId"); - return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value)); - }); - psIdCol.setPrefWidth(100); + TableColumn psIdCol = col("PrestaShop ID", + p -> p.getPrestashopId() != null ? String.valueOf(p.getPrestashopId()) : "—"); + psIdCol.setPrefWidth(110); - // Status column - TableColumn, String> statusCol = new TableColumn<>("Status"); - statusCol.setCellValueFactory(cf -> { - String value = (String) cf.getValue().get("syncStatus"); - return new javafx.beans.property.SimpleObjectProperty<>(value); - }); - statusCol.setPrefWidth(80); - statusCol.setCellFactory(col -> new TableCell, String>() { + TableColumn lastSyncCol = col("Última sync", + p -> DateUtils.formatRelative(p.getLastSyncedAt())); + lastSyncCol.setPrefWidth(130); + + // Status chip column + TableColumn statusCol = new TableColumn<>("Estado"); + statusCol.setCellValueFactory(cf -> new SimpleStringProperty(cf.getValue().getSyncStatus())); + statusCol.setPrefWidth(120); + statusCol.setCellFactory(c -> new TableCell<>() { @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - setStyle(""); - } else { - setText(item); - switch (item) { - case "SYNCED" -> setStyle("-fx-text-fill: #2e7d32; -fx-font-weight: bold;"); - case "PENDING" -> setStyle("-fx-text-fill: #f57f17; -fx-font-weight: bold;"); - case "ERROR" -> setStyle("-fx-text-fill: #c62828; -fx-font-weight: bold;"); - } - } + protected void updateItem(String status, boolean empty) { + super.updateItem(status, empty); + if (empty || status == null) { setGraphic(null); return; } + Label chip = new Label(DateUtils.productStatusLabel(status)); + chip.getStyleClass().add(DateUtils.productStatusCssClass(status)); + setGraphic(chip); + setText(null); } }); - // Error message column - TableColumn, String> errorCol = new TableColumn<>("Error"); - errorCol.setCellValueFactory(cf -> { - Object value = cf.getValue().get("errorMessage"); - String text = value != null ? (String) value : ""; - return new javafx.beans.property.SimpleObjectProperty<>(text); - }); + TableColumn errorCol = col("Error", + p -> p.getErrorMessage() != null ? p.getErrorMessage() : ""); errorCol.setPrefWidth(200); - table.getColumns().addAll(skuCol, dolIdCol, psIdCol, statusCol, errorCol); - + table.getColumns().addAll(skuCol, dolIdCol, psIdCol, lastSyncCol, statusCol, errorCol); return table; } + private TableColumn col( + String header, java.util.function.Function extractor) { + TableColumn col = new TableColumn<>(header); + col.setCellValueFactory(cf -> new SimpleStringProperty(extractor.apply(cf.getValue()))); + return col; + } + private HBox createActionBox() { HBox box = new HBox(10); - box.setPadding(new Insets(15)); - box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); + box.setPadding(new Insets(10, 0, 0, 0)); box.setAlignment(Pos.CENTER_RIGHT); - Button syncButton = new Button("🔄 Sync Products from Dolibarr"); - syncButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #1976d2; -fx-text-fill: white;"); - syncButton.setOnAction(e -> triggerProductSync()); + Button refreshBtn = new Button("🔄 Actualizar"); + refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 16;"); + refreshBtn.setOnAction(e -> loadProducts()); - Button stockButton = new Button("📊 Sync Stock"); - stockButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #388e3c; -fx-text-fill: white;"); - stockButton.setOnAction(e -> triggerStockSync()); + Button syncProductsBtn = new Button("Sincronizar productos"); + syncProductsBtn.setStyle("-fx-background-color: #1565C0; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 10 20; -fx-font-weight: bold; -fx-cursor: hand;"); + syncProductsBtn.setOnAction(e -> triggerSync("PRODUCT_PUSH", syncProductsBtn)); - box.getChildren().addAll(syncButton, stockButton); + Button syncStockBtn = new Button("Sincronizar stock"); + syncStockBtn.setStyle("-fx-background-color: #2E7D32; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 10 20; -fx-cursor: hand;"); + syncStockBtn.setOnAction(e -> triggerSync("STOCK_PUSH", syncStockBtn)); + box.getChildren().addAll(refreshBtn, syncProductsBtn, syncStockBtn); return box; } - private void loadProducts(String status) { - Thread thread = new Thread(() -> { + private void loadProducts() { + Thread t = new Thread(() -> { try { SyncServiceClient client = sessionManager.getSyncServiceClient(); - List> mappings = client.getProductMappings(status); - - ObservableList> items = FXCollections.observableArrayList(mappings); - Platform.runLater(() -> productsTable.setItems(items)); + allProducts = client.getProductMappings(activeStatusFilter); + Platform.runLater(() -> productsTable.setItems(FXCollections.observableArrayList(allProducts))); } catch (Exception e) { log.error("Error loading products: {}", e.getMessage()); - Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load products: " + e.getMessage())); + Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los productos: " + e.getMessage())); } }); - thread.setDaemon(true); - thread.start(); + t.setDaemon(true); + t.start(); } - private void triggerProductSync() { - Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION); - confirmAlert.setTitle("Confirm Sync"); - confirmAlert.setHeaderText("Sync Products from Dolibarr to PrestaShop?"); - confirmAlert.setContentText("This will push new/modified products from Dolibarr to PrestaShop."); - - if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { - Thread thread = new Thread(() -> { - try { - SyncServiceClient client = sessionManager.getSyncServiceClient(); - Map result = client.triggerProductSync(); - - Platform.runLater(() -> { - int processed = ((Number) result.get("processed")).intValue(); - int failed = ((Number) result.get("failed")).intValue(); - - if (failed == 0) { - AlertUtil.showInfo("Success", - "Sync completed: " + processed + " products processed"); - } else { - AlertUtil.showWarn("Partial Success", - "Processed: " + processed + ", Failed: " + failed); - } - - loadProducts(null); - dashboardController.refreshDashboard(); - }); - } catch (Exception e) { - Platform.runLater(() -> - AlertUtil.showError("Sync Error", e.getMessage()) - ); - } - }); - thread.setDaemon(true); - thread.start(); - } + private void applyFilters(TextField searchField, String searchText) { + String query = searchText == null ? "" : searchText.toLowerCase(); + List filtered = allProducts.stream() + .filter(p -> p.getSku() != null && p.getSku().toLowerCase().contains(query)) + .collect(Collectors.toList()); + productsTable.setItems(FXCollections.observableArrayList(filtered)); } - private void triggerStockSync() { - Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION); - confirmAlert.setTitle("Confirm Sync"); - confirmAlert.setHeaderText("Sync Stock from Dolibarr to PrestaShop?"); - confirmAlert.setContentText("This will update stock levels in PrestaShop from Dolibarr."); + private void triggerSync(String type, Button btn) { + String label = type.equals("PRODUCT_PUSH") ? "Productos" : "Stock"; + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmar sincronización"); + confirm.setHeaderText("¿Sincronizar " + label + "?"); + confirm.setContentText(DateUtils.syncTypeDirection(type)); + if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return; - if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { - Thread thread = new Thread(() -> { - try { - SyncServiceClient client = sessionManager.getSyncServiceClient(); - Map result = client.triggerStockSync(); + btn.setDisable(true); + Thread t = new Thread(() -> { + try { + SyncServiceClient client = sessionManager.getSyncServiceClient(); + SyncTriggerResponse result = type.equals("PRODUCT_PUSH") + ? client.triggerProductSync() + : client.triggerStockSync(); - Platform.runLater(() -> { - int processed = ((Number) result.get("processed")).intValue(); - int failed = ((Number) result.get("failed")).intValue(); - - if (failed == 0) { - AlertUtil.showInfo("Success", - "Stock sync completed: " + processed + " items updated"); - } else { - AlertUtil.showWarn("Partial Success", - "Processed: " + processed + ", Failed: " + failed); - } - - dashboardController.refreshDashboard(); - }); - } catch (Exception e) { - Platform.runLater(() -> - AlertUtil.showError("Sync Error", e.getMessage()) - ); - } - }); - thread.setDaemon(true); - thread.start(); - } + Platform.runLater(() -> { + btn.setDisable(false); + if (result.getItemsFailed() == 0) { + AlertUtil.showInfo("Completado", label + ": " + result.getItemsProcessed() + " procesados."); + } else { + AlertUtil.showWarn("Completado con errores", + result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos."); + } + loadProducts(); + dashboardController.refreshDashboard(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + btn.setDisable(false); + AlertUtil.showError("Error de sincronización", e.getMessage()); + }); + } + }); + t.setDaemon(true); + t.start(); } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/SettingsController.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/SettingsController.java new file mode 100644 index 0000000..e6fcdca --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/SettingsController.java @@ -0,0 +1,168 @@ +package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; + +import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; +import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import lombok.extern.slf4j.Slf4j; + +/** + * Tab de ajustes de conexión. + * Permite cambiar la URL del servidor, usuario y contraseña en caliente. + * Equivalente a SettingsScreen de la app Android. + */ +@Slf4j +public class SettingsController { + + private final SessionManager sessionManager; + + public SettingsController(SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + public ScrollPane createView() { + VBox root = new VBox(20); + root.setPadding(new Insets(24)); + root.setStyle("-fx-background-color: #FAFAFA;"); + root.setMaxWidth(640); + + Label title = new Label("Ajustes de conexión"); + title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;"); + Label subtitle = new Label("Configura el servidor sync-service"); + subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;"); + + // Form card + VBox formCard = new VBox(14); + formCard.setPadding(new Insets(20)); + formCard.setStyle( + "-fx-background-color: white;" + + "-fx-background-radius: 12;" + + "-fx-border-radius: 12;" + + "-fx-border-color: #E0E0E0;" + + "-fx-border-width: 1;" + ); + + // URL + Label urlLabel = new Label("URL del servidor"); + urlLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + TextField urlField = new TextField(sessionManager.getBaseUrl()); + urlField.setPromptText("https://tu-servidor.up.railway.app/"); + Label urlHelp = new Label("Incluye la barra final /"); + urlHelp.setStyle("-fx-font-size: 10; -fx-text-fill: #9E9E9E;"); + + // Username + Label userLabel = new Label("Usuario"); + userLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + TextField userField = new TextField( + sessionManager.getUsername() != null ? sessionManager.getUsername() : "admin"); + userField.setPromptText("admin"); + + // Password with visibility toggle + Label passLabel = new Label("Contraseña"); + passLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + PasswordField passField = new PasswordField(); + passField.setText(sessionManager.getPassword() != null ? sessionManager.getPassword() : ""); + TextField passVisible = new TextField(); + passVisible.setManaged(false); + passVisible.setVisible(false); + Button toggleBtn = new Button("👁"); + toggleBtn.setStyle("-fx-background-color: transparent; -fx-cursor: hand; -fx-padding: 4; -fx-border-width: 0;"); + toggleBtn.setOnAction(e -> { + boolean show = !passVisible.isVisible(); + if (show) passVisible.setText(passField.getText()); + else passField.setText(passVisible.getText()); + passVisible.setVisible(show); + passVisible.setManaged(show); + passField.setVisible(!show); + passField.setManaged(!show); + }); + HBox.setHgrow(passField, Priority.ALWAYS); + HBox.setHgrow(passVisible, Priority.ALWAYS); + HBox passBox = new HBox(6, passField, passVisible, toggleBtn); + passBox.setAlignment(Pos.CENTER_LEFT); + + formCard.getChildren().addAll( + urlLabel, urlField, urlHelp, + new Separator(), + userLabel, userField, + passLabel, passBox + ); + + // Test result label + Label testResultLabel = new Label(); + testResultLabel.setWrapText(true); + testResultLabel.setVisible(false); + + // Buttons + HBox btnBox = new HBox(12); + btnBox.setAlignment(Pos.CENTER_RIGHT); + + Button testBtn = new Button("Probar conexión"); + testBtn.setStyle("-fx-background-color: #546E7A; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 10 20; -fx-cursor: hand;"); + + Button saveBtn = new Button("Guardar"); + saveBtn.setStyle("-fx-background-color: #1565C0; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 10 20; -fx-font-weight: bold; -fx-cursor: hand;"); + + testBtn.setOnAction(e -> handleTest(urlField, userField, passField, passVisible, testBtn, testResultLabel)); + saveBtn.setOnAction(e -> handleSave(urlField, userField, passField, passVisible, testResultLabel)); + + btnBox.getChildren().addAll(testBtn, saveBtn); + + root.getChildren().addAll(title, subtitle, formCard, testResultLabel, btnBox); + + ScrollPane scrollPane = new ScrollPane(root); + scrollPane.setFitToWidth(true); + return scrollPane; + } + + private void handleTest(TextField urlField, TextField userField, + PasswordField passField, TextField passVisible, + Button testBtn, Label resultLabel) { + String url = urlField.getText().trim(); + String user = userField.getText().trim(); + String pass = passField.isVisible() ? passField.getText() : passVisible.getText(); + + testBtn.setDisable(true); + resultLabel.setText("Probando conexión..."); + resultLabel.setStyle("-fx-text-fill: #546E7A;"); + resultLabel.setVisible(true); + + Thread t = new Thread(() -> { + boolean ok = sessionManager.testConnection(url, user, pass); + Platform.runLater(() -> { + testBtn.setDisable(false); + if (ok) { + resultLabel.setText("✓ Conexión exitosa"); + resultLabel.setStyle("-fx-text-fill: #2E7D32; -fx-font-weight: bold;"); + } else { + resultLabel.setText("✗ No se pudo conectar. Verifica la URL y credenciales."); + resultLabel.setStyle("-fx-text-fill: #C62828; -fx-font-weight: bold;"); + } + }); + }); + t.setDaemon(true); + t.start(); + } + + private void handleSave(TextField urlField, TextField userField, + PasswordField passField, TextField passVisible, + Label resultLabel) { + String url = urlField.getText().trim(); + String user = userField.getText().trim(); + String pass = passField.isVisible() ? passField.getText() : passVisible.getText(); + + if (url.isEmpty() || user.isEmpty() || pass.isEmpty()) { + AlertUtil.showError("Validación", "Todos los campos son obligatorios."); + return; + } + + sessionManager.updateSettings(url, user, pass); + resultLabel.setVisible(false); + AlertUtil.showInfo("Ajustes guardados", "Los ajustes de conexión se han guardado correctamente."); + } +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/dialogs/LogDetailDialog.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/dialogs/LogDetailDialog.java new file mode 100644 index 0000000..f9e4fff --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/dialogs/LogDetailDialog.java @@ -0,0 +1,154 @@ +package com.teterialosjuanjos.tfg.javafxclient.ui.dialogs; + +import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse; +import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.Modality; +import javafx.stage.Stage; + +/** + * Diálogo modal con el detalle de un registro de sincronización. + * Equivalente a LogDetailScreen de la app Android. + */ +public class LogDetailDialog { + + private final SyncLogResponse log; + + public LogDetailDialog(SyncLogResponse log) { + this.log = log; + } + + public void show() { + Stage stage = new Stage(); + stage.initModality(Modality.APPLICATION_MODAL); + stage.setTitle("Detalle del log #" + log.getId()); + stage.setWidth(580); + stage.setResizable(true); + + VBox root = new VBox(16); + root.setPadding(new Insets(24)); + root.setStyle("-fx-background-color: #FAFAFA;"); + + root.getChildren().add(createStatusBanner()); + root.getChildren().add(new Separator()); + root.getChildren().add(createInfoSection()); + root.getChildren().add(createResultSection()); + + if (log.getErrorDetails() != null && !log.getErrorDetails().isBlank()) { + root.getChildren().add(createErrorSection()); + } + + Button closeBtn = new Button("Cerrar"); + closeBtn.setStyle("-fx-background-color: #1565C0; -fx-text-fill: white; " + + "-fx-background-radius: 8; -fx-padding: 8 24; -fx-font-weight: bold; -fx-cursor: hand;"); + closeBtn.setOnAction(e -> stage.close()); + + HBox btnBox = new HBox(closeBtn); + btnBox.setAlignment(Pos.CENTER_RIGHT); + root.getChildren().add(btnBox); + + ScrollPane scrollPane = new ScrollPane(root); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-background-color: #FAFAFA;"); + + Scene scene = new Scene(scrollPane); + try { + scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm()); + } catch (Exception ignored) {} + + stage.setScene(scene); + stage.show(); + } + + private VBox createStatusBanner() { + int failed = log.getItemsFailed() != null ? log.getItemsFailed() : 0; + boolean inProgress = log.getFinishedAt() == null; + + String icon, statusText, cssClass; + if (inProgress) { + icon = "⏱"; statusText = "En curso"; cssClass = "banner-info"; + } else if (failed > 0) { + icon = "✗"; statusText = failed + " elemento(s) fallaron"; cssClass = "banner-error"; + } else { + icon = "✓"; statusText = "Completado sin errores"; cssClass = "banner-success"; + } + + Label statusLabel = new Label(icon + " " + statusText); + statusLabel.setStyle("-fx-font-size: 14; -fx-font-weight: bold;"); + + Label subtitleLabel = new Label( + DateUtils.syncTypeLabel(log.getSyncType()) + ": " + + DateUtils.syncTypeDirection(log.getSyncType()) + ); + subtitleLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #757575;"); + + VBox banner = new VBox(4, statusLabel, subtitleLabel); + banner.getStyleClass().add(cssClass); + return banner; + } + + private VBox createInfoSection() { + Label sectionTitle = new Label("Información"); + sectionTitle.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + + GridPane grid = new GridPane(); + grid.setHgap(20); + grid.setVgap(8); + + addRow(grid, 0, "Tipo de sync", DateUtils.syncTypeLabel(log.getSyncType())); + addRow(grid, 1, "Inicio", DateUtils.formatDateTime(log.getStartedAt())); + addRow(grid, 2, "Fin", log.getFinishedAt() != null ? DateUtils.formatDateTime(log.getFinishedAt()) : "En curso"); + addRow(grid, 3, "Duración", DateUtils.formatDuration(log.getStartedAt(), log.getFinishedAt())); + + return new VBox(8, sectionTitle, grid); + } + + private VBox createResultSection() { + Label sectionTitle = new Label("Resultado"); + sectionTitle.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + + int processed = log.getItemsProcessed() != null ? log.getItemsProcessed() : 0; + int failed = log.getItemsFailed() != null ? log.getItemsFailed() : 0; + + GridPane grid = new GridPane(); + grid.setHgap(20); + grid.setVgap(8); + + addRow(grid, 0, "Procesados correctamente", String.valueOf(processed)); + + Label failKey = new Label("Fallidos:"); + failKey.setStyle("-fx-text-fill: #757575; -fx-font-size: 11;"); + Label failVal = new Label(String.valueOf(failed)); + failVal.setStyle("-fx-font-weight: bold; -fx-font-size: 11;" + + (failed > 0 ? " -fx-text-fill: #C62828;" : "")); + grid.add(failKey, 0, 1); + grid.add(failVal, 1, 1); + + return new VBox(8, sectionTitle, grid); + } + + private VBox createErrorSection() { + Label sectionTitle = new Label("Detalle de errores"); + sectionTitle.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: #C62828;"); + + TextArea errorArea = new TextArea(log.getErrorDetails()); + errorArea.setEditable(false); + errorArea.setWrapText(true); + errorArea.setPrefHeight(160); + + return new VBox(8, sectionTitle, errorArea); + } + + private void addRow(GridPane grid, int row, String key, String value) { + Label k = new Label(key + ":"); + k.setStyle("-fx-text-fill: #757575; -fx-font-size: 11;"); + Label v = new Label(value); + v.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); + grid.add(k, 0, row); + grid.add(v, 1, row); + } +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.java index d8b6ebe..bb93af3 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.java @@ -7,17 +7,20 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; /** * Pantalla de login para autenticación contra el sync-service. + * Pre-rellena los campos con los ajustes guardados en SettingsStore. */ @Slf4j public class LoginStage { + private static final String PROD_URL = "https://proyectointermodular-production-a9c3.up.railway.app"; + private static final String DEV_URL = "http://localhost:8080"; + private final SessionManager sessionManager; private final Stage primaryStage; private Stage loginWindow; @@ -29,9 +32,9 @@ public class LoginStage { public void show() { loginWindow = new Stage(); - loginWindow.setTitle("TFG Sync Service — Login"); - loginWindow.setWidth(400); - loginWindow.setHeight(500); + loginWindow.setTitle("Sync Manager — Iniciar sesión"); + loginWindow.setWidth(420); + loginWindow.setHeight(520); loginWindow.setResizable(false); VBox root = createLoginForm(); @@ -45,129 +48,121 @@ public class LoginStage { } private VBox createLoginForm() { - VBox form = new VBox(15); - form.setPadding(new Insets(40)); - form.setAlignment(Pos.TOP_CENTER); - form.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 0;"); + VBox form = new VBox(0); + form.setStyle("-fx-background-color: #F5F5F5;"); + + // Top color bar + HBox topBar = new HBox(); + topBar.setMinHeight(6); + topBar.setStyle("-fx-background-color: #1565C0;"); + + // Card + VBox card = new VBox(14); + card.setPadding(new Insets(36, 40, 36, 40)); + card.setAlignment(Pos.TOP_CENTER); + card.setStyle( + "-fx-background-color: white;" + + "-fx-background-radius: 0 0 12 12;" + + "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.08), 8, 0, 0, 3);" + ); + VBox.setVgrow(card, Priority.ALWAYS); // Title - Label title = new Label("Sync Service Client"); - title.setStyle("-fx-font-size: 24; -fx-font-weight: bold; -fx-text-fill: #1976d2;"); + Label title = new Label("Sync Manager"); + title.setStyle("-fx-font-size: 26; -fx-font-weight: bold; -fx-text-fill: #1565C0;"); - Label subtitle = new Label("Dolibarr ↔ PrestaShop Integration"); - subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #666666;"); + Label subtitle = new Label("Dolibarr · PrestaShop"); + subtitle.setStyle("-fx-font-size: 13; -fx-text-fill: #757575;"); - // URL input - Label urlLabel = new Label("Base URL:"); - urlLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); - TextField urlField = new TextField(); - urlField.setText("https://proyectointermodular-production-a9c3.up.railway.app"); + VBox titleBox = new VBox(4, title, subtitle); + titleBox.setAlignment(Pos.CENTER); + + Separator sep = new Separator(); + sep.setPadding(new Insets(4, 0, 4, 0)); + + // URL field + Label urlLabel = new Label("URL del servidor"); + urlLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + TextField urlField = new TextField(sessionManager.getBaseUrl()); + urlField.setPromptText("https://tu-servidor.up.railway.app"); urlField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); - // Username input - Label usernameLabel = new Label("Username:"); - usernameLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); - TextField usernameField = new TextField(); - usernameField.setPromptText("admin"); - usernameField.setText("admin"); - usernameField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); + // Username + Label userLabel = new Label("Usuario"); + userLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + TextField userField = new TextField( + sessionManager.getUsername() != null ? sessionManager.getUsername() : ""); + userField.setPromptText("admin"); + userField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); - // Password input - Label passwordLabel = new Label("Password:"); - passwordLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); - PasswordField passwordField = new PasswordField(); - passwordField.setPromptText("••••••••"); - passwordField.setText("admin123"); - passwordField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); - - // Login button - Button loginButton = new Button("Login"); - loginButton.setStyle( - "-fx-font-size: 12; " + - "-fx-padding: 10; " + - "-fx-background-color: #1976d2; " + - "-fx-text-fill: white; " + - "-fx-font-weight: bold; " + - "-fx-cursor: hand;" - ); - loginButton.setPrefWidth(Double.MAX_VALUE); - - loginButton.setOnAction(e -> handleLogin( - urlField.getText(), - usernameField.getText(), - passwordField.getText() - )); + // Password + Label passLabel = new Label("Contraseña"); + passLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;"); + PasswordField passField = new PasswordField(); + passField.setText(sessionManager.getPassword() != null ? sessionManager.getPassword() : ""); + passField.setPromptText("••••••••"); + passField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); // Dev mode toggle - CheckBox devModeCheckBox = new CheckBox("Use local dev server (http://localhost:8080)"); - devModeCheckBox.setStyle("-fx-font-size: 11;"); - devModeCheckBox.selectedProperty().addListener((obs, old, newVal) -> { - if (newVal) { - urlField.setText("http://localhost:8080"); - } else { - urlField.setText("https://proyectointermodular-production-a9c3.up.railway.app"); - } - }); + CheckBox devCheckBox = new CheckBox("Usar servidor local (localhost:8080)"); + devCheckBox.setStyle("-fx-font-size: 11; -fx-text-fill: #546E7A;"); + devCheckBox.selectedProperty().addListener((obs, old, val) -> + urlField.setText(val ? DEV_URL : PROD_URL)); - // Separator - Separator separator = new Separator(); + // Login button + Button loginButton = new Button("Conectar"); + loginButton.getStyleClass().add("btn-primary"); + loginButton.setMaxWidth(Double.MAX_VALUE); + loginButton.setPrefHeight(42); + loginButton.setStyle(loginButton.getStyle() + + "-fx-font-size: 13; -fx-font-weight: bold; -fx-cursor: hand;"); + loginButton.setOnAction(e -> handleLogin(urlField, userField, passField)); - // Info label - Label infoLabel = new Label("Tip: Default credentials are admin / admin123"); - infoLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #999999; -fx-font-style: italic;"); + // Allow Enter key in password field + passField.setOnAction(e -> handleLogin(urlField, userField, passField)); - form.getChildren().addAll( - title, - subtitle, - new Separator(), - urlLabel, - urlField, - usernameLabel, - usernameField, - passwordLabel, - passwordField, - separator, - devModeCheckBox, - new Region(), // spacer - loginButton, - infoLabel + card.getChildren().addAll( + titleBox, sep, + urlLabel, urlField, + userLabel, userField, + passLabel, passField, + devCheckBox, + new Region(), + loginButton ); + form.getChildren().addAll(topBar, card); return form; } - private void handleLogin(String baseUrl, String username, String password) { - if (baseUrl.isEmpty() || username.isEmpty() || password.isEmpty()) { - AlertUtil.showError("Validation Error", "All fields are required"); + private void handleLogin(TextField urlField, TextField userField, PasswordField passField) { + String url = urlField.getText().trim(); + String user = userField.getText().trim(); + String pass = passField.getText(); + + if (url.isEmpty() || user.isEmpty() || pass.isEmpty()) { + AlertUtil.showError("Validación", "Todos los campos son obligatorios."); return; } - // Ejecuta en background para no freezear UI - Thread loginThread = new Thread(() -> { + Thread t = new Thread(() -> { try { - boolean success = sessionManager.authenticate(baseUrl, username, password); + boolean success = sessionManager.authenticate(url, user, pass); Platform.runLater(() -> { if (success) { - AlertUtil.showInfo("Success", "Authentication successful!"); loginWindow.close(); - openMainStage(); + new MainStage(sessionManager, primaryStage).show(); } else { - AlertUtil.showError("Login Failed", - "Could not authenticate. Check credentials and URL."); + AlertUtil.showError("Autenticación fallida", + "No se pudo conectar. Verifica URL y credenciales."); } }); } catch (Exception ex) { - Platform.runLater(() -> { - AlertUtil.showError("Connection Error", ex.getMessage()); - }); + log.error("Login error: {}", ex.getMessage()); + Platform.runLater(() -> AlertUtil.showError("Error de conexión", ex.getMessage())); } }); - loginThread.setDaemon(true); - loginThread.start(); + t.setDaemon(true); + t.start(); } - - private void openMainStage() { - MainStage mainStage = new MainStage(sessionManager, primaryStage); - mainStage.show(); - } -} \ No newline at end of file +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.java index 4897e33..47487ad 100644 --- a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.java +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.java @@ -6,27 +6,18 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; /** - * Pantalla principal con navegación por tabs y dashboard. + * Ventana principal con navegación por tabs: Panel, Productos, Pedidos, Logs, Ajustes. */ @Slf4j public class MainStage { private final SessionManager sessionManager; private final Stage primaryStage; - private Stage mainWindow; - - private DashboardController dashboardController; - private ProductsController productsController; - private OrdersController ordersController; - private LogsController logsController; public MainStage(SessionManager sessionManager, Stage primaryStage) { this.sessionManager = sessionManager; @@ -34,12 +25,14 @@ public class MainStage { } public void show() { - mainWindow = new Stage(); - mainWindow.setTitle("TFG Sync Service — Dashboard"); + Stage mainWindow = new Stage(); + mainWindow.setTitle("Sync Manager"); mainWindow.setWidth(1200); - mainWindow.setHeight(700); + mainWindow.setHeight(720); + mainWindow.setMinWidth(900); + mainWindow.setMinHeight(600); - BorderPane root = createMainLayout(); + BorderPane root = createMainLayout(mainWindow); Scene scene = new Scene(root); scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm()); @@ -48,92 +41,83 @@ public class MainStage { mainWindow.show(); } - private BorderPane createMainLayout() { + private BorderPane createMainLayout(Stage window) { BorderPane root = new BorderPane(); - - // Top: Header con usuario y logout - root.setTop(createHeader()); - - // Center: Tabs + root.setTop(createHeader(window)); root.setCenter(createTabPane()); - return root; } - private VBox createHeader() { - VBox header = new VBox(); - header.setPadding(new Insets(15)); - header.setStyle("-fx-background-color: #1976d2; -fx-border-color: #1565c0; -fx-border-width: 0 0 2 0;"); + private HBox createHeader(Stage window) { + HBox header = new HBox(12); + header.setPadding(new Insets(12, 20, 12, 20)); + header.setAlignment(Pos.CENTER_LEFT); + header.setStyle("-fx-background-color: #1565C0;"); - HBox topBar = new HBox(10); - topBar.setAlignment(Pos.CENTER_LEFT); + Label appName = new Label("Sync Manager"); + appName.setStyle("-fx-font-size: 17; -fx-font-weight: bold; -fx-text-fill: white;"); - Label title = new Label("Sync Service Control Panel"); - title.setStyle("-fx-font-size: 18; -fx-font-weight: bold; -fx-text-fill: white;"); + Label sep = new Label("·"); + sep.setStyle("-fx-text-fill: rgba(255,255,255,0.5); -fx-font-size: 14;"); + + Label connectedLabel = new Label("✓ " + sessionManager.getBaseUrl()); + connectedLabel.setStyle("-fx-font-size: 11; -fx-text-fill: rgba(255,255,255,0.75);"); Region spacer = new Region(); - HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); + HBox.setHgrow(spacer, Priority.ALWAYS); - Label userLabel = new Label("User: " + sessionManager.getUsername()); - userLabel.setStyle("-fx-font-size: 12; -fx-text-fill: white;"); + Label userChip = new Label(sessionManager.getUsername()); + userChip.setStyle( + "-fx-font-size: 11; -fx-text-fill: white;" + + "-fx-background-color: rgba(255,255,255,0.15);" + + "-fx-background-radius: 12; -fx-padding: 4 10;" + ); - Button logoutButton = new Button("Logout"); - logoutButton.setStyle("-fx-font-size: 11; -fx-padding: 5;"); - logoutButton.setOnAction(e -> handleLogout()); - - topBar.getChildren().addAll(title, spacer, userLabel, logoutButton); - - HBox statusBar = new HBox(20); - statusBar.setPadding(new Insets(10, 15, 10, 15)); - statusBar.setStyle("-fx-background-color: #f5f5f5;"); - - Label statusLabel = new Label("✓ Connected to: " + sessionManager.getBaseUrl()); - statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #2e7d32;"); - - statusBar.getChildren().add(statusLabel); - - header.getChildren().addAll(topBar, statusBar); + Button logoutBtn = new Button("Salir"); + logoutBtn.setStyle( + "-fx-background-color: rgba(255,255,255,0.15); -fx-text-fill: white;" + + "-fx-background-radius: 8; -fx-padding: 5 12; -fx-cursor: hand; -fx-font-size: 11;" + ); + logoutBtn.setOnAction(e -> handleLogout(window)); + header.getChildren().addAll(appName, sep, connectedLabel, spacer, userChip, logoutBtn); return header; } private TabPane createTabPane() { - TabPane tabPane = new TabPane(); + DashboardController dashboardController = new DashboardController(sessionManager); + ProductsController productsController = new ProductsController(sessionManager, dashboardController); + OrdersController ordersController = new OrdersController(sessionManager, dashboardController); + LogsController logsController = new LogsController(sessionManager); + SettingsController settingsController = new SettingsController(sessionManager); + + Tab dashboardTab = tab("📊 Panel", dashboardController.createView()); + Tab productsTab = tab("📦 Productos", productsController.createView()); + Tab ordersTab = tab("🛒 Pedidos", ordersController.createView()); + Tab logsTab = tab("📋 Historial", logsController.createView()); + Tab settingsTab = tab("⚙ Ajustes", settingsController.createView()); + + TabPane tabPane = new TabPane(dashboardTab, productsTab, ordersTab, logsTab, settingsTab); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.setStyle("-fx-font-size: 12;"); - - // Dashboard tab - dashboardController = new DashboardController(sessionManager); - Tab dashboardTab = new Tab("📊 Dashboard", dashboardController.createView()); - dashboardTab.setStyle("-fx-padding: 10;"); - - // Products tab - productsController = new ProductsController(sessionManager, dashboardController); - Tab productsTab = new Tab("📦 Products", productsController.createView()); - - // Orders tab - ordersController = new OrdersController(sessionManager, dashboardController); - Tab ordersTab = new Tab("🛒 Orders", ordersController.createView()); - - // Logs tab - logsController = new LogsController(sessionManager); - Tab logsTab = new Tab("📋 Logs", logsController.createView()); - - tabPane.getTabs().addAll(dashboardTab, productsTab, ordersTab, logsTab); - return tabPane; } - private void handleLogout() { - Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle("Logout"); - alert.setHeaderText("Are you sure?"); - alert.setContentText("You will be logged out and returned to the login screen."); - - if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { - sessionManager.logout(); - mainWindow.close(); - new LoginStage(sessionManager, primaryStage).show(); - } + private Tab tab(String label, javafx.scene.Node content) { + Tab t = new Tab(label, content); + t.setStyle("-fx-padding: 8 12;"); + return t; } -} \ No newline at end of file + + private void handleLogout(Stage window) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Cerrar sesión"); + alert.setHeaderText("¿Cerrar sesión?"); + alert.setContentText("Volverás a la pantalla de inicio de sesión."); + if (alert.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return; + + sessionManager.logout(); + window.close(); + new LoginStage(sessionManager, primaryStage).show(); + } +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/DateUtils.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/DateUtils.java new file mode 100644 index 0000000..83c3cee --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/DateUtils.java @@ -0,0 +1,105 @@ +package com.teterialosjuanjos.tfg.javafxclient.util; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** Utilidades de formateo de fechas y etiquetas de dominio. */ +public class DateUtils { + + private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); + + public static String formatRelative(String isoInstant) { + if (isoInstant == null || isoInstant.isBlank()) return "—"; + try { + long seconds = Instant.now().getEpochSecond() - Instant.parse(isoInstant).getEpochSecond(); + if (seconds < 60) return "hace " + seconds + "s"; + if (seconds < 3600) return "hace " + (seconds / 60) + " min"; + if (seconds < 86400) return "hace " + (seconds / 3600) + " h"; + return "hace " + (seconds / 86400) + " días"; + } catch (Exception e) { + return isoInstant.length() >= 19 ? isoInstant.substring(0, 19) : isoInstant; + } + } + + public static String formatDateTime(String isoInstant) { + if (isoInstant == null || isoInstant.isBlank()) return "—"; + try { + LocalDateTime ldt = LocalDateTime.ofInstant(Instant.parse(isoInstant), ZoneId.systemDefault()); + return ldt.format(DT_FMT); + } catch (Exception e) { + return isoInstant.length() >= 19 ? isoInstant.substring(0, 19) : isoInstant; + } + } + + public static String formatDuration(String start, String end) { + if (start == null || end == null) return "—"; + try { + long seconds = Instant.parse(end).getEpochSecond() - Instant.parse(start).getEpochSecond(); + if (seconds < 60) return seconds + "s"; + return (seconds / 60) + "m " + (seconds % 60) + "s"; + } catch (Exception e) { + return "—"; + } + } + + public static String syncTypeLabel(String syncType) { + if (syncType == null) return "—"; + return switch (syncType) { + case "PRODUCT_PUSH" -> "Productos"; + case "STOCK_PUSH" -> "Stock"; + case "ORDER_PULL" -> "Pedidos"; + default -> syncType; + }; + } + + public static String syncTypeDirection(String syncType) { + if (syncType == null) return ""; + return switch (syncType) { + case "PRODUCT_PUSH", "STOCK_PUSH" -> "Dolibarr → PrestaShop"; + case "ORDER_PULL" -> "PrestaShop → Dolibarr"; + default -> ""; + }; + } + + public static String productStatusLabel(String status) { + if (status == null) return "—"; + return switch (status) { + case "SYNCED" -> "Sincronizado"; + case "PENDING" -> "Pendiente"; + case "ERROR" -> "Error"; + default -> status; + }; + } + + public static String orderStatusLabel(String status) { + if (status == null) return "—"; + return switch (status) { + case "IMPORTED" -> "Importado"; + case "INVOICED" -> "Facturado"; + case "ERROR" -> "Error"; + default -> status; + }; + } + + public static String productStatusCssClass(String status) { + if (status == null) return ""; + return switch (status) { + case "SYNCED" -> "chip-synced"; + case "PENDING" -> "chip-pending"; + case "ERROR" -> "chip-error-status"; + default -> ""; + }; + } + + public static String orderStatusCssClass(String status) { + if (status == null) return ""; + return switch (status) { + case "IMPORTED" -> "chip-imported"; + case "INVOICED" -> "chip-invoiced"; + case "ERROR" -> "chip-error-status"; + default -> ""; + }; + } +} diff --git a/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/SettingsStore.java b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/SettingsStore.java new file mode 100644 index 0000000..51475ac --- /dev/null +++ b/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/SettingsStore.java @@ -0,0 +1,37 @@ +package com.teterialosjuanjos.tfg.javafxclient.util; + +import com.teterialosjuanjos.tfg.javafxclient.model.AppSettings; + +import java.util.prefs.Preferences; + +/** + * Persistencia de ajustes de conexión usando Java Preferences API. + * En Windows: HKCU\Software\JavaSoft\Prefs + * En Linux/Mac: ~/.java/.userPrefs + */ +public class SettingsStore { + + private static final Preferences prefs = Preferences.userNodeForPackage(SettingsStore.class); + + private static final String KEY_URL = "base_url"; + private static final String KEY_USERNAME = "username"; + private static final String KEY_PASSWORD = "password"; + + private static final String DEFAULT_URL = "https://proyectointermodular-production-a9c3.up.railway.app"; + private static final String DEFAULT_USER = "admin"; + private static final String DEFAULT_PASS = "admin123"; + + public static AppSettings load() { + return new AppSettings( + prefs.get(KEY_URL, DEFAULT_URL), + prefs.get(KEY_USERNAME, DEFAULT_USER), + prefs.get(KEY_PASSWORD, DEFAULT_PASS) + ); + } + + public static void save(AppSettings settings) { + prefs.put(KEY_URL, settings.getBaseUrl()); + prefs.put(KEY_USERNAME, settings.getUsername()); + prefs.put(KEY_PASSWORD, settings.getPassword()); + } +} diff --git a/javafx-client/src/main/resources/logback.xml b/javafx-client/src/main/resources/logback.xml index 78a646b..2b70989 100644 --- a/javafx-client/src/main/resources/logback.xml +++ b/javafx-client/src/main/resources/logback.xml @@ -10,5 +10,5 @@ - - \ No newline at end of file + + diff --git a/javafx-client/src/main/resources/styles.css b/javafx-client/src/main/resources/styles.css index 75a7960..8e5cd0e 100644 --- a/javafx-client/src/main/resources/styles.css +++ b/javafx-client/src/main/resources/styles.css @@ -1,93 +1,261 @@ -/* Material Design Inspired CSS for TFG Sync Service Client */ +/* TFG Sync Service Client — Material 3 (Android palette) */ * { - -fx-font-family: "Segoe UI", "Ubuntu", sans-serif; + -fx-font-family: "Segoe UI", "Roboto", sans-serif; } .root { - -fx-background-color: #fafafa; - -fx-font-size: 12; + -fx-background-color: #FAFAFA; + -fx-font-size: 12px; } -/* Buttons */ +/* === BUTTONS === */ .button { - -fx-padding: 8; - -fx-font-size: 12; - -fx-text-fill: #333; - -fx-background-color: #e0e0e0; - -fx-background-radius: 4; + -fx-padding: 8 16 8 16; + -fx-font-size: 12px; + -fx-background-radius: 8; + -fx-background-color: #E0E0E0; + -fx-text-fill: #212121; + -fx-cursor: hand; + -fx-border-width: 0; } +.button:hover { -fx-background-color: #BDBDBD; } +.button:pressed { -fx-background-color: #9E9E9E; } -.button:hover { - -fx-background-color: #d0d0d0; -} - -/* Text Fields */ -.text-field, -.password-field { - -fx-padding: 8; - -fx-border-color: #bdbdbd; - -fx-border-width: 0 0 1 0; - -fx-font-size: 12; -} - -/* Labels */ -.label { - -fx-text-fill: #333; -} - -/* Tables */ -.table-view { - -fx-background-color: white; - -fx-border-color: #e0e0e0; - -fx-font-size: 11; -} - -.table-view .column-header { - -fx-background-color: #f5f5f5; +.btn-primary { + -fx-background-color: #1565C0; + -fx-text-fill: white; -fx-font-weight: bold; } +.btn-primary:hover { -fx-background-color: #1976D2; } -.table-row-cell { - -fx-font-size: 11; - -fx-background-color: white; +.btn-success { + -fx-background-color: #2E7D32; + -fx-text-fill: white; +} +.btn-success:hover { -fx-background-color: #388E3C; } + +.btn-warning { + -fx-background-color: #E65100; + -fx-text-fill: white; +} +.btn-warning:hover { -fx-background-color: #EF6C00; } + +.btn-secondary { + -fx-background-color: #546E7A; + -fx-text-fill: white; +} +.btn-secondary:hover { -fx-background-color: #607D8B; } + +.btn-outline { + -fx-background-color: transparent; + -fx-border-color: #1565C0; + -fx-border-width: 1; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-text-fill: #1565C0; +} +.btn-outline:hover { -fx-background-color: #E3F2FD; } + +/* === FILTER CHIPS (ToggleButton) === */ +.filter-chip { + -fx-background-color: #ECEFF1; + -fx-text-fill: #546E7A; + -fx-background-radius: 20; + -fx-border-radius: 20; + -fx-padding: 6 16 6 16; + -fx-font-size: 11px; + -fx-cursor: hand; + -fx-border-width: 1; + -fx-border-color: #CFD8DC; +} +.filter-chip:selected { + -fx-background-color: #1565C0; + -fx-text-fill: white; + -fx-border-color: #1565C0; +} +.filter-chip:hover { + -fx-background-color: #CFD8DC; +} +.filter-chip:selected:hover { + -fx-background-color: #1976D2; } -/* ComboBox */ -.combo-box, -.combo-box-base { +/* === STATUS CHIPS (Label) === */ +.chip-synced { + -fx-background-color: #E8F5E9; + -fx-text-fill: #2E7D32; + -fx-background-radius: 16; + -fx-padding: 2 10 2 10; + -fx-font-weight: bold; + -fx-font-size: 10px; +} +.chip-pending { + -fx-background-color: #FFF3E0; + -fx-text-fill: #E65100; + -fx-background-radius: 16; + -fx-padding: 2 10 2 10; + -fx-font-weight: bold; + -fx-font-size: 10px; +} +.chip-error-status { + -fx-background-color: #FFEBEE; + -fx-text-fill: #C62828; + -fx-background-radius: 16; + -fx-padding: 2 10 2 10; + -fx-font-weight: bold; + -fx-font-size: 10px; +} +.chip-imported { + -fx-background-color: #ECEFF1; + -fx-text-fill: #546E7A; + -fx-background-radius: 16; + -fx-padding: 2 10 2 10; + -fx-font-weight: bold; + -fx-font-size: 10px; +} +.chip-invoiced { + -fx-background-color: #E3F2FD; + -fx-text-fill: #1565C0; + -fx-background-radius: 16; + -fx-padding: 2 10 2 10; + -fx-font-weight: bold; + -fx-font-size: 10px; +} + +/* === BANNERS === */ +.banner-error { + -fx-background-color: #FFEBEE; + -fx-border-color: #EF9A9A; + -fx-border-width: 0 0 0 4; + -fx-background-radius: 4; + -fx-padding: 12; +} +.banner-warning { + -fx-background-color: #FFF8E1; + -fx-border-color: #FFD54F; + -fx-border-width: 0 0 0 4; + -fx-background-radius: 4; + -fx-padding: 12; +} +.banner-info { + -fx-background-color: #E3F2FD; + -fx-border-color: #90CAF9; + -fx-border-width: 0 0 0 4; + -fx-background-radius: 4; + -fx-padding: 12; +} +.banner-success { + -fx-background-color: #E8F5E9; + -fx-border-color: #A5D6A7; + -fx-border-width: 0 0 0 4; + -fx-background-radius: 4; + -fx-padding: 12; +} + +/* === TEXT FIELDS === */ +.text-field, .password-field { + -fx-padding: 10; -fx-background-color: white; - -fx-border-color: #bdbdbd; + -fx-background-radius: 8; + -fx-border-color: #BDBDBD; + -fx-border-width: 1; + -fx-border-radius: 8; + -fx-font-size: 12px; +} +.text-field:focused, .password-field:focused { + -fx-border-color: #1565C0; + -fx-border-width: 2; + -fx-background-insets: 0; +} + +/* === LABELS === */ +.label { -fx-text-fill: #212121; } + +/* === TABLES === */ +.table-view { + -fx-background-color: white; + -fx-border-color: #E0E0E0; + -fx-border-width: 1; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-font-size: 11px; +} +.table-view .column-header { + -fx-background-color: #F5F5F5; + -fx-font-weight: bold; + -fx-padding: 10 8 10 8; + -fx-text-fill: #546E7A; + -fx-font-size: 11px; + -fx-border-color: transparent; +} +.table-view .column-header-background { + -fx-background-color: #F5F5F5; + -fx-border-color: #E0E0E0; + -fx-border-width: 0 0 1 0; +} +.table-row-cell { + -fx-background-color: white; + -fx-padding: 4 0 4 0; + -fx-border-color: transparent transparent #F5F5F5 transparent; + -fx-border-width: 0 0 1 0; +} +.table-row-cell:odd { -fx-background-color: #FAFAFA; } +.table-row-cell:hover { -fx-background-color: #E3F2FD; } +.table-row-cell:selected { -fx-background-color: #BBDEFB; } + +/* === TEXT AREA === */ +.text-area { + -fx-background-color: #F5F5F5; + -fx-text-fill: #333; + -fx-font-size: 11px; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-border-color: #E0E0E0; + -fx-border-width: 1; +} +.text-area .content { + -fx-background-color: #F5F5F5; + -fx-background-radius: 8; +} + +/* === COMBO BOX === */ +.combo-box { + -fx-background-color: white; + -fx-border-color: #BDBDBD; + -fx-border-radius: 8; + -fx-background-radius: 8; -fx-padding: 5; } -/* TextArea */ -.text-area { - -fx-background-color: #f5f5f5; - -fx-text-fill: #333; - -fx-font-size: 10; +/* === TAB PANE === */ +.tab-pane > .tab-header-area > .tab-header-background { + -fx-background-color: #F5F5F5; + -fx-border-color: #E0E0E0; + -fx-border-width: 0 0 1 0; } - -/* Separator */ -.separator { - -fx-text-fill: #e0e0e0; -} - -/* TabPane */ -.tab-pane { - -fx-padding: 0; -} - .tab { - -fx-padding: 10; + -fx-background-color: transparent; + -fx-padding: 10 20 10 20; + -fx-font-size: 12px; + -fx-cursor: hand; } - -.tab-header-background { - -fx-background-color: #f5f5f5; +.tab:selected { + -fx-background-color: white; + -fx-border-color: #1565C0; + -fx-border-width: 0 0 3 0; } +.tab .tab-label { -fx-text-fill: #757575; } +.tab:selected .tab-label { -fx-text-fill: #1565C0; -fx-font-weight: bold; } +.tab-pane { -fx-tab-min-height: 44px; } -/* CheckBox */ -.check-box { - -fx-font-size: 11; - -fx-text-fill: #333; -} \ No newline at end of file +/* === SCROLL PANE === */ +.scroll-pane { -fx-background-color: transparent; -fx-border-color: transparent; } +.scroll-pane .viewport { -fx-background-color: transparent; } +.scroll-pane > .scroll-bar { -fx-background-color: transparent; } + +/* === SEPARATOR === */ +.separator .line { -fx-border-color: #E0E0E0; -fx-border-width: 1; } + +/* === CHECK BOX === */ +.check-box { -fx-font-size: 11px; -fx-text-fill: #212121; } diff --git a/javafx-client/target/classes/applications.properties b/javafx-client/target/classes/applications.properties deleted file mode 100644 index e69de29..0000000 diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/JavaFxClientApplication.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/JavaFxClientApplication.class deleted file mode 100644 index 179aa6f..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/JavaFxClientApplication.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.class deleted file mode 100644 index a3d3a09..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.class deleted file mode 100644 index de418b8..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/MappingDto.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/MappingDto.class deleted file mode 100644 index 1a16dee..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/MappingDto.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/OrderDto.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/OrderDto.class deleted file mode 100644 index a572b01..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/OrderDto.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/ProductDto.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/ProductDto.class deleted file mode 100644 index d610484..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/ProductDto.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/SyncDto.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/SyncDto.class deleted file mode 100644 index f1e1b2e..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/model/dto/SyncDto.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.class deleted file mode 100644 index 8356925..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LoginController.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LoginController.class deleted file mode 100644 index 5a2fdc8..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LoginController.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController$1.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController$1.class deleted file mode 100644 index 141fd6d..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController$1.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.class deleted file mode 100644 index 46fb1a5..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController$1.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController$1.class deleted file mode 100644 index 43c0d7a..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController$1.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.class deleted file mode 100644 index 5492b9a..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController$1.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController$1.class deleted file mode 100644 index 19b2afb..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController$1.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.class deleted file mode 100644 index 8c15464..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.class deleted file mode 100644 index 242c578..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.class deleted file mode 100644 index 6cc3c3b..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/AlertUtil.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/AlertUtil.class deleted file mode 100644 index eca27ec..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/AlertUtil.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/FormatUtil.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/FormatUtil.class deleted file mode 100644 index e1b8754..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/FormatUtil.class and /dev/null differ diff --git a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/TaskExecutor.class b/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/TaskExecutor.class deleted file mode 100644 index 8b73383..0000000 Binary files a/javafx-client/target/classes/com/teterialosjuanjos/tfg/javafxclient/util/TaskExecutor.class and /dev/null differ diff --git a/javafx-client/target/classes/fxml/dashboard.fxml b/javafx-client/target/classes/fxml/dashboard.fxml deleted file mode 100644 index e69de29..0000000 diff --git a/javafx-client/target/classes/fxml/login.fxml b/javafx-client/target/classes/fxml/login.fxml deleted file mode 100644 index e69de29..0000000 diff --git a/javafx-client/target/classes/fxml/logs.fxml b/javafx-client/target/classes/fxml/logs.fxml deleted file mode 100644 index e69de29..0000000 diff --git a/javafx-client/target/classes/fxml/orders.fxml b/javafx-client/target/classes/fxml/orders.fxml deleted file mode 100644 index e69de29..0000000 diff --git a/javafx-client/target/classes/fxml/products.fxml b/javafx-client/target/classes/fxml/products.fxml deleted file mode 100644 index e69de29..0000000 diff --git a/javafx-client/target/classes/logback.xml b/javafx-client/target/classes/logback.xml deleted file mode 100644 index 78a646b..0000000 --- a/javafx-client/target/classes/logback.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - %d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n - - - - - - - - - \ No newline at end of file diff --git a/javafx-client/target/classes/styles.css b/javafx-client/target/classes/styles.css deleted file mode 100644 index 75a7960..0000000 --- a/javafx-client/target/classes/styles.css +++ /dev/null @@ -1,93 +0,0 @@ -/* Material Design Inspired CSS for TFG Sync Service Client */ - -* { - -fx-font-family: "Segoe UI", "Ubuntu", sans-serif; -} - -.root { - -fx-background-color: #fafafa; - -fx-font-size: 12; -} - -/* Buttons */ -.button { - -fx-padding: 8; - -fx-font-size: 12; - -fx-text-fill: #333; - -fx-background-color: #e0e0e0; - -fx-background-radius: 4; -} - -.button:hover { - -fx-background-color: #d0d0d0; -} - -/* Text Fields */ -.text-field, -.password-field { - -fx-padding: 8; - -fx-border-color: #bdbdbd; - -fx-border-width: 0 0 1 0; - -fx-font-size: 12; -} - -/* Labels */ -.label { - -fx-text-fill: #333; -} - -/* Tables */ -.table-view { - -fx-background-color: white; - -fx-border-color: #e0e0e0; - -fx-font-size: 11; -} - -.table-view .column-header { - -fx-background-color: #f5f5f5; - -fx-font-weight: bold; -} - -.table-row-cell { - -fx-font-size: 11; - -fx-background-color: white; -} - -/* ComboBox */ -.combo-box, -.combo-box-base { - -fx-background-color: white; - -fx-border-color: #bdbdbd; - -fx-padding: 5; -} - -/* TextArea */ -.text-area { - -fx-background-color: #f5f5f5; - -fx-text-fill: #333; - -fx-font-size: 10; -} - -/* Separator */ -.separator { - -fx-text-fill: #e0e0e0; -} - -/* TabPane */ -.tab-pane { - -fx-padding: 0; -} - -.tab { - -fx-padding: 10; -} - -.tab-header-background { - -fx-background-color: #f5f5f5; -} - -/* CheckBox */ -.check-box { - -fx-font-size: 11; - -fx-text-fill: #333; -} \ No newline at end of file diff --git a/javafx-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/javafx-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst deleted file mode 100644 index d8a1738..0000000 --- a/javafx-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ /dev/null @@ -1,20 +0,0 @@ -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController$1.class -com/teterialosjuanjos/tfg/javafxclient/util/AlertUtil.class -com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.class -com/teterialosjuanjos/tfg/javafxclient/model/dto/OrderDto.class -com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.class -com/teterialosjuanjos/tfg/javafxclient/util/TaskExecutor.class -com/teterialosjuanjos/tfg/javafxclient/JavaFxClientApplication.class -com/teterialosjuanjos/tfg/javafxclient/util/FormatUtil.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.class -com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController$1.class -com/teterialosjuanjos/tfg/javafxclient/model/dto/ProductDto.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LoginController.class -com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.class -com/teterialosjuanjos/tfg/javafxclient/model/dto/MappingDto.class -com/teterialosjuanjos/tfg/javafxclient/model/dto/SyncDto.class -com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController$1.class diff --git a/javafx-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/javafx-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst deleted file mode 100644 index ff74091..0000000 --- a/javafx-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ /dev/null @@ -1,17 +0,0 @@ -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/ProductsController.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/DashboardController.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/JavaFxClientApplication.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/api/SyncServiceClient.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/LoginStage.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/dto/OrderDto.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/FormatUtil.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/SessionManager.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/dto/SyncDto.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LoginController.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/OrdersController.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/dto/MappingDto.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/AlertUtil.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/util/TaskExecutor.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/model/dto/ProductDto.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/stages/MainStage.java -/home/mireyasp/Descargas/PROYECTO INTERMODULAR/ProyectoIntermodular/javafx-client/src/main/java/com/teterialosjuanjos/tfg/javafxclient/ui/controllers/LogsController.java