Merge pull request #1 from luklpz/develop

DesktopClient con JavaFx
This commit is contained in:
mireya-sp 2026-05-15 17:25:15 +02:00 committed by GitHub
commit f50f64dcdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1964 additions and 0 deletions

145
javafx-client/pom.xml Normal file
View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.teterialosjuanjos.tfg</groupId>
<artifactId>javafx-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>javafx-client</name>
<description>TFG — JavaFX Desktop Client para Sync Service</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<javafx.version>21.0.2</javafx.version>
<javafx.maven.plugin.version>0.0.8</javafx.maven.plugin.version>
</properties>
<dependencies>
<!-- JavaFX - TODO los módulos necesarios -->
<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>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<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) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.0</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- JavaFX Maven Plugin - ESENCIAL -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>${javafx.maven.plugin.version}</version>
<configuration>
<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

@ -0,0 +1,33 @@
package com.teterialosjuanjos.tfg.javafxclient;
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
import com.teterialosjuanjos.tfg.javafxclient.ui.stages.LoginStage;
import javafx.application.Application;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* JavaFX Desktop Client para TFG Integración Dolibarr-PrestaShop.
*
* <p>Punto de entrada de la aplicación. Inicia sesión y luego accede al
* {@code sync-service} en Railway para ver/crear productos, pedidos, logs.</p>
*/
@Slf4j
public class JavaFxClientApplication extends Application {
@Override
public void start(Stage primaryStage) {
log.info("Starting JavaFX Client Application");
// Inicializa SessionManager (almacena credenciales + cliente HTTP)
SessionManager sessionManager = new SessionManager();
// Muestra LoginStage
LoginStage loginStage = new LoginStage(sessionManager, primaryStage);
loginStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@ -0,0 +1,179 @@
package com.teterialosjuanjos.tfg.javafxclient.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Cliente HTTP tipado para el sync-service en Railway.
*
* <p>Maneja autenticación Basic Auth para todos los endpoints.</p>
*/
@Slf4j
public class SyncServiceClient {
private final HttpClient httpClient;
private final String baseUrl;
private final String authHeader;
private final ObjectMapper objectMapper;
public SyncServiceClient(String baseUrl, String username, String password) {
this.baseUrl = baseUrl.replaceAll("/$", ""); // Remove trailing slash
this.httpClient = HttpClient.newHttpClient();
this.authHeader = buildBasicAuthHeader(username, password);
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
}
// ── Health Check ────────────────────────────────────────────────────────
public Map<String, Object> checkHealth() throws IOException, InterruptedException {
String url = baseUrl + "/actuator/health";
return getJson(url, Map.class);
}
// ── 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());
}
/**
* Obtiene un log específico por ID.
*/
public Map<String, Object> getLog(Long id) throws IOException, InterruptedException {
String url = baseUrl + "/api/logs/" + id;
return getJson(url, Map.class);
}
// ── Mappings ─────────────────────────────────────────────────────────────
/**
* 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());
}
/**
* 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());
}
// ── 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);
}
/**
* 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);
}
/**
* Dispara sincronización de pedidos.
*/
public Map<String, Object> triggerOrderSync() throws IOException, InterruptedException {
String url = baseUrl + "/api/sync/orders";
return postJson(url, new HashMap<>(), Map.class);
}
// ── Helpers ──────────────────────────────────────────────────────────────
private <T> T getJson(String url, Class<T> responseType) throws IOException, InterruptedException {
HttpResponse<String> response = sendRequest("GET", url, null);
if (response.statusCode() == 200) {
return objectMapper.readValue(response.body(), responseType);
}
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);
if (response.statusCode() == 200 || response.statusCode() == 201) {
return objectMapper.readValue(response.body(), responseType);
}
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
}
private HttpResponse<String> sendRequest(String method, String url, String body)
throws IOException, InterruptedException {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", authHeader)
.header("Content-Type", "application/json")
.method(method, body != null
? HttpRequest.BodyPublishers.ofString(body)
: HttpRequest.BodyPublishers.noBody());
HttpRequest request = builder.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}
private String buildBasicAuthHeader(String username, String password) {
String credentials = username + ":" + password;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
return "Basic " + encoded;
}
}

View File

@ -0,0 +1,60 @@
package com.teterialosjuanjos.tfg.javafxclient.model;
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* Gestiona la sesión actual: credenciales, cliente HTTP, estado de autenticación.
*/
@Slf4j
@Getter
@Setter
public class SessionManager {
private String baseUrl;
private String username;
private String password;
private SyncServiceClient syncServiceClient;
private boolean authenticated;
public SessionManager() {
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
*/
public boolean authenticate(String baseUrl, String username, String password) {
this.baseUrl = baseUrl;
this.username = username;
this.password = password;
try {
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
// Prueba conectividad
syncServiceClient.checkHealth();
this.authenticated = true;
log.info("Authentication successful for user: {}", username);
return true;
} catch (Exception e) {
log.error("Authentication failed: {}", e.getMessage());
this.authenticated = false;
return false;
}
}
public void logout() {
this.authenticated = false;
this.syncServiceClient = null;
this.username = null;
this.password = null;
log.info("Logged out");
}
}

View File

@ -0,0 +1,232 @@
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.util.AlertUtil;
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.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.List;
import java.util.Map;
/**
* Dashboard con estado de sincronizaciones y estadísticas.
*/
@Slf4j
public class DashboardController {
private final SessionManager sessionManager;
private Label lastProductSyncLabel;
private Label lastStockSyncLabel;
private Label lastOrderSyncLabel;
private Label successRateLabel;
private Label totalSyncsLabel;
public DashboardController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public VBox createView() {
VBox root = new VBox(20);
root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;");
// Title
Label title = new Label("Synchronization Dashboard");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// Sync stats cards
GridPane statsGrid = createStatsGrid();
// Action buttons
HBox actionsBox = createActionsBox();
root.getChildren().addAll(
title,
statsGrid,
actionsBox,
new Label() // spacer
);
// Auto-refresh cada 10 segundos
startAutoRefresh();
// Carga inicial
refreshDashboard();
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) {
VBox card = new VBox(10);
card.setPadding(new Insets(15));
card.setStyle(
"-fx-background-color: white; " +
"-fx-border-color: " + color + "; " +
"-fx-border-width: 3 0 0 0; " +
"-fx-border-radius: 0;"
);
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 + ";");
Label valueLabel = new Label(value);
valueLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold; -fx-text-fill: #333333;");
card.getChildren().addAll(titleLabel, valueLabel);
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(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
List<Map<String, Object>> logs = client.getLogs(null);
Platform.runLater(() -> updateDashboard(logs));
} catch (Exception e) {
log.error("Error refreshing dashboard: {}", e.getMessage());
}
});
thread.setDaemon(true);
thread.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);
}
}
// Success rate
long successCount = logs.stream()
.filter(l -> ((Number) l.get("itemsFailed")).longValue() == 0)
.count();
long successRate = (successCount * 100) / logs.size();
successRateLabel.setText(successRate + "%");
}
private void startAutoRefresh() {
Timeline timeline = new Timeline(
new KeyFrame(Duration.seconds(10), 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

@ -0,0 +1,222 @@
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.util.AlertUtil;
import javafx.application.Platform;
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 lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* Tab para logs de sincronización.
*/
@Slf4j
public class LogsController {
private final SessionManager sessionManager;
private TableView<Map<String, Object>> logsTable;
private TextArea errorDetailsArea;
public LogsController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public VBox createView() {
VBox root = new VBox(15);
root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;");
// Title
Label title = new Label("Synchronization Logs");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// Table
logsTable = createLogsTable();
VBox.setVgrow(logsTable, javafx.scene.layout.Priority.ALWAYS);
logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> {
if (newVal != null) {
showLogDetails(newVal);
}
});
// Details panel
VBox detailsPanel = createDetailsPanel();
// Filter & Actions
HBox filterBox = createFilterBox();
root.getChildren().addAll(title, filterBox, logsTable, detailsPanel);
// Load initial data
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);
// 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);
// 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;");
}
}
});
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;");
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);
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()));
}
});
thread.setDaemon(true);
thread.start();
}
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);
}
}
}

View File

@ -0,0 +1,225 @@
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.util.AlertUtil;
import javafx.application.Platform;
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 lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* Tab para pedidos: ver mapeos de PrestaShop → Dolibarr y disparar sync.
*/
@Slf4j
public class OrdersController {
private final SessionManager sessionManager;
private final DashboardController dashboardController;
private TableView<Map<String, Object>> ordersTable;
public OrdersController(SessionManager sessionManager, DashboardController dashboardController) {
this.sessionManager = sessionManager;
this.dashboardController = dashboardController;
}
public VBox createView() {
VBox root = new VBox(15);
root.setPadding(new Insets(20));
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;");
// Info box
HBox infoBox = createInfoBox();
// Table
ordersTable = createOrdersTable();
VBox.setVgrow(ordersTable, javafx.scene.layout.Priority.ALWAYS);
// Action buttons
HBox actionBox = createActionBox();
root.getChildren().addAll(title, infoBox, ordersTable, actionBox);
// Load initial data
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<>();
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);
// 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) : "—"
);
});
dolOrderIdCol.setPrefWidth(120);
// 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) : "—"
);
});
dolInvoiceIdCol.setPrefWidth(100);
// 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>() {
@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;");
}
}
}
});
// 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);
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);
Label label = new Label("Actions:");
label.setStyle("-fx-font-weight: bold;");
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);
return box;
}
private void loadOrders() {
Thread thread = 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));
} catch (Exception e) {
log.error("Error loading orders: {}", e.getMessage());
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load orders: " + e.getMessage()));
}
});
thread.setDaemon(true);
thread.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.");
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();
}
}
}

View File

@ -0,0 +1,271 @@
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.util.AlertUtil;
import javafx.application.Platform;
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 lombok.extern.slf4j.Slf4j;
import java.util.*;
/**
* Tab para productos: ver mapeos y disparar sync.
*/
@Slf4j
public class ProductsController {
private final SessionManager sessionManager;
private final DashboardController dashboardController;
private TableView<Map<String, Object>> productsTable;
public ProductsController(SessionManager sessionManager, DashboardController dashboardController) {
this.sessionManager = sessionManager;
this.dashboardController = dashboardController;
}
public VBox createView() {
VBox root = new VBox(15);
root.setPadding(new Insets(20));
root.setStyle("-fx-background-color: #fafafa;");
// Title
Label title = new Label("Product Mappings");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// Filter & Actions
HBox filterBox = createFilterBox();
// Table
productsTable = createProductsTable();
VBox.setVgrow(productsTable, javafx.scene.layout.Priority.ALWAYS);
// Action buttons
HBox actionBox = createActionBox();
root.getChildren().addAll(title, filterBox, productsTable, actionBox);
// Load initial data
loadProducts(null);
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);
Label filterLabel = new Label("Filter by status:");
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);
});
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);
return box;
}
private TableView<Map<String, Object>> createProductsTable() {
TableView<Map<String, Object>> 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);
// 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));
});
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);
// 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>() {
@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;");
}
}
}
});
// 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);
});
errorCol.setPrefWidth(200);
table.getColumns().addAll(skuCol, dolIdCol, psIdCol, statusCol, errorCol);
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_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 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());
box.getChildren().addAll(syncButton, stockButton);
return box;
}
private void loadProducts(String status) {
Thread thread = 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));
} catch (Exception e) {
log.error("Error loading products: {}", e.getMessage());
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load products: " + e.getMessage()));
}
});
thread.setDaemon(true);
thread.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 triggerStockSync() {
Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION);
confirmAlert.setTitle("Confirm Sync");
confirmAlert.setHeaderText("Sync Stock from Dolibarr to PrestaShop?");
confirmAlert.setContentText("This will update stock levels in PrestaShop from Dolibarr.");
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
Thread thread = new Thread(() -> {
try {
SyncServiceClient client = sessionManager.getSyncServiceClient();
Map<String, Object> result = client.triggerStockSync();
Platform.runLater(() -> {
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();
}
}
}

View File

@ -0,0 +1,173 @@
package com.teterialosjuanjos.tfg.javafxclient.ui.stages;
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.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* Pantalla de login para autenticación contra el sync-service.
*/
@Slf4j
public class LoginStage {
private final SessionManager sessionManager;
private final Stage primaryStage;
private Stage loginWindow;
public LoginStage(SessionManager sessionManager, Stage primaryStage) {
this.sessionManager = sessionManager;
this.primaryStage = primaryStage;
}
public void show() {
loginWindow = new Stage();
loginWindow.setTitle("TFG Sync Service — Login");
loginWindow.setWidth(400);
loginWindow.setHeight(500);
loginWindow.setResizable(false);
VBox root = createLoginForm();
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
loginWindow.setScene(scene);
loginWindow.setOnCloseRequest(e -> Platform.exit());
loginWindow.show();
}
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;");
// Title
Label title = new Label("Sync Service Client");
title.setStyle("-fx-font-size: 24; -fx-font-weight: bold; -fx-text-fill: #1976d2;");
Label subtitle = new Label("Dolibarr ↔ PrestaShop Integration");
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #666666;");
// 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");
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;");
// 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()
));
// 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");
}
});
// Separator
Separator separator = new Separator();
// 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;");
form.getChildren().addAll(
title,
subtitle,
new Separator(),
urlLabel,
urlField,
usernameLabel,
usernameField,
passwordLabel,
passwordField,
separator,
devModeCheckBox,
new Region(), // spacer
loginButton,
infoLabel
);
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");
return;
}
// Ejecuta en background para no freezear UI
Thread loginThread = new Thread(() -> {
try {
boolean success = sessionManager.authenticate(baseUrl, username, password);
Platform.runLater(() -> {
if (success) {
AlertUtil.showInfo("Success", "Authentication successful!");
loginWindow.close();
openMainStage();
} else {
AlertUtil.showError("Login Failed",
"Could not authenticate. Check credentials and URL.");
}
});
} catch (Exception ex) {
Platform.runLater(() -> {
AlertUtil.showError("Connection Error", ex.getMessage());
});
}
});
loginThread.setDaemon(true);
loginThread.start();
}
private void openMainStage() {
MainStage mainStage = new MainStage(sessionManager, primaryStage);
mainStage.show();
}
}

View File

@ -0,0 +1,139 @@
package com.teterialosjuanjos.tfg.javafxclient.ui.stages;
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
import com.teterialosjuanjos.tfg.javafxclient.ui.controllers.*;
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.stage.Stage;
import lombok.extern.slf4j.Slf4j;
/**
* Pantalla principal con navegación por tabs y dashboard.
*/
@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;
this.primaryStage = primaryStage;
}
public void show() {
mainWindow = new Stage();
mainWindow.setTitle("TFG Sync Service — Dashboard");
mainWindow.setWidth(1200);
mainWindow.setHeight(700);
BorderPane root = createMainLayout();
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
mainWindow.setScene(scene);
mainWindow.show();
}
private BorderPane createMainLayout() {
BorderPane root = new BorderPane();
// Top: Header con usuario y logout
root.setTop(createHeader());
// Center: Tabs
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;");
HBox topBar = new HBox(10);
topBar.setAlignment(Pos.CENTER_LEFT);
Label title = new Label("Sync Service Control Panel");
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold; -fx-text-fill: white;");
Region spacer = new Region();
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
Label userLabel = new Label("User: " + sessionManager.getUsername());
userLabel.setStyle("-fx-font-size: 12; -fx-text-fill: white;");
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);
return header;
}
private TabPane createTabPane() {
TabPane tabPane = new TabPane();
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
tabPane.setStyle("-fx-font-size: 12;");
// Dashboard tab
dashboardController = new DashboardController(sessionManager);
Tab dashboardTab = new Tab("📊 Dashboard", dashboardController.createView());
dashboardTab.setStyle("-fx-padding: 10;");
// Products tab
productsController = new ProductsController(sessionManager, dashboardController);
Tab productsTab = new Tab("📦 Products", productsController.createView());
// Orders tab
ordersController = new OrdersController(sessionManager, dashboardController);
Tab ordersTab = new Tab("🛒 Orders", ordersController.createView());
// Logs tab
logsController = new LogsController(sessionManager);
Tab logsTab = new Tab("📋 Logs", logsController.createView());
tabPane.getTabs().addAll(dashboardTab, productsTab, ordersTab, logsTab);
return tabPane;
}
private void handleLogout() {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Logout");
alert.setHeaderText("Are you sure?");
alert.setContentText("You will be logged out and returned to the login screen.");
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
sessionManager.logout();
mainWindow.close();
new LoginStage(sessionManager, primaryStage).show();
}
}
}

View File

@ -0,0 +1,34 @@
package com.teterialosjuanjos.tfg.javafxclient.util;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
/**
* Utilidades para mostrar diálogos de alerta.
*/
public class AlertUtil {
public static void showInfo(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
public static void showWarn(String title, String message) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
public static void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@ -0,0 +1,14 @@
<?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

@ -0,0 +1,93 @@
/* 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

@ -0,0 +1,14 @@
<?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

@ -0,0 +1,93 @@
/* 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

@ -0,0 +1,20 @@
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

@ -0,0 +1,17 @@
/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