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>
<dependencies>
<!-- JavaFX - TODO los módulos necesarios -->
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
@ -41,13 +36,8 @@
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- JSON parsing (Jackson) -->
<!-- JSON (Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@ -107,7 +97,7 @@
</configuration>
</plugin>
<!-- JavaFX Maven Plugin - ESENCIAL -->
<!-- Ejecutar con: mvn javafx:run -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
@ -116,30 +106,6 @@
<mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass>
</configuration>
</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>
</build>
</project>

View File

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

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;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
import com.teterialosjuanjos.tfg.javafxclient.util.SettingsStore;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* Gestiona la sesión actual: credenciales, cliente HTTP, estado de autenticación.
* Gestiona la sesión actual: credenciales, cliente HTTP y persistencia de ajustes.
* Carga los ajustes guardados en el constructor.
*/
@Slf4j
@Getter
@ -20,16 +22,16 @@ public class SessionManager {
private boolean authenticated;
public SessionManager() {
AppSettings saved = SettingsStore.load();
this.baseUrl = saved.getBaseUrl();
this.username = saved.getUsername();
this.password = saved.getPassword();
this.authenticated = false;
// Default: Railway production
this.baseUrl = "https://proyectointermodular-production-a9c3.up.railway.app";
}
/**
* Intenta autenticarse contra el sync-service.
* Si es exitoso, inicializa el cliente HTTP.
*
* @return true si auth fue exitosa
* Autentica contra el sync-service verificando el health endpoint.
* Si tiene éxito, guarda los ajustes en SettingsStore.
*/
public boolean authenticate(String baseUrl, String username, String password) {
this.baseUrl = baseUrl;
@ -38,9 +40,9 @@ public class SessionManager {
try {
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
// Prueba conectividad
syncServiceClient.checkHealth();
this.authenticated = true;
SettingsStore.save(new AppSettings(baseUrl, username, password));
log.info("Authentication successful for user: {}", username);
return true;
} catch (Exception e) {
@ -50,6 +52,30 @@ public class SessionManager {
}
}
/**
* Actualiza los ajustes y reinicializa el cliente HTTP sin health check.
* Usado desde la pantalla de ajustes para cambiar la configuración en caliente.
*/
public void updateSettings(String baseUrl, String username, String password) {
this.baseUrl = baseUrl;
this.username = username;
this.password = password;
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
SettingsStore.save(new AppSettings(baseUrl, username, password));
log.info("Settings updated, new base URL: {}", baseUrl);
}
/** Comprueba conectividad con los ajustes dados sin modificar el estado. */
public boolean testConnection(String baseUrl, String username, String password) {
try {
new SyncServiceClient(baseUrl, username, password).checkHealth();
return true;
} catch (Exception e) {
log.debug("Connection test failed: {}", e.getMessage());
return false;
}
}
public void logout() {
this.authenticated = false;
this.syncServiceClient = null;

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.model.SessionManager;
import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse;
import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse;
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Dashboard con estado de sincronizaciones y estadísticas.
* Tab Dashboard: botón "Sincronizar todo", 3 tarjetas de sync con datos del último log.
* Equivalente a DashboardScreen de la app Android.
*/
@Slf4j
public class DashboardController {
private final SessionManager sessionManager;
private Label lastProductSyncLabel;
private Label lastStockSyncLabel;
private Label lastOrderSyncLabel;
private Label successRateLabel;
private Label totalSyncsLabel;
private final Map<String, Label> lastSyncInfoLabels = new HashMap<>();
private Button syncAllButton;
private Label syncAllStatusLabel;
public DashboardController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
@ -43,190 +40,222 @@ public class DashboardController {
public VBox createView() {
VBox root = new VBox(20);
root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;");
root.setPadding(new Insets(24));
root.setStyle("-fx-background-color: #FAFAFA;");
// Title
Label title = new Label("Synchronization Dashboard");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// Header
Label appTitle = new Label("Sync Manager");
appTitle.setStyle("-fx-font-size: 22; -fx-font-weight: bold; -fx-text-fill: #1565C0;");
Label appSubtitle = new Label("Dolibarr · PrestaShop");
appSubtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
VBox headerBox = new VBox(2, appTitle, appSubtitle);
// Sync stats cards
GridPane statsGrid = createStatsGrid();
// Action buttons
HBox actionsBox = createActionsBox();
root.getChildren().addAll(
title,
statsGrid,
actionsBox,
new Label() // spacer
// Sync All button
syncAllButton = new Button("Sincronizar todo");
syncAllButton.setMaxWidth(Double.MAX_VALUE);
syncAllButton.setPrefHeight(46);
syncAllButton.setStyle(
"-fx-background-color: #1565C0; -fx-text-fill: white; -fx-font-size: 14; " +
"-fx-font-weight: bold; -fx-background-radius: 10; -fx-cursor: hand;"
);
syncAllButton.setOnAction(e -> handleSyncAll());
// Auto-refresh cada 10 segundos
startAutoRefresh();
syncAllStatusLabel = new Label("");
syncAllStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565C0;");
syncAllStatusLabel.setVisible(false);
// 3 sync cards
HBox cardsBox = new HBox(16);
VBox productCard = createSyncCard("📦", "Productos", "Dolibarr → PrestaShop", "PRODUCT_PUSH", "#1565C0");
VBox stockCard = createSyncCard("📊", "Stock", "Dolibarr → PrestaShop", "STOCK_PUSH", "#2E7D32");
VBox ordersCard = createSyncCard("🛒", "Pedidos", "PrestaShop → Dolibarr", "ORDER_PULL", "#E65100");
HBox.setHgrow(productCard, Priority.ALWAYS);
HBox.setHgrow(stockCard, Priority.ALWAYS);
HBox.setHgrow(ordersCard, Priority.ALWAYS);
cardsBox.getChildren().addAll(productCard, stockCard, ordersCard);
root.getChildren().addAll(headerBox, syncAllButton, syncAllStatusLabel, cardsBox);
// Carga inicial
refreshDashboard();
startAutoRefresh();
return root;
}
private GridPane createStatsGrid() {
GridPane grid = new GridPane();
grid.setHgap(15);
grid.setVgap(15);
grid.setPadding(new Insets(10));
grid.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0; -fx-border-radius: 4;");
// Card: Last Product Sync
VBox productCard = createStatCard(
"📦 Last Product Sync",
"Never",
"#1976d2"
);
lastProductSyncLabel = (Label) productCard.getChildren().get(1);
GridPane.setConstraints(productCard, 0, 0);
// Card: Last Stock Sync
VBox stockCard = createStatCard(
"📊 Last Stock Sync",
"Never",
"#388e3c"
);
lastStockSyncLabel = (Label) stockCard.getChildren().get(1);
GridPane.setConstraints(stockCard, 1, 0);
// Card: Last Order Sync
VBox orderCard = createStatCard(
"🛒 Last Order Sync",
"Never",
"#f57c00"
);
lastOrderSyncLabel = (Label) orderCard.getChildren().get(1);
GridPane.setConstraints(orderCard, 2, 0);
// Card: Total Syncs
VBox totalCard = createStatCard(
"📈 Total Syncs",
"0",
"#6a1b9a"
);
totalSyncsLabel = (Label) totalCard.getChildren().get(1);
GridPane.setConstraints(totalCard, 0, 1);
// Card: Success Rate
VBox rateCard = createStatCard(
"✓ Success Rate",
"0%",
"#00796b"
);
successRateLabel = (Label) rateCard.getChildren().get(1);
GridPane.setConstraints(rateCard, 1, 1);
grid.getChildren().addAll(productCard, stockCard, orderCard, totalCard, rateCard);
return grid;
}
private VBox createStatCard(String title, String value, String color) {
private VBox createSyncCard(String icon, String title, String direction,
String type, String color) {
VBox card = new VBox(10);
card.setPadding(new Insets(15));
card.setPadding(new Insets(16));
card.setStyle(
"-fx-background-color: white; " +
"-fx-border-color: " + color + "; " +
"-fx-border-width: 3 0 0 0; " +
"-fx-border-radius: 0;"
"-fx-background-color: white;" +
"-fx-background-radius: 12;" +
"-fx-border-radius: 12;" +
"-fx-border-color: #E0E0E0 #E0E0E0 #E0E0E0 " + color + ";" +
"-fx-border-width: 1 1 1 4;" +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.06), 6, 0, 0, 2);"
);
card.setMinWidth(250);
card.setAlignment(Pos.TOP_LEFT);
Label titleLabel = new Label(title);
titleLabel.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: " + color + ";");
// Title row + button
HBox headerRow = new HBox(10);
headerRow.setAlignment(Pos.CENTER_LEFT);
Label valueLabel = new Label(value);
valueLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold; -fx-text-fill: #333333;");
Label titleLabel = new Label(icon + " " + title);
titleLabel.setStyle("-fx-font-size: 14; -fx-font-weight: bold; -fx-text-fill: " + color + ";");
card.getChildren().addAll(titleLabel, valueLabel);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
Button syncBtn = new Button("Sincronizar");
syncBtn.setStyle(
"-fx-background-color: " + color + "; -fx-text-fill: white; " +
"-fx-background-radius: 8; -fx-padding: 6 14; -fx-font-size: 11; -fx-cursor: hand;"
);
syncBtn.setOnAction(e -> handleSingleSync(type, syncBtn));
headerRow.getChildren().addAll(titleLabel, spacer, syncBtn);
Label dirLabel = new Label(direction);
dirLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #9E9E9E;");
Label lastSyncLabel = new Label("Sin sincronizaciones previas");
lastSyncLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #BDBDBD;");
lastSyncInfoLabels.put(type, lastSyncLabel);
card.getChildren().addAll(headerRow, dirLabel, lastSyncLabel);
return card;
}
private HBox createActionsBox() {
HBox box = new HBox(10);
box.setPadding(new Insets(15));
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
box.setAlignment(Pos.CENTER_LEFT);
Label label = new Label("Quick Actions:");
label.setStyle("-fx-font-weight: bold;");
// Buttons
javafx.scene.control.Button refreshButton = new javafx.scene.control.Button("🔄 Refresh Now");
refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;");
refreshButton.setOnAction(e -> refreshDashboard());
box.getChildren().addAll(label, refreshButton);
return box;
}
public void refreshDashboard() {
Thread thread = new Thread(() -> {
Thread t = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
List<Map<String, Object>> logs = client.getLogs(null);
Platform.runLater(() -> updateDashboard(logs));
List<SyncLogResponse> logs = client.getLogs(null);
Platform.runLater(() -> updateCards(logs));
} catch (Exception e) {
log.error("Error refreshing dashboard: {}", e.getMessage());
log.error("Dashboard refresh failed: {}", e.getMessage());
}
});
thread.setDaemon(true);
thread.start();
t.setDaemon(true);
t.start();
}
private void updateDashboard(List<Map<String, Object>> logs) {
if (logs.isEmpty()) {
totalSyncsLabel.setText("0");
successRateLabel.setText("0%");
return;
}
totalSyncsLabel.setText(String.valueOf(logs.size()));
// Busca último sync de cada tipo
for (Map<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);
private void updateCards(List<SyncLogResponse> logs) {
// Toma el más reciente de cada tipo (el primero encontrado en la lista, que viene newest-first)
Map<String, SyncLogResponse> latestPerType = new HashMap<>();
for (SyncLogResponse entry : logs) {
if (entry.getSyncType() != null) {
latestPerType.putIfAbsent(entry.getSyncType(), entry);
}
}
// Success rate
long successCount = logs.stream()
.filter(l -> ((Number) l.get("itemsFailed")).longValue() == 0)
.count();
long successRate = (successCount * 100) / logs.size();
successRateLabel.setText(successRate + "%");
latestPerType.forEach((type, entry) -> {
Label label = lastSyncInfoLabels.get(type);
if (label == null) return;
String time = DateUtils.formatRelative(entry.getStartedAt());
int processed = entry.getItemsProcessed() != null ? entry.getItemsProcessed() : 0;
int failed = entry.getItemsFailed() != null ? entry.getItemsFailed() : 0;
String text = time + " · " + processed + " procesados";
if (failed > 0) text += ", " + failed + " fallidos";
label.setText(text);
label.setStyle("-fx-font-size: 11; -fx-text-fill: " + (failed > 0 ? "#C62828" : "#757575") + ";");
});
}
private void handleSingleSync(String type, Button btn) {
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Confirmar sincronización");
confirm.setHeaderText("¿Ejecutar sincronización de " + DateUtils.syncTypeLabel(type) + "?");
confirm.setContentText(DateUtils.syncTypeDirection(type));
if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
btn.setDisable(true);
Thread t = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
SyncTriggerResponse result = switch (type) {
case "PRODUCT_PUSH" -> client.triggerProductSync();
case "STOCK_PUSH" -> client.triggerStockSync();
case "ORDER_PULL" -> client.triggerOrderSync();
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
Platform.runLater(() -> {
btn.setDisable(false);
if (result.getItemsFailed() == 0) {
AlertUtil.showInfo("Completado",
DateUtils.syncTypeLabel(type) + ": " + result.getItemsProcessed() + " procesados.");
} else {
AlertUtil.showWarn("Completado con errores",
result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
}
refreshDashboard();
});
} catch (Exception e) {
Platform.runLater(() -> {
btn.setDisable(false);
AlertUtil.showError("Error de sincronización", e.getMessage());
});
}
});
t.setDaemon(true);
t.start();
}
private void handleSyncAll() {
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Sincronizar todo");
confirm.setHeaderText("Ejecutará productos, stock y pedidos en secuencia.");
confirm.setContentText("¿Continuar?");
if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
syncAllButton.setDisable(true);
syncAllStatusLabel.setVisible(true);
Thread t = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando productos (1/3)..."));
SyncTriggerResponse r1 = client.triggerProductSync();
Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando stock (2/3)..."));
SyncTriggerResponse r2 = client.triggerStockSync();
Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando pedidos (3/3)..."));
SyncTriggerResponse r3 = client.triggerOrderSync();
int totalProcessed = r1.getItemsProcessed() + r2.getItemsProcessed() + r3.getItemsProcessed();
int totalFailed = r1.getItemsFailed() + r2.getItemsFailed() + r3.getItemsFailed();
Platform.runLater(() -> {
syncAllButton.setDisable(false);
syncAllStatusLabel.setVisible(false);
if (totalFailed == 0) {
AlertUtil.showInfo("Sincronización completada",
"Total procesados: " + totalProcessed);
} else {
AlertUtil.showWarn("Completado con errores",
"Procesados: " + totalProcessed + ", Fallidos: " + totalFailed);
}
refreshDashboard();
});
} catch (Exception e) {
Platform.runLater(() -> {
syncAllButton.setDisable(false);
syncAllStatusLabel.setVisible(false);
AlertUtil.showError("Error en sincronización", e.getMessage());
});
}
});
t.setDaemon(true);
t.start();
}
private void startAutoRefresh() {
Timeline timeline = new Timeline(
new KeyFrame(Duration.seconds(10), e -> refreshDashboard())
);
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(30), e -> refreshDashboard()));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
private String formatInstant(Instant instant) {
LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
return ldt.format(formatter);
}
}

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.model.SessionManager;
import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse;
import com.teterialosjuanjos.tfg.javafxclient.ui.dialogs.LogDetailDialog;
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.layout.*;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* Tab para logs de sincronización.
* Tab de historial de sincronizaciones.
* Click en una fila abre LogDetailDialog.
* Equivalente a LogsScreen de la app Android.
*/
@Slf4j
public class LogsController {
private final SessionManager sessionManager;
private TableView<Map<String, Object>> logsTable;
private TextArea errorDetailsArea;
private TableView<SyncLogResponse> logsTable;
public LogsController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public VBox createView() {
VBox root = new VBox(15);
VBox root = new VBox(12);
root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;");
root.setStyle("-fx-background-color: #FAFAFA;");
// Title
Label title = new Label("Synchronization Logs");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// Header
HBox headerRow = new HBox(10);
headerRow.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2);
Label title = new Label("Historial de sincronización");
title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
Label subtitle = new Label("Registros de cada ejecución");
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
titleBox.getChildren().addAll(title, subtitle);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
Button refreshBtn = new Button("🔄 Actualizar");
refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 14;");
refreshBtn.setOnAction(e -> loadLogs(null));
headerRow.getChildren().addAll(titleBox, spacer, refreshBtn);
// Filter chips
HBox chipsBox = createFilterChips();
// Table
logsTable = createLogsTable();
VBox.setVgrow(logsTable, javafx.scene.layout.Priority.ALWAYS);
logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> {
if (newVal != null) {
showLogDetails(newVal);
logsTable = createTable();
VBox.setVgrow(logsTable, Priority.ALWAYS);
// Click row → open detail dialog
logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, log) -> {
if (log != null) {
new LogDetailDialog(log).show();
Platform.runLater(() -> logsTable.getSelectionModel().clearSelection());
}
});
// Details panel
VBox detailsPanel = createDetailsPanel();
// Filter & Actions
HBox filterBox = createFilterBox();
root.getChildren().addAll(title, filterBox, logsTable, detailsPanel);
// Load initial data
root.getChildren().addAll(headerRow, chipsBox, logsTable);
loadLogs(null);
return root;
}
private TableView<Map<String, Object>> createLogsTable() {
TableView<Map<String, Object>> table = new TableView<>();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
table.setStyle("-fx-font-size: 11;");
table.setPrefHeight(300);
private HBox createFilterChips() {
ToggleGroup group = new ToggleGroup();
// ID column
TableColumn<Map<String, Object>, String> idCol = new TableColumn<>("ID");
idCol.setCellValueFactory(cf -> {
Object value = cf.getValue().get("id");
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
});
idCol.setPrefWidth(50);
ToggleButton allBtn = chip("Todos", null, group, true);
ToggleButton productsBtn = chip("Productos", "PRODUCT_PUSH", group, false);
ToggleButton stockBtn = chip("Stock", "STOCK_PUSH", group, false);
ToggleButton ordersBtn = chip("Pedidos", "ORDER_PULL", group, false);
// Type column
TableColumn<Map<String, Object>, String> typeCol = new TableColumn<>("Type");
typeCol.setCellValueFactory(cf -> {
String value = (String) cf.getValue().get("syncType");
return new javafx.beans.property.SimpleObjectProperty<>(value);
});
typeCol.setPrefWidth(120);
// Started At column
TableColumn<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;");
}
}
group.selectedToggleProperty().addListener((obs, old, newToggle) -> {
if (newToggle == null) { allBtn.setSelected(true); return; }
loadLogs((String) newToggle.getUserData());
});
table.getColumns().addAll(idCol, typeCol, startedCol, finishedCol, processedCol, failedCol);
return table;
}
private VBox createDetailsPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
panel.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
Label label = new Label("Error Details (click a row to see details):");
label.setStyle("-fx-font-weight: bold;");
errorDetailsArea = new TextArea();
errorDetailsArea.setEditable(false);
errorDetailsArea.setWrapText(true);
errorDetailsArea.setPrefHeight(120);
errorDetailsArea.setStyle("-fx-font-size: 10; -fx-control-inner-background: #f5f5f5;");
panel.getChildren().addAll(label, errorDetailsArea);
return panel;
}
private HBox createFilterBox() {
HBox box = new HBox(10);
box.setPadding(new Insets(10));
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
HBox box = new HBox(8, allBtn, productsBtn, stockBtn, ordersBtn);
box.setAlignment(Pos.CENTER_LEFT);
Label filterLabel = new Label("Filter by type:");
ComboBox<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;
}
private void loadLogs(String type) {
Thread thread = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
List<Map<String, Object>> logs = client.getLogs(type);
private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) {
ToggleButton btn = new ToggleButton(text);
btn.setToggleGroup(group);
btn.setSelected(selected);
btn.setUserData(value);
btn.getStyleClass().add("filter-chip");
return btn;
}
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(logs);
Platform.runLater(() -> logsTable.setItems(items));
} catch (Exception e) {
log.error("Error loading logs: {}", e.getMessage());
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load logs: " + e.getMessage()));
private TableView<SyncLogResponse> createTable() {
TableView<SyncLogResponse> table = new TableView<>();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
table.setStyle("-fx-cursor: hand;");
// 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) {
String errorDetails = (String) log.get("errorDetails");
if (errorDetails != null && !errorDetails.isEmpty()) {
errorDetailsArea.setText(errorDetails);
} else {
int processed = ((Number) log.get("itemsProcessed")).intValue();
int failed = ((Number) log.get("itemsFailed")).intValue();
errorDetailsArea.setText("✓ Sync completed successfully\n" +
"Processed: " + processed + "\n" +
"Failed: " + failed);
}
private void loadLogs(String type) {
Thread t = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
List<SyncLogResponse> logs = client.getLogs(type);
Platform.runLater(() -> logsTable.setItems(FXCollections.observableArrayList(logs)));
} catch (Exception e) {
log.error("Error loading logs: {}", e.getMessage());
Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los logs: " + e.getMessage()));
}
});
t.setDaemon(true);
t.start();
}
}

View File

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

View File

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

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

View File

@ -6,27 +6,18 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* Pantalla principal con navegación por tabs y dashboard.
* Ventana principal con navegación por tabs: Panel, Productos, Pedidos, Logs, Ajustes.
*/
@Slf4j
public class MainStage {
private final SessionManager sessionManager;
private final Stage primaryStage;
private Stage mainWindow;
private DashboardController dashboardController;
private ProductsController productsController;
private OrdersController ordersController;
private LogsController logsController;
public MainStage(SessionManager sessionManager, Stage primaryStage) {
this.sessionManager = sessionManager;
@ -34,12 +25,14 @@ public class MainStage {
}
public void show() {
mainWindow = new Stage();
mainWindow.setTitle("TFG Sync Service — Dashboard");
Stage mainWindow = new Stage();
mainWindow.setTitle("Sync Manager");
mainWindow.setWidth(1200);
mainWindow.setHeight(700);
mainWindow.setHeight(720);
mainWindow.setMinWidth(900);
mainWindow.setMinHeight(600);
BorderPane root = createMainLayout();
BorderPane root = createMainLayout(mainWindow);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
@ -48,92 +41,83 @@ public class MainStage {
mainWindow.show();
}
private BorderPane createMainLayout() {
private BorderPane createMainLayout(Stage window) {
BorderPane root = new BorderPane();
// Top: Header con usuario y logout
root.setTop(createHeader());
// Center: Tabs
root.setTop(createHeader(window));
root.setCenter(createTabPane());
return root;
}
private VBox createHeader() {
VBox header = new VBox();
header.setPadding(new Insets(15));
header.setStyle("-fx-background-color: #1976d2; -fx-border-color: #1565c0; -fx-border-width: 0 0 2 0;");
private HBox createHeader(Stage window) {
HBox header = new HBox(12);
header.setPadding(new Insets(12, 20, 12, 20));
header.setAlignment(Pos.CENTER_LEFT);
header.setStyle("-fx-background-color: #1565C0;");
HBox topBar = new HBox(10);
topBar.setAlignment(Pos.CENTER_LEFT);
Label appName = new Label("Sync Manager");
appName.setStyle("-fx-font-size: 17; -fx-font-weight: bold; -fx-text-fill: white;");
Label title = new Label("Sync Service Control Panel");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold; -fx-text-fill: white;");
Label sep = new Label("·");
sep.setStyle("-fx-text-fill: rgba(255,255,255,0.5); -fx-font-size: 14;");
Label connectedLabel = new Label("✓ " + sessionManager.getBaseUrl());
connectedLabel.setStyle("-fx-font-size: 11; -fx-text-fill: rgba(255,255,255,0.75);");
Region spacer = new Region();
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
HBox.setHgrow(spacer, Priority.ALWAYS);
Label userLabel = new Label("User: " + sessionManager.getUsername());
userLabel.setStyle("-fx-font-size: 12; -fx-text-fill: white;");
Label userChip = new Label(sessionManager.getUsername());
userChip.setStyle(
"-fx-font-size: 11; -fx-text-fill: white;" +
"-fx-background-color: rgba(255,255,255,0.15);" +
"-fx-background-radius: 12; -fx-padding: 4 10;"
);
Button logoutButton = new Button("Logout");
logoutButton.setStyle("-fx-font-size: 11; -fx-padding: 5;");
logoutButton.setOnAction(e -> handleLogout());
topBar.getChildren().addAll(title, spacer, userLabel, logoutButton);
HBox statusBar = new HBox(20);
statusBar.setPadding(new Insets(10, 15, 10, 15));
statusBar.setStyle("-fx-background-color: #f5f5f5;");
Label statusLabel = new Label("✓ Connected to: " + sessionManager.getBaseUrl());
statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #2e7d32;");
statusBar.getChildren().add(statusLabel);
header.getChildren().addAll(topBar, statusBar);
Button logoutBtn = new Button("Salir");
logoutBtn.setStyle(
"-fx-background-color: rgba(255,255,255,0.15); -fx-text-fill: white;" +
"-fx-background-radius: 8; -fx-padding: 5 12; -fx-cursor: hand; -fx-font-size: 11;"
);
logoutBtn.setOnAction(e -> handleLogout(window));
header.getChildren().addAll(appName, sep, connectedLabel, spacer, userChip, logoutBtn);
return header;
}
private TabPane createTabPane() {
TabPane tabPane = new TabPane();
DashboardController dashboardController = new DashboardController(sessionManager);
ProductsController productsController = new ProductsController(sessionManager, dashboardController);
OrdersController ordersController = new OrdersController(sessionManager, dashboardController);
LogsController logsController = new LogsController(sessionManager);
SettingsController settingsController = new SettingsController(sessionManager);
Tab dashboardTab = tab("📊 Panel", dashboardController.createView());
Tab productsTab = tab("📦 Productos", productsController.createView());
Tab ordersTab = tab("🛒 Pedidos", ordersController.createView());
Tab logsTab = tab("📋 Historial", logsController.createView());
Tab settingsTab = tab("⚙ Ajustes", settingsController.createView());
TabPane tabPane = new TabPane(dashboardTab, productsTab, ordersTab, logsTab, settingsTab);
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
tabPane.setStyle("-fx-font-size: 12;");
// Dashboard tab
dashboardController = new DashboardController(sessionManager);
Tab dashboardTab = new Tab("📊 Dashboard", dashboardController.createView());
dashboardTab.setStyle("-fx-padding: 10;");
// Products tab
productsController = new ProductsController(sessionManager, dashboardController);
Tab productsTab = new Tab("📦 Products", productsController.createView());
// Orders tab
ordersController = new OrdersController(sessionManager, dashboardController);
Tab ordersTab = new Tab("🛒 Orders", ordersController.createView());
// Logs tab
logsController = new LogsController(sessionManager);
Tab logsTab = new Tab("📋 Logs", logsController.createView());
tabPane.getTabs().addAll(dashboardTab, productsTab, ordersTab, logsTab);
return tabPane;
}
private void handleLogout() {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Logout");
alert.setHeaderText("Are you sure?");
alert.setContentText("You will be logged out and returned to the login screen.");
private Tab tab(String label, javafx.scene.Node content) {
Tab t = new Tab(label, content);
t.setStyle("-fx-padding: 8 12;");
return t;
}
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
sessionManager.logout();
mainWindow.close();
new LoginStage(sessionManager, primaryStage).show();
}
private void handleLogout(Stage window) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Cerrar sesión");
alert.setHeaderText("¿Cerrar sesión?");
alert.setContentText("Volverás a la pantalla de inicio de sesión.");
if (alert.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
sessionManager.logout();
window.close();
new LoginStage(sessionManager, primaryStage).show();
}
}

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"/>
</root>
<logger name="com.teterialosjuanjos.tfg.javafx_client" level="DEBUG"/>
<logger name="com.teterialosjuanjos.tfg.javafxclient" level="DEBUG"/>
</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 {
-fx-background-color: #fafafa;
-fx-font-size: 12;
-fx-background-color: #FAFAFA;
-fx-font-size: 12px;
}
/* Buttons */
/* === BUTTONS === */
.button {
-fx-padding: 8;
-fx-font-size: 12;
-fx-text-fill: #333;
-fx-background-color: #e0e0e0;
-fx-background-radius: 4;
-fx-padding: 8 16 8 16;
-fx-font-size: 12px;
-fx-background-radius: 8;
-fx-background-color: #E0E0E0;
-fx-text-fill: #212121;
-fx-cursor: hand;
-fx-border-width: 0;
}
.button:hover { -fx-background-color: #BDBDBD; }
.button:pressed { -fx-background-color: #9E9E9E; }
.button:hover {
-fx-background-color: #d0d0d0;
}
/* Text Fields */
.text-field,
.password-field {
-fx-padding: 8;
-fx-border-color: #bdbdbd;
-fx-border-width: 0 0 1 0;
-fx-font-size: 12;
}
/* Labels */
.label {
-fx-text-fill: #333;
}
/* Tables */
.table-view {
-fx-background-color: white;
-fx-border-color: #e0e0e0;
-fx-font-size: 11;
}
.table-view .column-header {
-fx-background-color: #f5f5f5;
.btn-primary {
-fx-background-color: #1565C0;
-fx-text-fill: white;
-fx-font-weight: bold;
}
.btn-primary:hover { -fx-background-color: #1976D2; }
.table-row-cell {
-fx-font-size: 11;
-fx-background-color: white;
.btn-success {
-fx-background-color: #2E7D32;
-fx-text-fill: white;
}
.btn-success:hover { -fx-background-color: #388E3C; }
.btn-warning {
-fx-background-color: #E65100;
-fx-text-fill: white;
}
.btn-warning:hover { -fx-background-color: #EF6C00; }
.btn-secondary {
-fx-background-color: #546E7A;
-fx-text-fill: white;
}
.btn-secondary:hover { -fx-background-color: #607D8B; }
.btn-outline {
-fx-background-color: transparent;
-fx-border-color: #1565C0;
-fx-border-width: 1;
-fx-border-radius: 8;
-fx-background-radius: 8;
-fx-text-fill: #1565C0;
}
.btn-outline:hover { -fx-background-color: #E3F2FD; }
/* === FILTER CHIPS (ToggleButton) === */
.filter-chip {
-fx-background-color: #ECEFF1;
-fx-text-fill: #546E7A;
-fx-background-radius: 20;
-fx-border-radius: 20;
-fx-padding: 6 16 6 16;
-fx-font-size: 11px;
-fx-cursor: hand;
-fx-border-width: 1;
-fx-border-color: #CFD8DC;
}
.filter-chip:selected {
-fx-background-color: #1565C0;
-fx-text-fill: white;
-fx-border-color: #1565C0;
}
.filter-chip:hover {
-fx-background-color: #CFD8DC;
}
.filter-chip:selected:hover {
-fx-background-color: #1976D2;
}
/* ComboBox */
.combo-box,
.combo-box-base {
/* === STATUS CHIPS (Label) === */
.chip-synced {
-fx-background-color: #E8F5E9;
-fx-text-fill: #2E7D32;
-fx-background-radius: 16;
-fx-padding: 2 10 2 10;
-fx-font-weight: bold;
-fx-font-size: 10px;
}
.chip-pending {
-fx-background-color: #FFF3E0;
-fx-text-fill: #E65100;
-fx-background-radius: 16;
-fx-padding: 2 10 2 10;
-fx-font-weight: bold;
-fx-font-size: 10px;
}
.chip-error-status {
-fx-background-color: #FFEBEE;
-fx-text-fill: #C62828;
-fx-background-radius: 16;
-fx-padding: 2 10 2 10;
-fx-font-weight: bold;
-fx-font-size: 10px;
}
.chip-imported {
-fx-background-color: #ECEFF1;
-fx-text-fill: #546E7A;
-fx-background-radius: 16;
-fx-padding: 2 10 2 10;
-fx-font-weight: bold;
-fx-font-size: 10px;
}
.chip-invoiced {
-fx-background-color: #E3F2FD;
-fx-text-fill: #1565C0;
-fx-background-radius: 16;
-fx-padding: 2 10 2 10;
-fx-font-weight: bold;
-fx-font-size: 10px;
}
/* === BANNERS === */
.banner-error {
-fx-background-color: #FFEBEE;
-fx-border-color: #EF9A9A;
-fx-border-width: 0 0 0 4;
-fx-background-radius: 4;
-fx-padding: 12;
}
.banner-warning {
-fx-background-color: #FFF8E1;
-fx-border-color: #FFD54F;
-fx-border-width: 0 0 0 4;
-fx-background-radius: 4;
-fx-padding: 12;
}
.banner-info {
-fx-background-color: #E3F2FD;
-fx-border-color: #90CAF9;
-fx-border-width: 0 0 0 4;
-fx-background-radius: 4;
-fx-padding: 12;
}
.banner-success {
-fx-background-color: #E8F5E9;
-fx-border-color: #A5D6A7;
-fx-border-width: 0 0 0 4;
-fx-background-radius: 4;
-fx-padding: 12;
}
/* === TEXT FIELDS === */
.text-field, .password-field {
-fx-padding: 10;
-fx-background-color: white;
-fx-border-color: #bdbdbd;
-fx-background-radius: 8;
-fx-border-color: #BDBDBD;
-fx-border-width: 1;
-fx-border-radius: 8;
-fx-font-size: 12px;
}
.text-field:focused, .password-field:focused {
-fx-border-color: #1565C0;
-fx-border-width: 2;
-fx-background-insets: 0;
}
/* === LABELS === */
.label { -fx-text-fill: #212121; }
/* === TABLES === */
.table-view {
-fx-background-color: white;
-fx-border-color: #E0E0E0;
-fx-border-width: 1;
-fx-border-radius: 8;
-fx-background-radius: 8;
-fx-font-size: 11px;
}
.table-view .column-header {
-fx-background-color: #F5F5F5;
-fx-font-weight: bold;
-fx-padding: 10 8 10 8;
-fx-text-fill: #546E7A;
-fx-font-size: 11px;
-fx-border-color: transparent;
}
.table-view .column-header-background {
-fx-background-color: #F5F5F5;
-fx-border-color: #E0E0E0;
-fx-border-width: 0 0 1 0;
}
.table-row-cell {
-fx-background-color: white;
-fx-padding: 4 0 4 0;
-fx-border-color: transparent transparent #F5F5F5 transparent;
-fx-border-width: 0 0 1 0;
}
.table-row-cell:odd { -fx-background-color: #FAFAFA; }
.table-row-cell:hover { -fx-background-color: #E3F2FD; }
.table-row-cell:selected { -fx-background-color: #BBDEFB; }
/* === TEXT AREA === */
.text-area {
-fx-background-color: #F5F5F5;
-fx-text-fill: #333;
-fx-font-size: 11px;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #E0E0E0;
-fx-border-width: 1;
}
.text-area .content {
-fx-background-color: #F5F5F5;
-fx-background-radius: 8;
}
/* === COMBO BOX === */
.combo-box {
-fx-background-color: white;
-fx-border-color: #BDBDBD;
-fx-border-radius: 8;
-fx-background-radius: 8;
-fx-padding: 5;
}
/* TextArea */
.text-area {
-fx-background-color: #f5f5f5;
-fx-text-fill: #333;
-fx-font-size: 10;
/* === TAB PANE === */
.tab-pane > .tab-header-area > .tab-header-background {
-fx-background-color: #F5F5F5;
-fx-border-color: #E0E0E0;
-fx-border-width: 0 0 1 0;
}
/* Separator */
.separator {
-fx-text-fill: #e0e0e0;
}
/* TabPane */
.tab-pane {
-fx-padding: 0;
}
.tab {
-fx-padding: 10;
-fx-background-color: transparent;
-fx-padding: 10 20 10 20;
-fx-font-size: 12px;
-fx-cursor: hand;
}
.tab: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 {
-fx-background-color: #f5f5f5;
}
/* === SCROLL PANE === */
.scroll-pane { -fx-background-color: transparent; -fx-border-color: transparent; }
.scroll-pane .viewport { -fx-background-color: transparent; }
.scroll-pane > .scroll-bar { -fx-background-color: transparent; }
/* CheckBox */
.check-box {
-fx-font-size: 11;
-fx-text-fill: #333;
}
/* === SEPARATOR === */
.separator .line { -fx-border-color: #E0E0E0; -fx-border-width: 1; }
/* === 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