feat(javafx-client): redesign UI to match Android app and fix functional bugs

- Add typed DTOs (SyncLogResponse, ProductMappingResponse, OrderMappingResponse,
  SyncTriggerResponse, AppSettings) replacing fragile Map<String,Object> API
- Add SettingsStore (java.util.prefs) for persistent credentials across sessions
- Add DateUtils for relative time, duration, localized status labels
- Add LogDetailDialog (modal Stage) with status banner and error details
- Rewrite DashboardController: 3 sync cards, last log info, sync-all with progress,
  30s auto-refresh
- Rewrite ProductsController: SKU search, filter chips, status chips, typed table
- Rewrite OrdersController: typed table, status chips, Spanish labels
- Rewrite LogsController: filter chips, status icons, click row opens LogDetailDialog
- Rewrite SettingsController: password toggle, test connection, save to SettingsStore
- Rewrite LoginStage: pre-fill from SettingsStore, Material 3 card layout
- Rewrite MainStage: 5 tabs (Panel/Productos/Pedidos/Historial/Ajustes), blue header
- Rewrite SyncServiceClient to return typed DTOs via Jackson ObjectMapper
- Rewrite SessionManager to load/save settings via SettingsStore
- Rewrite styles.css: Material 3 palette matching Android app
- Fix logback.xml package name (javafx_client -> javafxclient)
- Fix dashboard showing oldest log instead of newest (putIfAbsent)
- Fix wrong API field names (processed/failed -> itemsProcessed/itemsFailed)
- Remove unused javafx-fxml, javafx-swing deps and broken maven-shade-plugin
- Add javafx-client/.gitignore and remove target/ from tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
luklpz 2026-05-17 20:12:44 +02:00
parent 0d32497abd
commit 7eb17a8da2
51 changed files with 1654 additions and 1222 deletions

7
javafx-client/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
target/
*.class
*.jar
*.log
.idea/
*.iml
.DS_Store

View File

@ -20,17 +20,12 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- JavaFX - TODO los módulos necesarios --> <!-- JavaFX -->
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version> <version>${javafx.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId> <artifactId>javafx-graphics</artifactId>
@ -41,13 +36,8 @@
<artifactId>javafx-base</artifactId> <artifactId>javafx-base</artifactId>
<version>${javafx.version}</version> <version>${javafx.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- JSON parsing (Jackson) --> <!-- JSON (Jackson) -->
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
@ -107,7 +97,7 @@
</configuration> </configuration>
</plugin> </plugin>
<!-- JavaFX Maven Plugin - ESENCIAL --> <!-- Ejecutar con: mvn javafx:run -->
<plugin> <plugin>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId> <artifactId>javafx-maven-plugin</artifactId>
@ -116,30 +106,6 @@
<mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass> <mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass>
</configuration> </configuration>
</plugin> </plugin>
<!-- JAR ejecutable para producción -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>fat</shadedClassifierName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@ -1,7 +1,8 @@
package com.teterialosjuanjos.tfg.javafxclient.api; package com.teterialosjuanjos.tfg.javafxclient.api;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; 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 lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
@ -10,12 +11,12 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; 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. * Cliente HTTP tipado para el sync-service.
* * Usa java.net.http.HttpClient (Java 11+) con Basic Auth.
* <p>Maneja autenticación Basic Auth para todos los endpoints.</p>
*/ */
@Slf4j @Slf4j
public class SyncServiceClient { public class SyncServiceClient {
@ -26,133 +27,81 @@ public class SyncServiceClient {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public SyncServiceClient(String baseUrl, String username, String password) { public SyncServiceClient(String baseUrl, String username, String password) {
this.baseUrl = baseUrl.replaceAll("/$", ""); // Remove trailing slash this.baseUrl = baseUrl.replaceAll("/$", "");
this.httpClient = HttpClient.newHttpClient(); this.httpClient = HttpClient.newHttpClient();
this.authHeader = buildBasicAuthHeader(username, password); this.authHeader = buildBasicAuthHeader(username, password);
this.objectMapper = new ObjectMapper(); this.objectMapper = new ObjectMapper()
this.objectMapper.registerModule(new JavaTimeModule()); .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
} }
// ── Health Check ──────────────────────────────────────────────────────── // ── Health ──────────────────────────────────────────────────────────────
public Map<String, Object> checkHealth() throws IOException, InterruptedException { /** Lanza IOException si el servidor no responde 200. */
String url = baseUrl + "/actuator/health"; public void checkHealth() throws IOException, InterruptedException {
return getJson(url, Map.class); HttpResponse<String> response = sendRequest("GET", baseUrl + "/actuator/health", null);
if (response.statusCode() != 200) {
throw new IOException("Health check failed: HTTP " + response.statusCode());
}
} }
// ── Logs ───────────────────────────────────────────────────────────────── // ── Logs ─────────────────────────────────────────────────────────────────
/** public List<SyncLogResponse> getLogs(String type) throws IOException, InterruptedException {
* Obtiene todos los logs de sincronización. String url = baseUrl + "/api/logs" + (type != null && !type.isEmpty() ? "?type=" + type : "");
* return getList(url, SyncLogResponse.class);
* @param type opcional, filtra por tipo (PRODUCT_PUSH, STOCK_PUSH, ORDER_PULL)
*/
public List<Map<String, Object>> getLogs(String type) throws IOException, InterruptedException {
String url = baseUrl + "/api/logs";
if (type != null && !type.isEmpty()) {
url += "?type=" + type;
} }
HttpResponse<String> response = sendRequest("GET", url, null); public SyncLogResponse getLog(Long id) throws IOException, InterruptedException {
if (response.statusCode() == 200) { return get(baseUrl + "/api/logs/" + id, SyncLogResponse.class);
return objectMapper.readValue(
response.body(),
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
);
}
throw new IOException("Failed to fetch logs: " + response.statusCode());
}
/**
* Obtiene un log específico por ID.
*/
public Map<String, Object> getLog(Long id) throws IOException, InterruptedException {
String url = baseUrl + "/api/logs/" + id;
return getJson(url, Map.class);
} }
// ── Mappings ───────────────────────────────────────────────────────────── // ── Mappings ─────────────────────────────────────────────────────────────
/** public List<ProductMappingResponse> getProductMappings(String status) throws IOException, InterruptedException {
* Obtiene todos los mapeos de productos. String url = baseUrl + "/api/mappings/products" +
* (status != null && !status.isEmpty() ? "?status=" + status : "");
* @param status opcional, filtra por status (PENDING, SYNCED, ERROR) return getList(url, ProductMappingResponse.class);
*/
public List<Map<String, Object>> getProductMappings(String status)
throws IOException, InterruptedException {
String url = baseUrl + "/api/mappings/products";
if (status != null && !status.isEmpty()) {
url += "?status=" + status;
} }
HttpResponse<String> response = sendRequest("GET", url, null); public List<OrderMappingResponse> getOrderMappings() throws IOException, InterruptedException {
if (response.statusCode() == 200) { return getList(baseUrl + "/api/mappings/orders", OrderMappingResponse.class);
return objectMapper.readValue(
response.body(),
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
);
}
throw new IOException("Failed to fetch product mappings: " + response.statusCode());
} }
/** // ── Sync Triggers ────────────────────────────────────────────────────────
* Obtiene todos los mapeos de pedidos.
*/
public List<Map<String, Object>> getOrderMappings() throws IOException, InterruptedException {
String url = baseUrl + "/api/mappings/orders";
HttpResponse<String> response = sendRequest("GET", url, null); public SyncTriggerResponse triggerProductSync() throws IOException, InterruptedException {
if (response.statusCode() == 200) { return post(baseUrl + "/api/sync/products", SyncTriggerResponse.class);
return objectMapper.readValue(
response.body(),
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
);
}
throw new IOException("Failed to fetch order mappings: " + response.statusCode());
} }
// ── Sync Triggers ─────────────────────────────────────────────────────── public SyncTriggerResponse triggerStockSync() throws IOException, InterruptedException {
return post(baseUrl + "/api/sync/stock", SyncTriggerResponse.class);
/**
* Dispara sincronización de productos.
*/
public Map<String, Object> triggerProductSync() throws IOException, InterruptedException {
String url = baseUrl + "/api/sync/products";
return postJson(url, new HashMap<>(), Map.class);
} }
/** public SyncTriggerResponse triggerOrderSync() throws IOException, InterruptedException {
* Dispara sincronización de stock. return post(baseUrl + "/api/sync/orders", SyncTriggerResponse.class);
*/
public Map<String, Object> triggerStockSync() throws IOException, InterruptedException {
String url = baseUrl + "/api/sync/stock";
return postJson(url, new HashMap<>(), Map.class);
}
/**
* Dispara sincronización de pedidos.
*/
public Map<String, Object> triggerOrderSync() throws IOException, InterruptedException {
String url = baseUrl + "/api/sync/orders";
return postJson(url, new HashMap<>(), Map.class);
} }
// ── Helpers ────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────
private <T> T getJson(String url, Class<T> responseType) throws IOException, InterruptedException { private <T> T get(String url, Class<T> type) throws IOException, InterruptedException {
HttpResponse<String> response = sendRequest("GET", url, null);
if (response.statusCode() == 200) return objectMapper.readValue(response.body(), type);
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
}
private <T> List<T> getList(String url, Class<T> elementType) throws IOException, InterruptedException {
HttpResponse<String> response = sendRequest("GET", url, null); HttpResponse<String> response = sendRequest("GET", url, null);
if (response.statusCode() == 200) { 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()); throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
} }
private <T> T postJson(String url, Object body, Class<T> responseType) private <T> T post(String url, Class<T> type) throws IOException, InterruptedException {
throws IOException, InterruptedException { HttpResponse<String> response = sendRequest("POST", url, "{}");
String jsonBody = objectMapper.writeValueAsString(body);
HttpResponse<String> response = sendRequest("POST", url, jsonBody);
if (response.statusCode() == 200 || response.statusCode() == 201) { 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()); throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
} }
@ -166,9 +115,7 @@ public class SyncServiceClient {
.method(method, body != null .method(method, body != null
? HttpRequest.BodyPublishers.ofString(body) ? HttpRequest.BodyPublishers.ofString(body)
: HttpRequest.BodyPublishers.noBody()); : HttpRequest.BodyPublishers.noBody());
return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
HttpRequest request = builder.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} }
private String buildBasicAuthHeader(String username, String password) { private String buildBasicAuthHeader(String username, String password) {

View File

@ -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"
);
}
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -1,12 +1,14 @@
package com.teterialosjuanjos.tfg.javafxclient.model; package com.teterialosjuanjos.tfg.javafxclient.model;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
import com.teterialosjuanjos.tfg.javafxclient.util.SettingsStore;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; 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 @Slf4j
@Getter @Getter
@ -20,16 +22,16 @@ public class SessionManager {
private boolean authenticated; private boolean authenticated;
public SessionManager() { public SessionManager() {
AppSettings saved = SettingsStore.load();
this.baseUrl = saved.getBaseUrl();
this.username = saved.getUsername();
this.password = saved.getPassword();
this.authenticated = false; this.authenticated = false;
// Default: Railway production
this.baseUrl = "https://proyectointermodular-production-a9c3.up.railway.app";
} }
/** /**
* Intenta autenticarse contra el sync-service. * Autentica contra el sync-service verificando el health endpoint.
* Si es exitoso, inicializa el cliente HTTP. * Si tiene éxito, guarda los ajustes en SettingsStore.
*
* @return true si auth fue exitosa
*/ */
public boolean authenticate(String baseUrl, String username, String password) { public boolean authenticate(String baseUrl, String username, String password) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
@ -38,9 +40,9 @@ public class SessionManager {
try { try {
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password); this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
// Prueba conectividad
syncServiceClient.checkHealth(); syncServiceClient.checkHealth();
this.authenticated = true; this.authenticated = true;
SettingsStore.save(new AppSettings(baseUrl, username, password));
log.info("Authentication successful for user: {}", username); log.info("Authentication successful for user: {}", username);
return true; return true;
} catch (Exception e) { } 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() { public void logout() {
this.authenticated = false; this.authenticated = false;
this.syncServiceClient = null; this.syncServiceClient = null;

View File

@ -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;
}

View File

@ -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<String> errors;
}

View File

@ -2,40 +2,37 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; 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.AlertUtil;
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
import javafx.animation.Animation; import javafx.animation.Animation;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.layout.GridPane; import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.*;
import javafx.scene.layout.VBox;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
import javafx.util.Duration; import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.Instant; import java.util.HashMap;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Map; 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 @Slf4j
public class DashboardController { public class DashboardController {
private final SessionManager sessionManager; private final SessionManager sessionManager;
private Label lastProductSyncLabel;
private Label lastStockSyncLabel; private final Map<String, Label> lastSyncInfoLabels = new HashMap<>();
private Label lastOrderSyncLabel; private Button syncAllButton;
private Label successRateLabel; private Label syncAllStatusLabel;
private Label totalSyncsLabel;
public DashboardController(SessionManager sessionManager) { public DashboardController(SessionManager sessionManager) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
@ -43,190 +40,222 @@ public class DashboardController {
public VBox createView() { public VBox createView() {
VBox root = new VBox(20); VBox root = new VBox(20);
root.setPadding(new Insets(20)); root.setPadding(new Insets(24));
root.setStyle("-fx-background-color: #fafafa;"); root.setStyle("-fx-background-color: #FAFAFA;");
// Title // Header
Label title = new Label("Synchronization Dashboard"); Label appTitle = new Label("Sync Manager");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); 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 // Sync All button
GridPane statsGrid = createStatsGrid(); syncAllButton = new Button("Sincronizar todo");
syncAllButton.setMaxWidth(Double.MAX_VALUE);
// Action buttons syncAllButton.setPrefHeight(46);
HBox actionsBox = createActionsBox(); syncAllButton.setStyle(
"-fx-background-color: #1565C0; -fx-text-fill: white; -fx-font-size: 14; " +
root.getChildren().addAll( "-fx-font-weight: bold; -fx-background-radius: 10; -fx-cursor: hand;"
title,
statsGrid,
actionsBox,
new Label() // spacer
); );
syncAllButton.setOnAction(e -> handleSyncAll());
// Auto-refresh cada 10 segundos syncAllStatusLabel = new Label("");
startAutoRefresh(); 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(); refreshDashboard();
startAutoRefresh();
return root; return root;
} }
private GridPane createStatsGrid() { private VBox createSyncCard(String icon, String title, String direction,
GridPane grid = new GridPane(); String type, String color) {
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) {
VBox card = new VBox(10); VBox card = new VBox(10);
card.setPadding(new Insets(15)); card.setPadding(new Insets(16));
card.setStyle( card.setStyle(
"-fx-background-color: white;" + "-fx-background-color: white;" +
"-fx-border-color: " + color + "; " + "-fx-background-radius: 12;" +
"-fx-border-width: 3 0 0 0; " + "-fx-border-radius: 12;" +
"-fx-border-radius: 0;" "-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); // Title row + button
titleLabel.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: " + color + ";"); HBox headerRow = new HBox(10);
headerRow.setAlignment(Pos.CENTER_LEFT);
Label valueLabel = new Label(value); Label titleLabel = new Label(icon + " " + title);
valueLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold; -fx-text-fill: #333333;"); 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; 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() { public void refreshDashboard() {
Thread thread = new Thread(() -> { Thread t = new Thread(() -> {
try { try {
SyncServiceClient client = sessionManager.getSyncServiceClient(); SyncServiceClient client = sessionManager.getSyncServiceClient();
List<Map<String, Object>> logs = client.getLogs(null); List<SyncLogResponse> logs = client.getLogs(null);
Platform.runLater(() -> updateCards(logs));
Platform.runLater(() -> updateDashboard(logs));
} catch (Exception e) { } catch (Exception e) {
log.error("Error refreshing dashboard: {}", e.getMessage()); log.error("Dashboard refresh failed: {}", e.getMessage());
} }
}); });
thread.setDaemon(true); t.setDaemon(true);
thread.start(); t.start();
} }
private void updateDashboard(List<Map<String, Object>> logs) { private void updateCards(List<SyncLogResponse> logs) {
if (logs.isEmpty()) { // Toma el más reciente de cada tipo (el primero encontrado en la lista, que viene newest-first)
totalSyncsLabel.setText("0"); Map<String, SyncLogResponse> latestPerType = new HashMap<>();
successRateLabel.setText("0%"); for (SyncLogResponse entry : logs) {
return; if (entry.getSyncType() != null) {
} latestPerType.putIfAbsent(entry.getSyncType(), entry);
totalSyncsLabel.setText(String.valueOf(logs.size()));
// Busca último sync de cada tipo
for (Map<String, Object> 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);
} }
} }
// Success rate latestPerType.forEach((type, entry) -> {
long successCount = logs.stream() Label label = lastSyncInfoLabels.get(type);
.filter(l -> ((Number) l.get("itemsFailed")).longValue() == 0) if (label == null) return;
.count();
long successRate = (successCount * 100) / logs.size(); String time = DateUtils.formatRelative(entry.getStartedAt());
successRateLabel.setText(successRate + "%"); 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() { private void startAutoRefresh() {
Timeline timeline = new Timeline( Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(30), e -> refreshDashboard()));
new KeyFrame(Duration.seconds(10), e -> refreshDashboard())
);
timeline.setCycleCount(Animation.INDEFINITE); timeline.setCycleCount(Animation.INDEFINITE);
timeline.play(); 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);
}
} }

View File

@ -2,221 +2,187 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager; 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.AlertUtil;
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.List; 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 @Slf4j
public class LogsController { public class LogsController {
private final SessionManager sessionManager; private final SessionManager sessionManager;
private TableView<Map<String, Object>> logsTable; private TableView<SyncLogResponse> logsTable;
private TextArea errorDetailsArea;
public LogsController(SessionManager sessionManager) { public LogsController(SessionManager sessionManager) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
} }
public VBox createView() { public VBox createView() {
VBox root = new VBox(15); VBox root = new VBox(12);
root.setPadding(new Insets(20)); root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;"); root.setStyle("-fx-background-color: #FAFAFA;");
// Title // Header
Label title = new Label("Synchronization Logs"); HBox headerRow = new HBox(10);
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); 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 // Table
logsTable = createLogsTable(); logsTable = createTable();
VBox.setVgrow(logsTable, javafx.scene.layout.Priority.ALWAYS); VBox.setVgrow(logsTable, Priority.ALWAYS);
logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> {
if (newVal != null) { // Click row → open detail dialog
showLogDetails(newVal); logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, log) -> {
if (log != null) {
new LogDetailDialog(log).show();
Platform.runLater(() -> logsTable.getSelectionModel().clearSelection());
} }
}); });
// Details panel root.getChildren().addAll(headerRow, chipsBox, logsTable);
VBox detailsPanel = createDetailsPanel();
// Filter & Actions
HBox filterBox = createFilterBox();
root.getChildren().addAll(title, filterBox, logsTable, detailsPanel);
// Load initial data
loadLogs(null); loadLogs(null);
return root; return root;
} }
private TableView<Map<String, Object>> createLogsTable() { private HBox createFilterChips() {
TableView<Map<String, Object>> table = new TableView<>(); ToggleGroup group = new ToggleGroup();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
table.setStyle("-fx-font-size: 11;");
table.setPrefHeight(300);
// ID column ToggleButton allBtn = chip("Todos", null, group, true);
TableColumn<Map<String, Object>, String> idCol = new TableColumn<>("ID"); ToggleButton productsBtn = chip("Productos", "PRODUCT_PUSH", group, false);
idCol.setCellValueFactory(cf -> { ToggleButton stockBtn = chip("Stock", "STOCK_PUSH", group, false);
Object value = cf.getValue().get("id"); ToggleButton ordersBtn = chip("Pedidos", "ORDER_PULL", group, false);
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
});
idCol.setPrefWidth(50);
// Type column group.selectedToggleProperty().addListener((obs, old, newToggle) -> {
TableColumn<Map<String, Object>, String> typeCol = new TableColumn<>("Type"); if (newToggle == null) { allBtn.setSelected(true); return; }
typeCol.setCellValueFactory(cf -> { loadLogs((String) newToggle.getUserData());
String value = (String) cf.getValue().get("syncType");
return new javafx.beans.property.SimpleObjectProperty<>(value);
});
typeCol.setPrefWidth(120);
// Started At column
TableColumn<Map<String, Object>, 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<Map<String, Object>, 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<Map<String, Object>, 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<Map<String, Object>, 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<Map<String, Object>, 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;");
}
}
}); });
table.getColumns().addAll(idCol, typeCol, startedCol, finishedCol, processedCol, failedCol); HBox box = new HBox(8, allBtn, productsBtn, stockBtn, ordersBtn);
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;");
box.setAlignment(Pos.CENTER_LEFT); box.setAlignment(Pos.CENTER_LEFT);
Label filterLabel = new Label("Filter by type:");
ComboBox<String> 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; return box;
} }
private void loadLogs(String type) { private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) {
Thread thread = new Thread(() -> { ToggleButton btn = new ToggleButton(text);
try { btn.setToggleGroup(group);
SyncServiceClient client = sessionManager.getSyncServiceClient(); btn.setSelected(selected);
List<Map<String, Object>> logs = client.getLogs(type); btn.setUserData(value);
btn.getStyleClass().add("filter-chip");
return btn;
}
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(logs); private TableView<SyncLogResponse> createTable() {
Platform.runLater(() -> logsTable.setItems(items)); TableView<SyncLogResponse> table = new TableView<>();
} catch (Exception e) { table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
log.error("Error loading logs: {}", e.getMessage()); table.setStyle("-fx-cursor: hand;");
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load logs: " + e.getMessage()));
// Status icon column
TableColumn<SyncLogResponse, SyncLogResponse> 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<SyncLogResponse, String> typeCol = new TableColumn<>("Tipo");
typeCol.setCellValueFactory(cf -> new SimpleStringProperty(DateUtils.syncTypeLabel(cf.getValue().getSyncType())));
typeCol.setPrefWidth(100);
// Started (relative)
TableColumn<SyncLogResponse, String> startedCol = new TableColumn<>("Inicio");
startedCol.setCellValueFactory(cf -> new SimpleStringProperty(DateUtils.formatRelative(cf.getValue().getStartedAt())));
startedCol.setPrefWidth(130);
// Duration
TableColumn<SyncLogResponse, String> durationCol = new TableColumn<>("Duración");
durationCol.setCellValueFactory(cf ->
new SimpleStringProperty(DateUtils.formatDuration(cf.getValue().getStartedAt(), cf.getValue().getFinishedAt())));
durationCol.setPrefWidth(80);
// Processed
TableColumn<SyncLogResponse, String> 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<SyncLogResponse, String> 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<String, Object> log) { private void loadLogs(String type) {
String errorDetails = (String) log.get("errorDetails"); Thread t = new Thread(() -> {
if (errorDetails != null && !errorDetails.isEmpty()) { try {
errorDetailsArea.setText(errorDetails); SyncServiceClient client = sessionManager.getSyncServiceClient();
} else { List<SyncLogResponse> logs = client.getLogs(type);
int processed = ((Number) log.get("itemsProcessed")).intValue(); Platform.runLater(() -> logsTable.setItems(FXCollections.observableArrayList(logs)));
int failed = ((Number) log.get("itemsFailed")).intValue(); } catch (Exception e) {
errorDetailsArea.setText("✓ Sync completed successfully\n" + log.error("Error loading logs: {}", e.getMessage());
"Processed: " + processed + "\n" + Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los logs: " + e.getMessage()));
"Failed: " + failed); }
} });
t.setDaemon(true);
t.start();
} }
} }

View File

@ -1,30 +1,32 @@
package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; 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.SessionManager;
import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse;
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.*;
import javafx.scene.layout.VBox;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.List; 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 @Slf4j
public class OrdersController { public class OrdersController {
private final SessionManager sessionManager; private final SessionManager sessionManager;
private final DashboardController dashboardController; private final DashboardController dashboardController;
private TableView<Map<String, Object>> ordersTable; private TableView<OrderMappingResponse> ordersTable;
public OrdersController(SessionManager sessionManager, DashboardController dashboardController) { public OrdersController(SessionManager sessionManager, DashboardController dashboardController) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
@ -32,194 +34,145 @@ public class OrdersController {
} }
public VBox createView() { public VBox createView() {
VBox root = new VBox(15); VBox root = new VBox(12);
root.setPadding(new Insets(20)); root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;"); root.setStyle("-fx-background-color: #FAFAFA;");
// Title Label title = new Label("Mappings");
Label title = new Label("Order Mappings (PrestaShop → Dolibarr)"); title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); Label subtitle = new Label("Pedidos — PrestaShop → Dolibarr");
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
// Info box // Info banner
HBox infoBox = createInfoBox(); 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 = createTable();
ordersTable = createOrdersTable(); VBox.setVgrow(ordersTable, Priority.ALWAYS);
VBox.setVgrow(ordersTable, javafx.scene.layout.Priority.ALWAYS);
// Action buttons
HBox actionBox = createActionBox(); HBox actionBox = createActionBox();
root.getChildren().addAll(title, infoBox, ordersTable, actionBox); root.getChildren().addAll(title, subtitle, infoBanner, ordersTable, actionBox);
// Load initial data
loadOrders(); loadOrders();
return root; return root;
} }
private HBox createInfoBox() { private TableView<OrderMappingResponse> createTable() {
HBox box = new HBox(10); TableView<OrderMappingResponse> table = new TableView<>();
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<Map<String, Object>> createOrdersTable() {
TableView<Map<String, Object>> table = new TableView<>();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
table.setStyle("-fx-font-size: 11;");
// PrestaShop Order ID column TableColumn<OrderMappingResponse, String> psIdCol = new TableColumn<>("Pedido PS #");
TableColumn<Map<String, Object>, String> psOrderIdCol = new TableColumn<>("PS Order ID"); psIdCol.setCellValueFactory(cf ->
psOrderIdCol.setCellValueFactory(cf -> { new SimpleStringProperty(String.valueOf(cf.getValue().getPrestashopOrderId())));
Object value = cf.getValue().get("prestashopOrderId"); psIdCol.setPrefWidth(100);
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
});
psOrderIdCol.setPrefWidth(120);
// Dolibarr Order ID column TableColumn<OrderMappingResponse, String> dolOrderCol = new TableColumn<>("Comanda Dolibarr");
TableColumn<Map<String, Object>, String> dolOrderIdCol = new TableColumn<>("Dolibarr Order ID"); dolOrderCol.setCellValueFactory(cf -> {
dolOrderIdCol.setCellValueFactory(cf -> { Integer v = cf.getValue().getDolibarrOrderId();
Object value = cf.getValue().get("dolibarrOrderId"); return new SimpleStringProperty(v != null ? String.valueOf(v) : "—");
return new javafx.beans.property.SimpleObjectProperty<>(
value != null ? String.valueOf(value) : "—"
);
}); });
dolOrderIdCol.setPrefWidth(120); dolOrderCol.setPrefWidth(130);
// Dolibarr Invoice ID column TableColumn<OrderMappingResponse, String> dolInvoiceCol = new TableColumn<>("Factura");
TableColumn<Map<String, Object>, String> dolInvoiceIdCol = new TableColumn<>("Invoice ID"); dolInvoiceCol.setCellValueFactory(cf -> {
dolInvoiceIdCol.setCellValueFactory(cf -> { Integer v = cf.getValue().getDolibarrInvoiceId();
Object value = cf.getValue().get("dolibarrInvoiceId"); return new SimpleStringProperty(v != null ? String.valueOf(v) : "—");
return new javafx.beans.property.SimpleObjectProperty<>(
value != null ? String.valueOf(value) : "—"
);
}); });
dolInvoiceIdCol.setPrefWidth(100); dolInvoiceCol.setPrefWidth(80);
// Status column // Status chip column
TableColumn<Map<String, Object>, String> statusCol = new TableColumn<>("Status"); TableColumn<OrderMappingResponse, String> statusCol = new TableColumn<>("Estado");
statusCol.setCellValueFactory(cf -> { statusCol.setCellValueFactory(cf -> new SimpleStringProperty(cf.getValue().getStatus()));
String value = (String) cf.getValue().get("status"); statusCol.setPrefWidth(110);
return new javafx.beans.property.SimpleObjectProperty<>(value); statusCol.setCellFactory(c -> new TableCell<>() {
});
statusCol.setPrefWidth(100);
statusCol.setCellFactory(col -> new TableCell<Map<String, Object>, String>() {
@Override @Override
protected void updateItem(String item, boolean empty) { protected void updateItem(String status, boolean empty) {
super.updateItem(item, empty); super.updateItem(status, empty);
if (empty || item == null) { if (empty || status == null) { setGraphic(null); return; }
Label chip = new Label(DateUtils.orderStatusLabel(status));
chip.getStyleClass().add(DateUtils.orderStatusCssClass(status));
setGraphic(chip);
setText(null); setText(null);
setStyle("");
} else {
setText(item);
if ("INVOICED".equals(item)) {
setStyle("-fx-text-fill: #2e7d32; -fx-font-weight: bold;");
}
}
} }
}); });
// Imported At column TableColumn<OrderMappingResponse, String> importedCol = new TableColumn<>("Importado");
TableColumn<Map<String, Object>, String> importedAtCol = new TableColumn<>("Imported At"); importedCol.setCellValueFactory(cf ->
importedAtCol.setCellValueFactory(cf -> { new SimpleStringProperty(DateUtils.formatDateTime(cf.getValue().getImportedAt())));
Object value = cf.getValue().get("importedAt"); importedCol.setPrefWidth(150);
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);
table.getColumns().addAll(psIdCol, dolOrderCol, dolInvoiceCol, statusCol, importedCol);
return table; return table;
} }
private HBox createActionBox() { private HBox createActionBox() {
HBox box = new HBox(10); HBox box = new HBox(10);
box.setPadding(new Insets(15)); box.setPadding(new Insets(10, 0, 0, 0));
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;"); box.setAlignment(Pos.CENTER_RIGHT);
box.setAlignment(Pos.CENTER_LEFT);
Label label = new Label("Actions:"); Button refreshBtn = new Button("🔄 Actualizar");
label.setStyle("-fx-font-weight: bold;"); 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"); Button syncBtn = new Button("Sincronizar pedidos");
syncButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #f57c00; -fx-text-fill: white;"); syncBtn.setStyle("-fx-background-color: #E65100; -fx-text-fill: white; " +
syncButton.setOnAction(e -> triggerOrderSync()); "-fx-background-radius: 8; -fx-padding: 10 20; -fx-font-weight: bold; -fx-cursor: hand;");
syncBtn.setOnAction(e -> triggerOrderSync(syncBtn));
Button refreshButton = new Button("🔄 Refresh");
refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;");
refreshButton.setOnAction(e -> loadOrders());
box.getChildren().addAll(label, syncButton, refreshButton);
box.getChildren().addAll(refreshBtn, syncBtn);
return box; return box;
} }
private void loadOrders() { private void loadOrders() {
Thread thread = new Thread(() -> { Thread t = new Thread(() -> {
try { try {
SyncServiceClient client = sessionManager.getSyncServiceClient(); SyncServiceClient client = sessionManager.getSyncServiceClient();
List<Map<String, Object>> mappings = client.getOrderMappings(); List<OrderMappingResponse> orders = client.getOrderMappings();
Platform.runLater(() -> ordersTable.setItems(FXCollections.observableArrayList(orders)));
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(mappings);
Platform.runLater(() -> ordersTable.setItems(items));
} catch (Exception e) { } catch (Exception e) {
log.error("Error loading orders: {}", e.getMessage()); 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); t.setDaemon(true);
thread.start(); t.start();
} }
private void triggerOrderSync() { private void triggerOrderSync(Button btn) {
Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION); Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirmAlert.setTitle("Confirm Sync"); confirm.setTitle("Confirmar sincronización");
confirmAlert.setHeaderText("Sync Orders from PrestaShop to Dolibarr?"); confirm.setHeaderText("¿Sincronizar pedidos de PrestaShop a Dolibarr?");
confirmAlert.setContentText("This will import new orders from PrestaShop as sales orders and invoices in 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) { btn.setDisable(true);
Thread thread = new Thread(() -> { Thread t = new Thread(() -> {
try { try {
SyncServiceClient client = sessionManager.getSyncServiceClient(); SyncServiceClient client = sessionManager.getSyncServiceClient();
Map<String, Object> result = client.triggerOrderSync(); SyncTriggerResponse result = client.triggerOrderSync();
Platform.runLater(() -> { Platform.runLater(() -> {
int processed = ((Number) result.get("processed")).intValue(); btn.setDisable(false);
int failed = ((Number) result.get("failed")).intValue(); if (result.getItemsFailed() == 0) {
AlertUtil.showInfo("Completado", "Pedidos importados: " + result.getItemsProcessed());
if (failed == 0) {
AlertUtil.showInfo("Success",
"Orders synced: " + processed + " orders imported");
} else { } else {
AlertUtil.showWarn("Partial Success", AlertUtil.showWarn("Completado con errores",
"Processed: " + processed + ", Failed: " + failed); result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
} }
loadOrders(); loadOrders();
dashboardController.refreshDashboard(); dashboardController.refreshDashboard();
}); });
} catch (Exception e) { } catch (Exception e) {
Platform.runLater(() -> Platform.runLater(() -> {
AlertUtil.showError("Sync Error", e.getMessage()) btn.setDisable(false);
); AlertUtil.showError("Error de sincronización", e.getMessage());
});
} }
}); });
thread.setDaemon(true); t.setDaemon(true);
thread.start(); t.start();
}
} }
} }

View File

@ -1,30 +1,37 @@
package com.teterialosjuanjos.tfg.javafxclient.ui.controllers; package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient; 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.SessionManager;
import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse;
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil; import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.extern.slf4j.Slf4j; 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 @Slf4j
public class ProductsController { public class ProductsController {
private final SessionManager sessionManager; private final SessionManager sessionManager;
private final DashboardController dashboardController; private final DashboardController dashboardController;
private TableView<Map<String, Object>> productsTable;
private TableView<ProductMappingResponse> productsTable;
private List<ProductMappingResponse> allProducts = List.of();
private String activeStatusFilter = null;
public ProductsController(SessionManager sessionManager, DashboardController dashboardController) { public ProductsController(SessionManager sessionManager, DashboardController dashboardController) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
@ -32,240 +39,197 @@ public class ProductsController {
} }
public VBox createView() { public VBox createView() {
VBox root = new VBox(15); VBox root = new VBox(12);
root.setPadding(new Insets(20)); root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;"); root.setStyle("-fx-background-color: #FAFAFA;");
// Title // Header
Label title = new Label("Product Mappings"); Label title = new Label("Mappings");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); 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 // Search bar
HBox filterBox = createFilterBox(); 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 // Table
productsTable = createProductsTable(); productsTable = createTable();
VBox.setVgrow(productsTable, javafx.scene.layout.Priority.ALWAYS); VBox.setVgrow(productsTable, Priority.ALWAYS);
// Action buttons // Action buttons
HBox actionBox = createActionBox(); HBox actionBox = createActionBox();
root.getChildren().addAll(title, filterBox, productsTable, actionBox); root.getChildren().addAll(title, subtitle, searchField, chipsBox, productsTable, actionBox);
// Load initial data
loadProducts(null);
loadProducts();
return root; return root;
} }
private HBox createFilterBox() { private HBox createFilterChips(TextField searchField) {
HBox box = new HBox(10); ToggleGroup group = new ToggleGroup();
box.setPadding(new Insets(10));
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
box.setAlignment(Pos.CENTER_LEFT);
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<String> statusCombo = new ComboBox<>(); group.selectedToggleProperty().addListener((obs, old, newToggle) -> {
statusCombo.setItems(FXCollections.observableArrayList( if (newToggle == null) { allBtn.setSelected(true); return; }
"All", "SYNCED", "PENDING", "ERROR" activeStatusFilter = (String) newToggle.getUserData();
)); loadProducts();
statusCombo.setValue("All");
statusCombo.setPrefWidth(120);
statusCombo.setOnAction(e -> {
String selected = statusCombo.getValue();
loadProducts("All".equals(selected) ? null : selected);
}); });
Region spacer = new Region(); HBox box = new HBox(8, allBtn, syncedBtn, pendingBtn, errorBtn);
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); box.setAlignment(Pos.CENTER_LEFT);
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);
return box; return box;
} }
private TableView<Map<String, Object>> createProductsTable() { private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) {
TableView<Map<String, Object>> table = new TableView<>(); ToggleButton btn = new ToggleButton(text);
btn.setToggleGroup(group);
btn.setSelected(selected);
btn.setUserData(value);
btn.getStyleClass().add("filter-chip");
return btn;
}
private TableView<ProductMappingResponse> createTable() {
TableView<ProductMappingResponse> table = new TableView<>();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
table.setStyle("-fx-font-size: 11;");
// SKU column TableColumn<ProductMappingResponse, String> skuCol = col("SKU", p -> p.getSku());
TableColumn<Map<String, Object>, String> skuCol = new TableColumn<>("SKU"); skuCol.setPrefWidth(140);
skuCol.setCellValueFactory(cf -> {
String value = (String) cf.getValue().get("sku");
return new javafx.beans.property.SimpleObjectProperty<>(value);
});
skuCol.setPrefWidth(120);
// Dolibarr ID column TableColumn<ProductMappingResponse, String> dolIdCol = col("Dolibarr ID",
TableColumn<Map<String, Object>, String> dolIdCol = new TableColumn<>("Dolibarr ID"); p -> p.getDolibarrId() != null ? String.valueOf(p.getDolibarrId()) : "—");
dolIdCol.setCellValueFactory(cf -> {
Object value = cf.getValue().get("dolibarrId");
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
});
dolIdCol.setPrefWidth(100); dolIdCol.setPrefWidth(100);
// PrestaShop ID column TableColumn<ProductMappingResponse, String> psIdCol = col("PrestaShop ID",
TableColumn<Map<String, Object>, String> psIdCol = new TableColumn<>("PrestaShop ID"); p -> p.getPrestashopId() != null ? String.valueOf(p.getPrestashopId()) : "—");
psIdCol.setCellValueFactory(cf -> { psIdCol.setPrefWidth(110);
Object value = cf.getValue().get("prestashopId");
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
});
psIdCol.setPrefWidth(100);
// Status column TableColumn<ProductMappingResponse, String> lastSyncCol = col("Última sync",
TableColumn<Map<String, Object>, String> statusCol = new TableColumn<>("Status"); p -> DateUtils.formatRelative(p.getLastSyncedAt()));
statusCol.setCellValueFactory(cf -> { lastSyncCol.setPrefWidth(130);
String value = (String) cf.getValue().get("syncStatus");
return new javafx.beans.property.SimpleObjectProperty<>(value); // Status chip column
}); TableColumn<ProductMappingResponse, String> statusCol = new TableColumn<>("Estado");
statusCol.setPrefWidth(80); statusCol.setCellValueFactory(cf -> new SimpleStringProperty(cf.getValue().getSyncStatus()));
statusCol.setCellFactory(col -> new TableCell<Map<String, Object>, String>() { statusCol.setPrefWidth(120);
statusCol.setCellFactory(c -> new TableCell<>() {
@Override @Override
protected void updateItem(String item, boolean empty) { protected void updateItem(String status, boolean empty) {
super.updateItem(item, empty); super.updateItem(status, empty);
if (empty || item == null) { if (empty || status == null) { setGraphic(null); return; }
Label chip = new Label(DateUtils.productStatusLabel(status));
chip.getStyleClass().add(DateUtils.productStatusCssClass(status));
setGraphic(chip);
setText(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;");
}
}
} }
}); });
// Error message column TableColumn<ProductMappingResponse, String> errorCol = col("Error",
TableColumn<Map<String, Object>, String> errorCol = new TableColumn<>("Error"); p -> p.getErrorMessage() != null ? p.getErrorMessage() : "");
errorCol.setCellValueFactory(cf -> {
Object value = cf.getValue().get("errorMessage");
String text = value != null ? (String) value : "";
return new javafx.beans.property.SimpleObjectProperty<>(text);
});
errorCol.setPrefWidth(200); errorCol.setPrefWidth(200);
table.getColumns().addAll(skuCol, dolIdCol, psIdCol, statusCol, errorCol); table.getColumns().addAll(skuCol, dolIdCol, psIdCol, lastSyncCol, statusCol, errorCol);
return table; return table;
} }
private <T> TableColumn<ProductMappingResponse, String> col(
String header, java.util.function.Function<ProductMappingResponse, String> extractor) {
TableColumn<ProductMappingResponse, String> col = new TableColumn<>(header);
col.setCellValueFactory(cf -> new SimpleStringProperty(extractor.apply(cf.getValue())));
return col;
}
private HBox createActionBox() { private HBox createActionBox() {
HBox box = new HBox(10); HBox box = new HBox(10);
box.setPadding(new Insets(15)); box.setPadding(new Insets(10, 0, 0, 0));
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
box.setAlignment(Pos.CENTER_RIGHT); box.setAlignment(Pos.CENTER_RIGHT);
Button syncButton = new Button("🔄 Sync Products from Dolibarr"); Button refreshBtn = new Button("🔄 Actualizar");
syncButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #1976d2; -fx-text-fill: white;"); refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 16;");
syncButton.setOnAction(e -> triggerProductSync()); refreshBtn.setOnAction(e -> loadProducts());
Button stockButton = new Button("📊 Sync Stock"); Button syncProductsBtn = new Button("Sincronizar productos");
stockButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #388e3c; -fx-text-fill: white;"); syncProductsBtn.setStyle("-fx-background-color: #1565C0; -fx-text-fill: white; " +
stockButton.setOnAction(e -> triggerStockSync()); "-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; return box;
} }
private void loadProducts(String status) { private void loadProducts() {
Thread thread = new Thread(() -> { Thread t = new Thread(() -> {
try { try {
SyncServiceClient client = sessionManager.getSyncServiceClient(); SyncServiceClient client = sessionManager.getSyncServiceClient();
List<Map<String, Object>> mappings = client.getProductMappings(status); allProducts = client.getProductMappings(activeStatusFilter);
Platform.runLater(() -> productsTable.setItems(FXCollections.observableArrayList(allProducts)));
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(mappings);
Platform.runLater(() -> productsTable.setItems(items));
} catch (Exception e) { } catch (Exception e) {
log.error("Error loading products: {}", e.getMessage()); 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); t.setDaemon(true);
thread.start(); t.start();
} }
private void triggerProductSync() { private void applyFilters(TextField searchField, String searchText) {
Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION); String query = searchText == null ? "" : searchText.toLowerCase();
confirmAlert.setTitle("Confirm Sync"); List<ProductMappingResponse> filtered = allProducts.stream()
confirmAlert.setHeaderText("Sync Products from Dolibarr to PrestaShop?"); .filter(p -> p.getSku() != null && p.getSku().toLowerCase().contains(query))
confirmAlert.setContentText("This will push new/modified products from Dolibarr to PrestaShop."); .collect(Collectors.toList());
productsTable.setItems(FXCollections.observableArrayList(filtered));
}
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { private void triggerSync(String type, Button btn) {
Thread thread = new Thread(() -> { 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;
btn.setDisable(true);
Thread t = new Thread(() -> {
try { try {
SyncServiceClient client = sessionManager.getSyncServiceClient(); SyncServiceClient client = sessionManager.getSyncServiceClient();
Map<String, Object> result = client.triggerProductSync(); SyncTriggerResponse result = type.equals("PRODUCT_PUSH")
? client.triggerProductSync()
: client.triggerStockSync();
Platform.runLater(() -> { Platform.runLater(() -> {
int processed = ((Number) result.get("processed")).intValue(); btn.setDisable(false);
int failed = ((Number) result.get("failed")).intValue(); if (result.getItemsFailed() == 0) {
AlertUtil.showInfo("Completado", label + ": " + result.getItemsProcessed() + " procesados.");
if (failed == 0) {
AlertUtil.showInfo("Success",
"Sync completed: " + processed + " products processed");
} else { } else {
AlertUtil.showWarn("Partial Success", AlertUtil.showWarn("Completado con errores",
"Processed: " + processed + ", Failed: " + failed); result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
} }
loadProducts();
loadProducts(null);
dashboardController.refreshDashboard(); dashboardController.refreshDashboard();
}); });
} catch (Exception e) { } catch (Exception e) {
Platform.runLater(() ->
AlertUtil.showError("Sync Error", e.getMessage())
);
}
});
thread.setDaemon(true);
thread.start();
}
}
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.");
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
Thread thread = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
Map<String, Object> result = client.triggerStockSync();
Platform.runLater(() -> { Platform.runLater(() -> {
int processed = ((Number) result.get("processed")).intValue(); btn.setDisable(false);
int failed = ((Number) result.get("failed")).intValue(); AlertUtil.showError("Error de sincronización", e.getMessage());
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); t.setDaemon(true);
thread.start(); t.start();
}
} }
} }

View File

@ -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.");
}
}

View File

@ -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);
}
}

View File

@ -7,17 +7,20 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.Region; import javafx.scene.layout.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage; import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Pantalla de login para autenticación contra el sync-service. * Pantalla de login para autenticación contra el sync-service.
* Pre-rellena los campos con los ajustes guardados en SettingsStore.
*/ */
@Slf4j @Slf4j
public class LoginStage { 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 SessionManager sessionManager;
private final Stage primaryStage; private final Stage primaryStage;
private Stage loginWindow; private Stage loginWindow;
@ -29,9 +32,9 @@ public class LoginStage {
public void show() { public void show() {
loginWindow = new Stage(); loginWindow = new Stage();
loginWindow.setTitle("TFG Sync Service — Login"); loginWindow.setTitle("Sync Manager — Iniciar sesión");
loginWindow.setWidth(400); loginWindow.setWidth(420);
loginWindow.setHeight(500); loginWindow.setHeight(520);
loginWindow.setResizable(false); loginWindow.setResizable(false);
VBox root = createLoginForm(); VBox root = createLoginForm();
@ -45,129 +48,121 @@ public class LoginStage {
} }
private VBox createLoginForm() { private VBox createLoginForm() {
VBox form = new VBox(15); VBox form = new VBox(0);
form.setPadding(new Insets(40)); form.setStyle("-fx-background-color: #F5F5F5;");
form.setAlignment(Pos.TOP_CENTER);
form.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 0;"); // 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 // Title
Label title = new Label("Sync Service Client"); Label title = new Label("Sync Manager");
title.setStyle("-fx-font-size: 24; -fx-font-weight: bold; -fx-text-fill: #1976d2;"); title.setStyle("-fx-font-size: 26; -fx-font-weight: bold; -fx-text-fill: #1565C0;");
Label subtitle = new Label("Dolibarr ↔ PrestaShop Integration"); Label subtitle = new Label("Dolibarr · PrestaShop");
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #666666;"); subtitle.setStyle("-fx-font-size: 13; -fx-text-fill: #757575;");
// URL input VBox titleBox = new VBox(4, title, subtitle);
Label urlLabel = new Label("Base URL:"); titleBox.setAlignment(Pos.CENTER);
urlLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;");
TextField urlField = new TextField(); Separator sep = new Separator();
urlField.setText("https://proyectointermodular-production-a9c3.up.railway.app"); 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;"); urlField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
// Username input // Username
Label usernameLabel = new Label("Username:"); Label userLabel = new Label("Usuario");
usernameLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); userLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;");
TextField usernameField = new TextField(); TextField userField = new TextField(
usernameField.setPromptText("admin"); sessionManager.getUsername() != null ? sessionManager.getUsername() : "");
usernameField.setText("admin"); userField.setPromptText("admin");
usernameField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); userField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
// Password input // Password
Label passwordLabel = new Label("Password:"); Label passLabel = new Label("Contraseña");
passwordLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); passLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;");
PasswordField passwordField = new PasswordField(); PasswordField passField = new PasswordField();
passwordField.setPromptText("••••••••"); passField.setText(sessionManager.getPassword() != null ? sessionManager.getPassword() : "");
passwordField.setText("admin123"); passField.setPromptText("••••••••");
passwordField.setStyle("-fx-font-size: 12; -fx-padding: 8;"); passField.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()
));
// Dev mode toggle // Dev mode toggle
CheckBox devModeCheckBox = new CheckBox("Use local dev server (http://localhost:8080)"); CheckBox devCheckBox = new CheckBox("Usar servidor local (localhost:8080)");
devModeCheckBox.setStyle("-fx-font-size: 11;"); devCheckBox.setStyle("-fx-font-size: 11; -fx-text-fill: #546E7A;");
devModeCheckBox.selectedProperty().addListener((obs, old, newVal) -> { devCheckBox.selectedProperty().addListener((obs, old, val) ->
if (newVal) { urlField.setText(val ? DEV_URL : PROD_URL));
urlField.setText("http://localhost:8080");
} else {
urlField.setText("https://proyectointermodular-production-a9c3.up.railway.app");
}
});
// Separator // Login button
Separator separator = new Separator(); 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 // Allow Enter key in password field
Label infoLabel = new Label("Tip: Default credentials are admin / admin123"); passField.setOnAction(e -> handleLogin(urlField, userField, passField));
infoLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #999999; -fx-font-style: italic;");
form.getChildren().addAll( card.getChildren().addAll(
title, titleBox, sep,
subtitle, urlLabel, urlField,
new Separator(), userLabel, userField,
urlLabel, passLabel, passField,
urlField, devCheckBox,
usernameLabel, new Region(),
usernameField, loginButton
passwordLabel,
passwordField,
separator,
devModeCheckBox,
new Region(), // spacer
loginButton,
infoLabel
); );
form.getChildren().addAll(topBar, card);
return form; return form;
} }
private void handleLogin(String baseUrl, String username, String password) { private void handleLogin(TextField urlField, TextField userField, PasswordField passField) {
if (baseUrl.isEmpty() || username.isEmpty() || password.isEmpty()) { String url = urlField.getText().trim();
AlertUtil.showError("Validation Error", "All fields are required"); 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; return;
} }
// Ejecuta en background para no freezear UI Thread t = new Thread(() -> {
Thread loginThread = new Thread(() -> {
try { try {
boolean success = sessionManager.authenticate(baseUrl, username, password); boolean success = sessionManager.authenticate(url, user, pass);
Platform.runLater(() -> { Platform.runLater(() -> {
if (success) { if (success) {
AlertUtil.showInfo("Success", "Authentication successful!");
loginWindow.close(); loginWindow.close();
openMainStage(); new MainStage(sessionManager, primaryStage).show();
} else { } else {
AlertUtil.showError("Login Failed", AlertUtil.showError("Autenticación fallida",
"Could not authenticate. Check credentials and URL."); "No se pudo conectar. Verifica URL y credenciales.");
} }
}); });
} catch (Exception ex) { } catch (Exception ex) {
Platform.runLater(() -> { log.error("Login error: {}", ex.getMessage());
AlertUtil.showError("Connection Error", ex.getMessage()); Platform.runLater(() -> AlertUtil.showError("Error de conexión", ex.getMessage()));
});
} }
}); });
loginThread.setDaemon(true); t.setDaemon(true);
loginThread.start(); t.start();
}
private void openMainStage() {
MainStage mainStage = new MainStage(sessionManager, primaryStage);
mainStage.show();
} }
} }

View File

@ -6,27 +6,18 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage; import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j; 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 @Slf4j
public class MainStage { public class MainStage {
private final SessionManager sessionManager; private final SessionManager sessionManager;
private final Stage primaryStage; 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) { public MainStage(SessionManager sessionManager, Stage primaryStage) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
@ -34,12 +25,14 @@ public class MainStage {
} }
public void show() { public void show() {
mainWindow = new Stage(); Stage mainWindow = new Stage();
mainWindow.setTitle("TFG Sync Service — Dashboard"); mainWindow.setTitle("Sync Manager");
mainWindow.setWidth(1200); 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 scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm()); scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
@ -48,92 +41,83 @@ public class MainStage {
mainWindow.show(); mainWindow.show();
} }
private BorderPane createMainLayout() { private BorderPane createMainLayout(Stage window) {
BorderPane root = new BorderPane(); BorderPane root = new BorderPane();
root.setTop(createHeader(window));
// Top: Header con usuario y logout
root.setTop(createHeader());
// Center: Tabs
root.setCenter(createTabPane()); root.setCenter(createTabPane());
return root; return root;
} }
private VBox createHeader() { private HBox createHeader(Stage window) {
VBox header = new VBox(); HBox header = new HBox(12);
header.setPadding(new Insets(15)); header.setPadding(new Insets(12, 20, 12, 20));
header.setStyle("-fx-background-color: #1976d2; -fx-border-color: #1565c0; -fx-border-width: 0 0 2 0;"); header.setAlignment(Pos.CENTER_LEFT);
header.setStyle("-fx-background-color: #1565C0;");
HBox topBar = new HBox(10); Label appName = new Label("Sync Manager");
topBar.setAlignment(Pos.CENTER_LEFT); appName.setStyle("-fx-font-size: 17; -fx-font-weight: bold; -fx-text-fill: white;");
Label title = new Label("Sync Service Control Panel"); Label sep = new Label("·");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold; -fx-text-fill: white;"); 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(); Region spacer = new Region();
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); HBox.setHgrow(spacer, Priority.ALWAYS);
Label userLabel = new Label("User: " + sessionManager.getUsername()); Label userChip = new Label(sessionManager.getUsername());
userLabel.setStyle("-fx-font-size: 12; -fx-text-fill: white;"); 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"); Button logoutBtn = new Button("Salir");
logoutButton.setStyle("-fx-font-size: 11; -fx-padding: 5;"); logoutBtn.setStyle(
logoutButton.setOnAction(e -> handleLogout()); "-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;"
topBar.getChildren().addAll(title, spacer, userLabel, logoutButton); );
logoutBtn.setOnAction(e -> handleLogout(window));
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);
header.getChildren().addAll(appName, sep, connectedLabel, spacer, userChip, logoutBtn);
return header; return header;
} }
private TabPane createTabPane() { 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.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
tabPane.setStyle("-fx-font-size: 12;"); 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; return tabPane;
} }
private void handleLogout() { private Tab tab(String label, javafx.scene.Node content) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION); Tab t = new Tab(label, content);
alert.setTitle("Logout"); t.setStyle("-fx-padding: 8 12;");
alert.setHeaderText("Are you sure?"); return t;
alert.setContentText("You will be logged out and returned to the login screen."); }
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;
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
sessionManager.logout(); sessionManager.logout();
mainWindow.close(); window.close();
new LoginStage(sessionManager, primaryStage).show(); new LoginStage(sessionManager, primaryStage).show();
} }
} }
}

View File

@ -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 -> "";
};
}
}

View File

@ -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());
}
}

View File

@ -10,5 +10,5 @@
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</root> </root>
<logger name="com.teterialosjuanjos.tfg.javafx_client" level="DEBUG"/> <logger name="com.teterialosjuanjos.tfg.javafxclient" level="DEBUG"/>
</configuration> </configuration>

View File

@ -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 { .root {
-fx-background-color: #fafafa; -fx-background-color: #FAFAFA;
-fx-font-size: 12; -fx-font-size: 12px;
} }
/* Buttons */ /* === BUTTONS === */
.button { .button {
-fx-padding: 8; -fx-padding: 8 16 8 16;
-fx-font-size: 12; -fx-font-size: 12px;
-fx-text-fill: #333; -fx-background-radius: 8;
-fx-background-color: #e0e0e0; -fx-background-color: #E0E0E0;
-fx-background-radius: 4; -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 { .btn-primary {
-fx-background-color: #d0d0d0; -fx-background-color: #1565C0;
} -fx-text-fill: white;
/* 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; -fx-font-weight: bold;
} }
.btn-primary:hover { -fx-background-color: #1976D2; }
.table-row-cell { .btn-success {
-fx-font-size: 11; -fx-background-color: #2E7D32;
-fx-background-color: white; -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 */ /* === STATUS CHIPS (Label) === */
.combo-box, .chip-synced {
.combo-box-base { -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-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; -fx-padding: 5;
} }
/* TextArea */ /* === TAB PANE === */
.text-area { .tab-pane > .tab-header-area > .tab-header-background {
-fx-background-color: #f5f5f5; -fx-background-color: #F5F5F5;
-fx-text-fill: #333; -fx-border-color: #E0E0E0;
-fx-font-size: 10; -fx-border-width: 0 0 1 0;
} }
/* Separator */
.separator {
-fx-text-fill: #e0e0e0;
}
/* TabPane */
.tab-pane {
-fx-padding: 0;
}
.tab { .tab {
-fx-padding: 10; -fx-background-color: transparent;
-fx-padding: 10 20 10 20;
-fx-font-size: 12px;
-fx-cursor: hand;
} }
.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; }
.tab-header-background { /* === SCROLL PANE === */
-fx-background-color: #f5f5f5; .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; }
/* CheckBox */ /* === SEPARATOR === */
.check-box { .separator .line { -fx-border-color: #E0E0E0; -fx-border-width: 1; }
-fx-font-size: 11;
-fx-text-fill: #333; /* === CHECK BOX === */
} .check-box { -fx-font-size: 11px; -fx-text-fill: #212121; }

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.teterialosjuanjos.tfg.javafx_client" level="DEBUG"/>
</configuration>

View File

@ -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;
}

View File

@ -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

View File

@ -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