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:
parent
0d32497abd
commit
7eb17a8da2
|
|
@ -0,0 +1,7 @@
|
|||
target/
|
||||
*.class
|
||||
*.jar
|
||||
*.log
|
||||
.idea/
|
||||
*.iml
|
||||
.DS_Store
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
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<OrderMappingResponse> getOrderMappings() throws IOException, InterruptedException {
|
||||
return getList(baseUrl + "/api/mappings/orders", OrderMappingResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los mapeos de pedidos.
|
||||
*/
|
||||
public List<Map<String, Object>> getOrderMappings() throws IOException, InterruptedException {
|
||||
String url = baseUrl + "/api/mappings/orders";
|
||||
// ── Sync Triggers ────────────────────────────────────────────────────────
|
||||
|
||||
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 SyncTriggerResponse triggerProductSync() throws IOException, InterruptedException {
|
||||
return post(baseUrl + "/api/sync/products", SyncTriggerResponse.class);
|
||||
}
|
||||
|
||||
// ── 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 triggerStockSync() throws IOException, InterruptedException {
|
||||
return post(baseUrl + "/api/sync/stock", 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
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);
|
||||
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);
|
||||
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(() -> {
|
||||
btn.setDisable(true);
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||
Map<String, Object> result = client.triggerOrderSync();
|
||||
|
||||
SyncTriggerResponse 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");
|
||||
btn.setDisable(false);
|
||||
if (result.getItemsFailed() == 0) {
|
||||
AlertUtil.showInfo("Completado", "Pedidos importados: " + result.getItemsProcessed());
|
||||
} else {
|
||||
AlertUtil.showWarn("Partial Success",
|
||||
"Processed: " + processed + ", Failed: " + failed);
|
||||
AlertUtil.showWarn("Completado con errores",
|
||||
result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
|
||||
}
|
||||
|
||||
loadOrders();
|
||||
dashboardController.refreshDashboard();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() ->
|
||||
AlertUtil.showError("Sync Error", e.getMessage())
|
||||
);
|
||||
Platform.runLater(() -> {
|
||||
btn.setDisable(false);
|
||||
AlertUtil.showError("Error de sincronización", e.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
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.");
|
||||
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));
|
||||
}
|
||||
|
||||
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
||||
Thread thread = new Thread(() -> {
|
||||
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;
|
||||
|
||||
btn.setDisable(true);
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||
Map<String, Object> result = client.triggerProductSync();
|
||||
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",
|
||||
"Sync completed: " + processed + " products processed");
|
||||
btn.setDisable(false);
|
||||
if (result.getItemsFailed() == 0) {
|
||||
AlertUtil.showInfo("Completado", label + ": " + result.getItemsProcessed() + " procesados.");
|
||||
} else {
|
||||
AlertUtil.showWarn("Partial Success",
|
||||
"Processed: " + processed + ", Failed: " + failed);
|
||||
AlertUtil.showWarn("Completado con errores",
|
||||
result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
|
||||
}
|
||||
|
||||
loadProducts(null);
|
||||
loadProducts();
|
||||
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();
|
||||
btn.setDisable(false);
|
||||
AlertUtil.showError("Error de sincronización", e.getMessage());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() ->
|
||||
AlertUtil.showError("Sync Error", e.getMessage())
|
||||
);
|
||||
}
|
||||
});
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
private void handleLogout(Stage window) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("Cerrar sesión");
|
||||
alert.setHeaderText("¿Cerrar sesión?");
|
||||
alert.setContentText("Volverás a la pantalla de inicio de sesión.");
|
||||
if (alert.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
|
||||
|
||||
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
||||
sessionManager.logout();
|
||||
mainWindow.close();
|
||||
window.close();
|
||||
new LoginStage(sessionManager, primaryStage).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> "";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue