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>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- JavaFX - TODO los módulos necesarios -->
|
<!-- JavaFX -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-controls</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
<version>${javafx.version}</version>
|
<version>${javafx.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.openjfx</groupId>
|
|
||||||
<artifactId>javafx-fxml</artifactId>
|
|
||||||
<version>${javafx.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-graphics</artifactId>
|
<artifactId>javafx-graphics</artifactId>
|
||||||
|
|
@ -41,13 +36,8 @@
|
||||||
<artifactId>javafx-base</artifactId>
|
<artifactId>javafx-base</artifactId>
|
||||||
<version>${javafx.version}</version>
|
<version>${javafx.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.openjfx</groupId>
|
|
||||||
<artifactId>javafx-swing</artifactId>
|
|
||||||
<version>${javafx.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- JSON parsing (Jackson) -->
|
<!-- JSON (Jackson) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
|
@ -107,7 +97,7 @@
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<!-- JavaFX Maven Plugin - ESENCIAL -->
|
<!-- Ejecutar con: mvn javafx:run -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-maven-plugin</artifactId>
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
|
|
@ -116,30 +106,6 @@
|
||||||
<mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass>
|
<mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<!-- JAR ejecutable para producción -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.5.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
|
||||||
<shadedClassifierName>fat</shadedClassifierName>
|
|
||||||
<transformers>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<mainClass>com.teterialosjuanjos.tfg.javafxclient.JavaFxClientApplication</mainClass>
|
|
||||||
</transformer>
|
|
||||||
</transformers>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package com.teterialosjuanjos.tfg.javafxclient.api;
|
package com.teterialosjuanjos.tfg.javafxclient.api;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.teterialosjuanjos.tfg.javafxclient.model.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -10,12 +11,12 @@ import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cliente HTTP tipado para el sync-service en Railway.
|
* Cliente HTTP tipado para el sync-service.
|
||||||
*
|
* Usa java.net.http.HttpClient (Java 11+) con Basic Auth.
|
||||||
* <p>Maneja autenticación Basic Auth para todos los endpoints.</p>
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SyncServiceClient {
|
public class SyncServiceClient {
|
||||||
|
|
@ -26,133 +27,81 @@ public class SyncServiceClient {
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public SyncServiceClient(String baseUrl, String username, String password) {
|
public SyncServiceClient(String baseUrl, String username, String password) {
|
||||||
this.baseUrl = baseUrl.replaceAll("/$", ""); // Remove trailing slash
|
this.baseUrl = baseUrl.replaceAll("/$", "");
|
||||||
this.httpClient = HttpClient.newHttpClient();
|
this.httpClient = HttpClient.newHttpClient();
|
||||||
this.authHeader = buildBasicAuthHeader(username, password);
|
this.authHeader = buildBasicAuthHeader(username, password);
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper()
|
||||||
this.objectMapper.registerModule(new JavaTimeModule());
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Health Check ────────────────────────────────────────────────────────
|
// ── Health ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public Map<String, Object> checkHealth() throws IOException, InterruptedException {
|
/** Lanza IOException si el servidor no responde 200. */
|
||||||
String url = baseUrl + "/actuator/health";
|
public void checkHealth() throws IOException, InterruptedException {
|
||||||
return getJson(url, Map.class);
|
HttpResponse<String> response = sendRequest("GET", baseUrl + "/actuator/health", null);
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new IOException("Health check failed: HTTP " + response.statusCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Logs ─────────────────────────────────────────────────────────────────
|
// ── Logs ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
public List<SyncLogResponse> getLogs(String type) throws IOException, InterruptedException {
|
||||||
* Obtiene todos los logs de sincronización.
|
String url = baseUrl + "/api/logs" + (type != null && !type.isEmpty() ? "?type=" + type : "");
|
||||||
*
|
return getList(url, SyncLogResponse.class);
|
||||||
* @param type opcional, filtra por tipo (PRODUCT_PUSH, STOCK_PUSH, ORDER_PULL)
|
|
||||||
*/
|
|
||||||
public List<Map<String, Object>> getLogs(String type) throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/logs";
|
|
||||||
if (type != null && !type.isEmpty()) {
|
|
||||||
url += "?type=" + type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse<String> response = sendRequest("GET", url, null);
|
public SyncLogResponse getLog(Long id) throws IOException, InterruptedException {
|
||||||
if (response.statusCode() == 200) {
|
return get(baseUrl + "/api/logs/" + id, SyncLogResponse.class);
|
||||||
return objectMapper.readValue(
|
|
||||||
response.body(),
|
|
||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new IOException("Failed to fetch logs: " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene un log específico por ID.
|
|
||||||
*/
|
|
||||||
public Map<String, Object> getLog(Long id) throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/logs/" + id;
|
|
||||||
return getJson(url, Map.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Mappings ─────────────────────────────────────────────────────────────
|
// ── Mappings ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
public List<ProductMappingResponse> getProductMappings(String status) throws IOException, InterruptedException {
|
||||||
* Obtiene todos los mapeos de productos.
|
String url = baseUrl + "/api/mappings/products" +
|
||||||
*
|
(status != null && !status.isEmpty() ? "?status=" + status : "");
|
||||||
* @param status opcional, filtra por status (PENDING, SYNCED, ERROR)
|
return getList(url, ProductMappingResponse.class);
|
||||||
*/
|
|
||||||
public List<Map<String, Object>> getProductMappings(String status)
|
|
||||||
throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/mappings/products";
|
|
||||||
if (status != null && !status.isEmpty()) {
|
|
||||||
url += "?status=" + status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse<String> response = sendRequest("GET", url, null);
|
public List<OrderMappingResponse> getOrderMappings() throws IOException, InterruptedException {
|
||||||
if (response.statusCode() == 200) {
|
return getList(baseUrl + "/api/mappings/orders", OrderMappingResponse.class);
|
||||||
return objectMapper.readValue(
|
|
||||||
response.body(),
|
|
||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new IOException("Failed to fetch product mappings: " + response.statusCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ── Sync Triggers ────────────────────────────────────────────────────────
|
||||||
* Obtiene todos los mapeos de pedidos.
|
|
||||||
*/
|
|
||||||
public List<Map<String, Object>> getOrderMappings() throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/mappings/orders";
|
|
||||||
|
|
||||||
HttpResponse<String> response = sendRequest("GET", url, null);
|
public SyncTriggerResponse triggerProductSync() throws IOException, InterruptedException {
|
||||||
if (response.statusCode() == 200) {
|
return post(baseUrl + "/api/sync/products", SyncTriggerResponse.class);
|
||||||
return objectMapper.readValue(
|
|
||||||
response.body(),
|
|
||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new IOException("Failed to fetch order mappings: " + response.statusCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sync Triggers ───────────────────────────────────────────────────────
|
public SyncTriggerResponse triggerStockSync() throws IOException, InterruptedException {
|
||||||
|
return post(baseUrl + "/api/sync/stock", SyncTriggerResponse.class);
|
||||||
/**
|
|
||||||
* Dispara sincronización de productos.
|
|
||||||
*/
|
|
||||||
public Map<String, Object> triggerProductSync() throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/sync/products";
|
|
||||||
return postJson(url, new HashMap<>(), Map.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public SyncTriggerResponse triggerOrderSync() throws IOException, InterruptedException {
|
||||||
* Dispara sincronización de stock.
|
return post(baseUrl + "/api/sync/orders", SyncTriggerResponse.class);
|
||||||
*/
|
|
||||||
public Map<String, Object> triggerStockSync() throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/sync/stock";
|
|
||||||
return postJson(url, new HashMap<>(), Map.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispara sincronización de pedidos.
|
|
||||||
*/
|
|
||||||
public Map<String, Object> triggerOrderSync() throws IOException, InterruptedException {
|
|
||||||
String url = baseUrl + "/api/sync/orders";
|
|
||||||
return postJson(url, new HashMap<>(), Map.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private <T> T getJson(String url, Class<T> responseType) throws IOException, InterruptedException {
|
private <T> T get(String url, Class<T> type) throws IOException, InterruptedException {
|
||||||
|
HttpResponse<String> response = sendRequest("GET", url, null);
|
||||||
|
if (response.statusCode() == 200) return objectMapper.readValue(response.body(), type);
|
||||||
|
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> List<T> getList(String url, Class<T> elementType) throws IOException, InterruptedException {
|
||||||
HttpResponse<String> response = sendRequest("GET", url, null);
|
HttpResponse<String> response = sendRequest("GET", url, null);
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
return objectMapper.readValue(response.body(), responseType);
|
return objectMapper.readValue(response.body(),
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, elementType));
|
||||||
}
|
}
|
||||||
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
|
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T postJson(String url, Object body, Class<T> responseType)
|
private <T> T post(String url, Class<T> type) throws IOException, InterruptedException {
|
||||||
throws IOException, InterruptedException {
|
HttpResponse<String> response = sendRequest("POST", url, "{}");
|
||||||
String jsonBody = objectMapper.writeValueAsString(body);
|
|
||||||
HttpResponse<String> response = sendRequest("POST", url, jsonBody);
|
|
||||||
if (response.statusCode() == 200 || response.statusCode() == 201) {
|
if (response.statusCode() == 200 || response.statusCode() == 201) {
|
||||||
return objectMapper.readValue(response.body(), responseType);
|
return objectMapper.readValue(response.body(), type);
|
||||||
}
|
}
|
||||||
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
|
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
|
||||||
}
|
}
|
||||||
|
|
@ -166,9 +115,7 @@ public class SyncServiceClient {
|
||||||
.method(method, body != null
|
.method(method, body != null
|
||||||
? HttpRequest.BodyPublishers.ofString(body)
|
? HttpRequest.BodyPublishers.ofString(body)
|
||||||
: HttpRequest.BodyPublishers.noBody());
|
: HttpRequest.BodyPublishers.noBody());
|
||||||
|
return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||||
HttpRequest request = builder.build();
|
|
||||||
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildBasicAuthHeader(String username, String password) {
|
private String buildBasicAuthHeader(String username, String password) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package com.teterialosjuanjos.tfg.javafxclient.model;
|
||||||
|
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.util.SettingsStore;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gestiona la sesión actual: credenciales, cliente HTTP, estado de autenticación.
|
* Gestiona la sesión actual: credenciales, cliente HTTP y persistencia de ajustes.
|
||||||
|
* Carga los ajustes guardados en el constructor.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
|
|
@ -20,16 +22,16 @@ public class SessionManager {
|
||||||
private boolean authenticated;
|
private boolean authenticated;
|
||||||
|
|
||||||
public SessionManager() {
|
public SessionManager() {
|
||||||
|
AppSettings saved = SettingsStore.load();
|
||||||
|
this.baseUrl = saved.getBaseUrl();
|
||||||
|
this.username = saved.getUsername();
|
||||||
|
this.password = saved.getPassword();
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
// Default: Railway production
|
|
||||||
this.baseUrl = "https://proyectointermodular-production-a9c3.up.railway.app";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intenta autenticarse contra el sync-service.
|
* Autentica contra el sync-service verificando el health endpoint.
|
||||||
* Si es exitoso, inicializa el cliente HTTP.
|
* Si tiene éxito, guarda los ajustes en SettingsStore.
|
||||||
*
|
|
||||||
* @return true si auth fue exitosa
|
|
||||||
*/
|
*/
|
||||||
public boolean authenticate(String baseUrl, String username, String password) {
|
public boolean authenticate(String baseUrl, String username, String password) {
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
|
|
@ -38,9 +40,9 @@ public class SessionManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
|
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
|
||||||
// Prueba conectividad
|
|
||||||
syncServiceClient.checkHealth();
|
syncServiceClient.checkHealth();
|
||||||
this.authenticated = true;
|
this.authenticated = true;
|
||||||
|
SettingsStore.save(new AppSettings(baseUrl, username, password));
|
||||||
log.info("Authentication successful for user: {}", username);
|
log.info("Authentication successful for user: {}", username);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -50,6 +52,30 @@ public class SessionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualiza los ajustes y reinicializa el cliente HTTP sin health check.
|
||||||
|
* Usado desde la pantalla de ajustes para cambiar la configuración en caliente.
|
||||||
|
*/
|
||||||
|
public void updateSettings(String baseUrl, String username, String password) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.syncServiceClient = new SyncServiceClient(baseUrl, username, password);
|
||||||
|
SettingsStore.save(new AppSettings(baseUrl, username, password));
|
||||||
|
log.info("Settings updated, new base URL: {}", baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Comprueba conectividad con los ajustes dados sin modificar el estado. */
|
||||||
|
public boolean testConnection(String baseUrl, String username, String password) {
|
||||||
|
try {
|
||||||
|
new SyncServiceClient(baseUrl, username, password).checkHealth();
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Connection test failed: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void logout() {
|
public void logout() {
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
this.syncServiceClient = null;
|
this.syncServiceClient = null;
|
||||||
|
|
|
||||||
|
|
@ -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.api.SyncServiceClient;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
|
||||||
import javafx.animation.Animation;
|
import javafx.animation.Animation;
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.util.HashMap;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard con estado de sincronizaciones y estadísticas.
|
* Tab Dashboard: botón "Sincronizar todo", 3 tarjetas de sync con datos del último log.
|
||||||
|
* Equivalente a DashboardScreen de la app Android.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DashboardController {
|
public class DashboardController {
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
private final SessionManager sessionManager;
|
||||||
private Label lastProductSyncLabel;
|
|
||||||
private Label lastStockSyncLabel;
|
private final Map<String, Label> lastSyncInfoLabels = new HashMap<>();
|
||||||
private Label lastOrderSyncLabel;
|
private Button syncAllButton;
|
||||||
private Label successRateLabel;
|
private Label syncAllStatusLabel;
|
||||||
private Label totalSyncsLabel;
|
|
||||||
|
|
||||||
public DashboardController(SessionManager sessionManager) {
|
public DashboardController(SessionManager sessionManager) {
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
|
|
@ -43,190 +40,222 @@ public class DashboardController {
|
||||||
|
|
||||||
public VBox createView() {
|
public VBox createView() {
|
||||||
VBox root = new VBox(20);
|
VBox root = new VBox(20);
|
||||||
root.setPadding(new Insets(20));
|
root.setPadding(new Insets(24));
|
||||||
root.setStyle("-fx-background-color: #fafafa;");
|
root.setStyle("-fx-background-color: #FAFAFA;");
|
||||||
|
|
||||||
// Title
|
// Header
|
||||||
Label title = new Label("Synchronization Dashboard");
|
Label appTitle = new Label("Sync Manager");
|
||||||
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
|
appTitle.setStyle("-fx-font-size: 22; -fx-font-weight: bold; -fx-text-fill: #1565C0;");
|
||||||
|
Label appSubtitle = new Label("Dolibarr · PrestaShop");
|
||||||
|
appSubtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
|
||||||
|
VBox headerBox = new VBox(2, appTitle, appSubtitle);
|
||||||
|
|
||||||
// Sync stats cards
|
// Sync All button
|
||||||
GridPane statsGrid = createStatsGrid();
|
syncAllButton = new Button("Sincronizar todo");
|
||||||
|
syncAllButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
// Action buttons
|
syncAllButton.setPrefHeight(46);
|
||||||
HBox actionsBox = createActionsBox();
|
syncAllButton.setStyle(
|
||||||
|
"-fx-background-color: #1565C0; -fx-text-fill: white; -fx-font-size: 14; " +
|
||||||
root.getChildren().addAll(
|
"-fx-font-weight: bold; -fx-background-radius: 10; -fx-cursor: hand;"
|
||||||
title,
|
|
||||||
statsGrid,
|
|
||||||
actionsBox,
|
|
||||||
new Label() // spacer
|
|
||||||
);
|
);
|
||||||
|
syncAllButton.setOnAction(e -> handleSyncAll());
|
||||||
|
|
||||||
// Auto-refresh cada 10 segundos
|
syncAllStatusLabel = new Label("");
|
||||||
startAutoRefresh();
|
syncAllStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565C0;");
|
||||||
|
syncAllStatusLabel.setVisible(false);
|
||||||
|
|
||||||
|
// 3 sync cards
|
||||||
|
HBox cardsBox = new HBox(16);
|
||||||
|
VBox productCard = createSyncCard("📦", "Productos", "Dolibarr → PrestaShop", "PRODUCT_PUSH", "#1565C0");
|
||||||
|
VBox stockCard = createSyncCard("📊", "Stock", "Dolibarr → PrestaShop", "STOCK_PUSH", "#2E7D32");
|
||||||
|
VBox ordersCard = createSyncCard("🛒", "Pedidos", "PrestaShop → Dolibarr", "ORDER_PULL", "#E65100");
|
||||||
|
|
||||||
|
HBox.setHgrow(productCard, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(stockCard, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(ordersCard, Priority.ALWAYS);
|
||||||
|
cardsBox.getChildren().addAll(productCard, stockCard, ordersCard);
|
||||||
|
|
||||||
|
root.getChildren().addAll(headerBox, syncAllButton, syncAllStatusLabel, cardsBox);
|
||||||
|
|
||||||
// Carga inicial
|
|
||||||
refreshDashboard();
|
refreshDashboard();
|
||||||
|
startAutoRefresh();
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GridPane createStatsGrid() {
|
private VBox createSyncCard(String icon, String title, String direction,
|
||||||
GridPane grid = new GridPane();
|
String type, String color) {
|
||||||
grid.setHgap(15);
|
|
||||||
grid.setVgap(15);
|
|
||||||
grid.setPadding(new Insets(10));
|
|
||||||
grid.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0; -fx-border-radius: 4;");
|
|
||||||
|
|
||||||
// Card: Last Product Sync
|
|
||||||
VBox productCard = createStatCard(
|
|
||||||
"📦 Last Product Sync",
|
|
||||||
"Never",
|
|
||||||
"#1976d2"
|
|
||||||
);
|
|
||||||
lastProductSyncLabel = (Label) productCard.getChildren().get(1);
|
|
||||||
GridPane.setConstraints(productCard, 0, 0);
|
|
||||||
|
|
||||||
// Card: Last Stock Sync
|
|
||||||
VBox stockCard = createStatCard(
|
|
||||||
"📊 Last Stock Sync",
|
|
||||||
"Never",
|
|
||||||
"#388e3c"
|
|
||||||
);
|
|
||||||
lastStockSyncLabel = (Label) stockCard.getChildren().get(1);
|
|
||||||
GridPane.setConstraints(stockCard, 1, 0);
|
|
||||||
|
|
||||||
// Card: Last Order Sync
|
|
||||||
VBox orderCard = createStatCard(
|
|
||||||
"🛒 Last Order Sync",
|
|
||||||
"Never",
|
|
||||||
"#f57c00"
|
|
||||||
);
|
|
||||||
lastOrderSyncLabel = (Label) orderCard.getChildren().get(1);
|
|
||||||
GridPane.setConstraints(orderCard, 2, 0);
|
|
||||||
|
|
||||||
// Card: Total Syncs
|
|
||||||
VBox totalCard = createStatCard(
|
|
||||||
"📈 Total Syncs",
|
|
||||||
"0",
|
|
||||||
"#6a1b9a"
|
|
||||||
);
|
|
||||||
totalSyncsLabel = (Label) totalCard.getChildren().get(1);
|
|
||||||
GridPane.setConstraints(totalCard, 0, 1);
|
|
||||||
|
|
||||||
// Card: Success Rate
|
|
||||||
VBox rateCard = createStatCard(
|
|
||||||
"✓ Success Rate",
|
|
||||||
"0%",
|
|
||||||
"#00796b"
|
|
||||||
);
|
|
||||||
successRateLabel = (Label) rateCard.getChildren().get(1);
|
|
||||||
GridPane.setConstraints(rateCard, 1, 1);
|
|
||||||
|
|
||||||
grid.getChildren().addAll(productCard, stockCard, orderCard, totalCard, rateCard);
|
|
||||||
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VBox createStatCard(String title, String value, String color) {
|
|
||||||
VBox card = new VBox(10);
|
VBox card = new VBox(10);
|
||||||
card.setPadding(new Insets(15));
|
card.setPadding(new Insets(16));
|
||||||
card.setStyle(
|
card.setStyle(
|
||||||
"-fx-background-color: white; " +
|
"-fx-background-color: white;" +
|
||||||
"-fx-border-color: " + color + "; " +
|
"-fx-background-radius: 12;" +
|
||||||
"-fx-border-width: 3 0 0 0; " +
|
"-fx-border-radius: 12;" +
|
||||||
"-fx-border-radius: 0;"
|
"-fx-border-color: #E0E0E0 #E0E0E0 #E0E0E0 " + color + ";" +
|
||||||
|
"-fx-border-width: 1 1 1 4;" +
|
||||||
|
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.06), 6, 0, 0, 2);"
|
||||||
);
|
);
|
||||||
card.setMinWidth(250);
|
|
||||||
card.setAlignment(Pos.TOP_LEFT);
|
|
||||||
|
|
||||||
Label titleLabel = new Label(title);
|
// Title row + button
|
||||||
titleLabel.setStyle("-fx-font-size: 13; -fx-font-weight: bold; -fx-text-fill: " + color + ";");
|
HBox headerRow = new HBox(10);
|
||||||
|
headerRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
Label valueLabel = new Label(value);
|
Label titleLabel = new Label(icon + " " + title);
|
||||||
valueLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold; -fx-text-fill: #333333;");
|
titleLabel.setStyle("-fx-font-size: 14; -fx-font-weight: bold; -fx-text-fill: " + color + ";");
|
||||||
|
|
||||||
card.getChildren().addAll(titleLabel, valueLabel);
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button syncBtn = new Button("Sincronizar");
|
||||||
|
syncBtn.setStyle(
|
||||||
|
"-fx-background-color: " + color + "; -fx-text-fill: white; " +
|
||||||
|
"-fx-background-radius: 8; -fx-padding: 6 14; -fx-font-size: 11; -fx-cursor: hand;"
|
||||||
|
);
|
||||||
|
syncBtn.setOnAction(e -> handleSingleSync(type, syncBtn));
|
||||||
|
|
||||||
|
headerRow.getChildren().addAll(titleLabel, spacer, syncBtn);
|
||||||
|
|
||||||
|
Label dirLabel = new Label(direction);
|
||||||
|
dirLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #9E9E9E;");
|
||||||
|
|
||||||
|
Label lastSyncLabel = new Label("Sin sincronizaciones previas");
|
||||||
|
lastSyncLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #BDBDBD;");
|
||||||
|
lastSyncInfoLabels.put(type, lastSyncLabel);
|
||||||
|
|
||||||
|
card.getChildren().addAll(headerRow, dirLabel, lastSyncLabel);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createActionsBox() {
|
|
||||||
HBox box = new HBox(10);
|
|
||||||
box.setPadding(new Insets(15));
|
|
||||||
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
|
|
||||||
Label label = new Label("Quick Actions:");
|
|
||||||
label.setStyle("-fx-font-weight: bold;");
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
javafx.scene.control.Button refreshButton = new javafx.scene.control.Button("🔄 Refresh Now");
|
|
||||||
refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;");
|
|
||||||
refreshButton.setOnAction(e -> refreshDashboard());
|
|
||||||
|
|
||||||
box.getChildren().addAll(label, refreshButton);
|
|
||||||
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshDashboard() {
|
public void refreshDashboard() {
|
||||||
Thread thread = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
List<Map<String, Object>> logs = client.getLogs(null);
|
List<SyncLogResponse> logs = client.getLogs(null);
|
||||||
|
Platform.runLater(() -> updateCards(logs));
|
||||||
Platform.runLater(() -> updateDashboard(logs));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error refreshing dashboard: {}", e.getMessage());
|
log.error("Dashboard refresh failed: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.setDaemon(true);
|
t.setDaemon(true);
|
||||||
thread.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDashboard(List<Map<String, Object>> logs) {
|
private void updateCards(List<SyncLogResponse> logs) {
|
||||||
if (logs.isEmpty()) {
|
// Toma el más reciente de cada tipo (el primero encontrado en la lista, que viene newest-first)
|
||||||
totalSyncsLabel.setText("0");
|
Map<String, SyncLogResponse> latestPerType = new HashMap<>();
|
||||||
successRateLabel.setText("0%");
|
for (SyncLogResponse entry : logs) {
|
||||||
return;
|
if (entry.getSyncType() != null) {
|
||||||
}
|
latestPerType.putIfAbsent(entry.getSyncType(), entry);
|
||||||
|
|
||||||
totalSyncsLabel.setText(String.valueOf(logs.size()));
|
|
||||||
|
|
||||||
// Busca último sync de cada tipo
|
|
||||||
for (Map<String, Object> log : logs) {
|
|
||||||
String syncType = (String) log.get("syncType");
|
|
||||||
Instant startedAt = Instant.parse((String) log.get("startedAt"));
|
|
||||||
String timeStr = formatInstant(startedAt);
|
|
||||||
|
|
||||||
switch (syncType) {
|
|
||||||
case "PRODUCT_PUSH" -> lastProductSyncLabel.setText(timeStr);
|
|
||||||
case "STOCK_PUSH" -> lastStockSyncLabel.setText(timeStr);
|
|
||||||
case "ORDER_PULL" -> lastOrderSyncLabel.setText(timeStr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success rate
|
latestPerType.forEach((type, entry) -> {
|
||||||
long successCount = logs.stream()
|
Label label = lastSyncInfoLabels.get(type);
|
||||||
.filter(l -> ((Number) l.get("itemsFailed")).longValue() == 0)
|
if (label == null) return;
|
||||||
.count();
|
|
||||||
long successRate = (successCount * 100) / logs.size();
|
String time = DateUtils.formatRelative(entry.getStartedAt());
|
||||||
successRateLabel.setText(successRate + "%");
|
int processed = entry.getItemsProcessed() != null ? entry.getItemsProcessed() : 0;
|
||||||
|
int failed = entry.getItemsFailed() != null ? entry.getItemsFailed() : 0;
|
||||||
|
String text = time + " · " + processed + " procesados";
|
||||||
|
if (failed > 0) text += ", " + failed + " fallidos";
|
||||||
|
|
||||||
|
label.setText(text);
|
||||||
|
label.setStyle("-fx-font-size: 11; -fx-text-fill: " + (failed > 0 ? "#C62828" : "#757575") + ";");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSingleSync(String type, Button btn) {
|
||||||
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
|
confirm.setTitle("Confirmar sincronización");
|
||||||
|
confirm.setHeaderText("¿Ejecutar sincronización de " + DateUtils.syncTypeLabel(type) + "?");
|
||||||
|
confirm.setContentText(DateUtils.syncTypeDirection(type));
|
||||||
|
if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
|
||||||
|
|
||||||
|
btn.setDisable(true);
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
|
SyncTriggerResponse result = switch (type) {
|
||||||
|
case "PRODUCT_PUSH" -> client.triggerProductSync();
|
||||||
|
case "STOCK_PUSH" -> client.triggerStockSync();
|
||||||
|
case "ORDER_PULL" -> client.triggerOrderSync();
|
||||||
|
default -> throw new IllegalArgumentException("Unknown type: " + type);
|
||||||
|
};
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
btn.setDisable(false);
|
||||||
|
if (result.getItemsFailed() == 0) {
|
||||||
|
AlertUtil.showInfo("Completado",
|
||||||
|
DateUtils.syncTypeLabel(type) + ": " + result.getItemsProcessed() + " procesados.");
|
||||||
|
} else {
|
||||||
|
AlertUtil.showWarn("Completado con errores",
|
||||||
|
result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
|
||||||
|
}
|
||||||
|
refreshDashboard();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
btn.setDisable(false);
|
||||||
|
AlertUtil.showError("Error de sincronización", e.getMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSyncAll() {
|
||||||
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
|
confirm.setTitle("Sincronizar todo");
|
||||||
|
confirm.setHeaderText("Ejecutará productos, stock y pedidos en secuencia.");
|
||||||
|
confirm.setContentText("¿Continuar?");
|
||||||
|
if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
|
||||||
|
|
||||||
|
syncAllButton.setDisable(true);
|
||||||
|
syncAllStatusLabel.setVisible(true);
|
||||||
|
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
|
|
||||||
|
Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando productos (1/3)..."));
|
||||||
|
SyncTriggerResponse r1 = client.triggerProductSync();
|
||||||
|
|
||||||
|
Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando stock (2/3)..."));
|
||||||
|
SyncTriggerResponse r2 = client.triggerStockSync();
|
||||||
|
|
||||||
|
Platform.runLater(() -> syncAllStatusLabel.setText("Sincronizando pedidos (3/3)..."));
|
||||||
|
SyncTriggerResponse r3 = client.triggerOrderSync();
|
||||||
|
|
||||||
|
int totalProcessed = r1.getItemsProcessed() + r2.getItemsProcessed() + r3.getItemsProcessed();
|
||||||
|
int totalFailed = r1.getItemsFailed() + r2.getItemsFailed() + r3.getItemsFailed();
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
syncAllButton.setDisable(false);
|
||||||
|
syncAllStatusLabel.setVisible(false);
|
||||||
|
if (totalFailed == 0) {
|
||||||
|
AlertUtil.showInfo("Sincronización completada",
|
||||||
|
"Total procesados: " + totalProcessed);
|
||||||
|
} else {
|
||||||
|
AlertUtil.showWarn("Completado con errores",
|
||||||
|
"Procesados: " + totalProcessed + ", Fallidos: " + totalFailed);
|
||||||
|
}
|
||||||
|
refreshDashboard();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
syncAllButton.setDisable(false);
|
||||||
|
syncAllStatusLabel.setVisible(false);
|
||||||
|
AlertUtil.showError("Error en sincronización", e.getMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startAutoRefresh() {
|
private void startAutoRefresh() {
|
||||||
Timeline timeline = new Timeline(
|
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(30), e -> refreshDashboard()));
|
||||||
new KeyFrame(Duration.seconds(10), e -> refreshDashboard())
|
|
||||||
);
|
|
||||||
timeline.setCycleCount(Animation.INDEFINITE);
|
timeline.setCycleCount(Animation.INDEFINITE);
|
||||||
timeline.play();
|
timeline.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatInstant(Instant instant) {
|
|
||||||
LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
|
|
||||||
return ldt.format(formatter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,221 +2,187 @@ package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
|
||||||
|
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.SyncLogResponse;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.ui.dialogs.LogDetailDialog;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab para logs de sincronización.
|
* Tab de historial de sincronizaciones.
|
||||||
|
* Click en una fila abre LogDetailDialog.
|
||||||
|
* Equivalente a LogsScreen de la app Android.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class LogsController {
|
public class LogsController {
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
private final SessionManager sessionManager;
|
||||||
private TableView<Map<String, Object>> logsTable;
|
private TableView<SyncLogResponse> logsTable;
|
||||||
private TextArea errorDetailsArea;
|
|
||||||
|
|
||||||
public LogsController(SessionManager sessionManager) {
|
public LogsController(SessionManager sessionManager) {
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VBox createView() {
|
public VBox createView() {
|
||||||
VBox root = new VBox(15);
|
VBox root = new VBox(12);
|
||||||
root.setPadding(new Insets(20));
|
root.setPadding(new Insets(20));
|
||||||
root.setStyle("-fx-background-color: #fafafa;");
|
root.setStyle("-fx-background-color: #FAFAFA;");
|
||||||
|
|
||||||
// Title
|
// Header
|
||||||
Label title = new Label("Synchronization Logs");
|
HBox headerRow = new HBox(10);
|
||||||
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
|
headerRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
VBox titleBox = new VBox(2);
|
||||||
|
Label title = new Label("Historial de sincronización");
|
||||||
|
title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
|
||||||
|
Label subtitle = new Label("Registros de cada ejecución");
|
||||||
|
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
|
||||||
|
titleBox.getChildren().addAll(title, subtitle);
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
Button refreshBtn = new Button("🔄 Actualizar");
|
||||||
|
refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 14;");
|
||||||
|
refreshBtn.setOnAction(e -> loadLogs(null));
|
||||||
|
headerRow.getChildren().addAll(titleBox, spacer, refreshBtn);
|
||||||
|
|
||||||
|
// Filter chips
|
||||||
|
HBox chipsBox = createFilterChips();
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
logsTable = createLogsTable();
|
logsTable = createTable();
|
||||||
VBox.setVgrow(logsTable, javafx.scene.layout.Priority.ALWAYS);
|
VBox.setVgrow(logsTable, Priority.ALWAYS);
|
||||||
logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newVal) -> {
|
|
||||||
if (newVal != null) {
|
// Click row → open detail dialog
|
||||||
showLogDetails(newVal);
|
logsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, log) -> {
|
||||||
|
if (log != null) {
|
||||||
|
new LogDetailDialog(log).show();
|
||||||
|
Platform.runLater(() -> logsTable.getSelectionModel().clearSelection());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Details panel
|
root.getChildren().addAll(headerRow, chipsBox, logsTable);
|
||||||
VBox detailsPanel = createDetailsPanel();
|
|
||||||
|
|
||||||
// Filter & Actions
|
|
||||||
HBox filterBox = createFilterBox();
|
|
||||||
|
|
||||||
root.getChildren().addAll(title, filterBox, logsTable, detailsPanel);
|
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
loadLogs(null);
|
loadLogs(null);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TableView<Map<String, Object>> createLogsTable() {
|
private HBox createFilterChips() {
|
||||||
TableView<Map<String, Object>> table = new TableView<>();
|
ToggleGroup group = new ToggleGroup();
|
||||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
|
|
||||||
table.setStyle("-fx-font-size: 11;");
|
|
||||||
table.setPrefHeight(300);
|
|
||||||
|
|
||||||
// ID column
|
ToggleButton allBtn = chip("Todos", null, group, true);
|
||||||
TableColumn<Map<String, Object>, String> idCol = new TableColumn<>("ID");
|
ToggleButton productsBtn = chip("Productos", "PRODUCT_PUSH", group, false);
|
||||||
idCol.setCellValueFactory(cf -> {
|
ToggleButton stockBtn = chip("Stock", "STOCK_PUSH", group, false);
|
||||||
Object value = cf.getValue().get("id");
|
ToggleButton ordersBtn = chip("Pedidos", "ORDER_PULL", group, false);
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
|
|
||||||
});
|
|
||||||
idCol.setPrefWidth(50);
|
|
||||||
|
|
||||||
// Type column
|
group.selectedToggleProperty().addListener((obs, old, newToggle) -> {
|
||||||
TableColumn<Map<String, Object>, String> typeCol = new TableColumn<>("Type");
|
if (newToggle == null) { allBtn.setSelected(true); return; }
|
||||||
typeCol.setCellValueFactory(cf -> {
|
loadLogs((String) newToggle.getUserData());
|
||||||
String value = (String) cf.getValue().get("syncType");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(value);
|
|
||||||
});
|
|
||||||
typeCol.setPrefWidth(120);
|
|
||||||
|
|
||||||
// Started At column
|
|
||||||
TableColumn<Map<String, Object>, String> startedCol = new TableColumn<>("Started At");
|
|
||||||
startedCol.setCellValueFactory(cf -> {
|
|
||||||
Object value = cf.getValue().get("startedAt");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(
|
|
||||||
value != null ? String.valueOf(value).substring(0, 19) : "—"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
startedCol.setPrefWidth(150);
|
|
||||||
|
|
||||||
// Finished At column
|
|
||||||
TableColumn<Map<String, Object>, String> finishedCol = new TableColumn<>("Finished At");
|
|
||||||
finishedCol.setCellValueFactory(cf -> {
|
|
||||||
Object value = cf.getValue().get("finishedAt");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(
|
|
||||||
value != null ? String.valueOf(value).substring(0, 19) : "—"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
finishedCol.setPrefWidth(150);
|
|
||||||
|
|
||||||
// Processed column
|
|
||||||
TableColumn<Map<String, Object>, String> processedCol = new TableColumn<>("Processed");
|
|
||||||
processedCol.setCellValueFactory(cf -> {
|
|
||||||
Object value = cf.getValue().get("itemsProcessed");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
|
|
||||||
});
|
|
||||||
processedCol.setPrefWidth(80);
|
|
||||||
|
|
||||||
// Failed column
|
|
||||||
TableColumn<Map<String, Object>, String> failedCol = new TableColumn<>("Failed");
|
|
||||||
failedCol.setCellValueFactory(cf -> {
|
|
||||||
Object value = cf.getValue().get("itemsFailed");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
|
|
||||||
});
|
|
||||||
failedCol.setPrefWidth(80);
|
|
||||||
failedCol.setCellFactory(col -> new TableCell<Map<String, Object>, String>() {
|
|
||||||
@Override
|
|
||||||
protected void updateItem(String item, boolean empty) {
|
|
||||||
super.updateItem(item, empty);
|
|
||||||
if (empty || item == null || "0".equals(item)) {
|
|
||||||
setText(item);
|
|
||||||
setStyle("");
|
|
||||||
} else {
|
|
||||||
setText(item);
|
|
||||||
setStyle("-fx-text-fill: #c62828; -fx-font-weight: bold;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
table.getColumns().addAll(idCol, typeCol, startedCol, finishedCol, processedCol, failedCol);
|
HBox box = new HBox(8, allBtn, productsBtn, stockBtn, ordersBtn);
|
||||||
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VBox createDetailsPanel() {
|
|
||||||
VBox panel = new VBox(10);
|
|
||||||
panel.setPadding(new Insets(10));
|
|
||||||
panel.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
|
|
||||||
|
|
||||||
Label label = new Label("Error Details (click a row to see details):");
|
|
||||||
label.setStyle("-fx-font-weight: bold;");
|
|
||||||
|
|
||||||
errorDetailsArea = new TextArea();
|
|
||||||
errorDetailsArea.setEditable(false);
|
|
||||||
errorDetailsArea.setWrapText(true);
|
|
||||||
errorDetailsArea.setPrefHeight(120);
|
|
||||||
errorDetailsArea.setStyle("-fx-font-size: 10; -fx-control-inner-background: #f5f5f5;");
|
|
||||||
|
|
||||||
panel.getChildren().addAll(label, errorDetailsArea);
|
|
||||||
|
|
||||||
return panel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HBox createFilterBox() {
|
|
||||||
HBox box = new HBox(10);
|
|
||||||
box.setPadding(new Insets(10));
|
|
||||||
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
Label filterLabel = new Label("Filter by type:");
|
|
||||||
|
|
||||||
ComboBox<String> typeCombo = new ComboBox<>();
|
|
||||||
typeCombo.setItems(FXCollections.observableArrayList(
|
|
||||||
"All", "PRODUCT_PUSH", "STOCK_PUSH", "ORDER_PULL"
|
|
||||||
));
|
|
||||||
typeCombo.setValue("All");
|
|
||||||
typeCombo.setPrefWidth(140);
|
|
||||||
|
|
||||||
typeCombo.setOnAction(e -> {
|
|
||||||
String selected = typeCombo.getValue();
|
|
||||||
loadLogs("All".equals(selected) ? null : selected);
|
|
||||||
});
|
|
||||||
|
|
||||||
Region spacer = new Region();
|
|
||||||
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
|
||||||
|
|
||||||
Button refreshButton = new Button("🔄 Refresh");
|
|
||||||
refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;");
|
|
||||||
refreshButton.setOnAction(e -> loadLogs(null));
|
|
||||||
|
|
||||||
box.getChildren().addAll(filterLabel, typeCombo, spacer, refreshButton);
|
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadLogs(String type) {
|
private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) {
|
||||||
Thread thread = new Thread(() -> {
|
ToggleButton btn = new ToggleButton(text);
|
||||||
try {
|
btn.setToggleGroup(group);
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
btn.setSelected(selected);
|
||||||
List<Map<String, Object>> logs = client.getLogs(type);
|
btn.setUserData(value);
|
||||||
|
btn.getStyleClass().add("filter-chip");
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(logs);
|
private TableView<SyncLogResponse> createTable() {
|
||||||
Platform.runLater(() -> logsTable.setItems(items));
|
TableView<SyncLogResponse> table = new TableView<>();
|
||||||
} catch (Exception e) {
|
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
|
||||||
log.error("Error loading logs: {}", e.getMessage());
|
table.setStyle("-fx-cursor: hand;");
|
||||||
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load logs: " + e.getMessage()));
|
|
||||||
|
// Status icon column
|
||||||
|
TableColumn<SyncLogResponse, SyncLogResponse> iconCol = new TableColumn<>("");
|
||||||
|
iconCol.setCellValueFactory(cf -> new javafx.beans.property.SimpleObjectProperty<>(cf.getValue()));
|
||||||
|
iconCol.setPrefWidth(36);
|
||||||
|
iconCol.setCellFactory(c -> new TableCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(SyncLogResponse item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null) { setGraphic(null); return; }
|
||||||
|
int failed = item.getItemsFailed() != null ? item.getItemsFailed() : 0;
|
||||||
|
boolean inP = item.getFinishedAt() == null;
|
||||||
|
String icon = inP ? "⏱" : (failed > 0 ? "✗" : "✓");
|
||||||
|
String color = inP ? "#1565C0" : (failed > 0 ? "#C62828" : "#2E7D32");
|
||||||
|
Label lbl = new Label(icon);
|
||||||
|
lbl.setStyle("-fx-font-size: 14; -fx-text-fill: " + color + ";");
|
||||||
|
setGraphic(lbl);
|
||||||
|
setText(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.setDaemon(true);
|
|
||||||
thread.start();
|
// Type
|
||||||
|
TableColumn<SyncLogResponse, String> typeCol = new TableColumn<>("Tipo");
|
||||||
|
typeCol.setCellValueFactory(cf -> new SimpleStringProperty(DateUtils.syncTypeLabel(cf.getValue().getSyncType())));
|
||||||
|
typeCol.setPrefWidth(100);
|
||||||
|
|
||||||
|
// Started (relative)
|
||||||
|
TableColumn<SyncLogResponse, String> startedCol = new TableColumn<>("Inicio");
|
||||||
|
startedCol.setCellValueFactory(cf -> new SimpleStringProperty(DateUtils.formatRelative(cf.getValue().getStartedAt())));
|
||||||
|
startedCol.setPrefWidth(130);
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
TableColumn<SyncLogResponse, String> durationCol = new TableColumn<>("Duración");
|
||||||
|
durationCol.setCellValueFactory(cf ->
|
||||||
|
new SimpleStringProperty(DateUtils.formatDuration(cf.getValue().getStartedAt(), cf.getValue().getFinishedAt())));
|
||||||
|
durationCol.setPrefWidth(80);
|
||||||
|
|
||||||
|
// Processed
|
||||||
|
TableColumn<SyncLogResponse, String> processedCol = new TableColumn<>("Procesados");
|
||||||
|
processedCol.setCellValueFactory(cf -> {
|
||||||
|
Integer v = cf.getValue().getItemsProcessed();
|
||||||
|
return new SimpleStringProperty(v != null ? String.valueOf(v) : "—");
|
||||||
|
});
|
||||||
|
processedCol.setPrefWidth(90);
|
||||||
|
|
||||||
|
// Failed (red if > 0)
|
||||||
|
TableColumn<SyncLogResponse, String> failedCol = new TableColumn<>("Fallidos");
|
||||||
|
failedCol.setCellValueFactory(cf -> {
|
||||||
|
Integer v = cf.getValue().getItemsFailed();
|
||||||
|
return new SimpleStringProperty(v != null ? String.valueOf(v) : "—");
|
||||||
|
});
|
||||||
|
failedCol.setPrefWidth(80);
|
||||||
|
failedCol.setCellFactory(c -> new TableCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null) { setText(null); setStyle(""); return; }
|
||||||
|
setText(item);
|
||||||
|
boolean isFailed = !"—".equals(item) && !"0".equals(item);
|
||||||
|
setStyle(isFailed ? "-fx-text-fill: #C62828; -fx-font-weight: bold;" : "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
table.getColumns().addAll(iconCol, typeCol, startedCol, durationCol, processedCol, failedCol);
|
||||||
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLogDetails(Map<String, Object> log) {
|
private void loadLogs(String type) {
|
||||||
String errorDetails = (String) log.get("errorDetails");
|
Thread t = new Thread(() -> {
|
||||||
if (errorDetails != null && !errorDetails.isEmpty()) {
|
try {
|
||||||
errorDetailsArea.setText(errorDetails);
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
} else {
|
List<SyncLogResponse> logs = client.getLogs(type);
|
||||||
int processed = ((Number) log.get("itemsProcessed")).intValue();
|
Platform.runLater(() -> logsTable.setItems(FXCollections.observableArrayList(logs)));
|
||||||
int failed = ((Number) log.get("itemsFailed")).intValue();
|
} catch (Exception e) {
|
||||||
errorDetailsArea.setText("✓ Sync completed successfully\n" +
|
log.error("Error loading logs: {}", e.getMessage());
|
||||||
"Processed: " + processed + "\n" +
|
Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los logs: " + e.getMessage()));
|
||||||
"Failed: " + failed);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
|
package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
|
||||||
|
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.OrderMappingResponse;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab para pedidos: ver mapeos de PrestaShop → Dolibarr y disparar sync.
|
* Tab de mapeos de pedidos: PrestaShop → Dolibarr.
|
||||||
|
* Equivalente a MappingsScreen (tab Pedidos) de la app Android.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class OrdersController {
|
public class OrdersController {
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
private final SessionManager sessionManager;
|
||||||
private final DashboardController dashboardController;
|
private final DashboardController dashboardController;
|
||||||
private TableView<Map<String, Object>> ordersTable;
|
private TableView<OrderMappingResponse> ordersTable;
|
||||||
|
|
||||||
public OrdersController(SessionManager sessionManager, DashboardController dashboardController) {
|
public OrdersController(SessionManager sessionManager, DashboardController dashboardController) {
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
|
|
@ -32,194 +34,145 @@ public class OrdersController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public VBox createView() {
|
public VBox createView() {
|
||||||
VBox root = new VBox(15);
|
VBox root = new VBox(12);
|
||||||
root.setPadding(new Insets(20));
|
root.setPadding(new Insets(20));
|
||||||
root.setStyle("-fx-background-color: #fafafa;");
|
root.setStyle("-fx-background-color: #FAFAFA;");
|
||||||
|
|
||||||
// Title
|
Label title = new Label("Mappings");
|
||||||
Label title = new Label("Order Mappings (PrestaShop → Dolibarr)");
|
title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
|
||||||
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
|
Label subtitle = new Label("Pedidos — PrestaShop → Dolibarr");
|
||||||
|
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
|
||||||
|
|
||||||
// Info box
|
// Info banner
|
||||||
HBox infoBox = createInfoBox();
|
HBox infoBanner = new HBox(8);
|
||||||
|
infoBanner.getStyleClass().add("banner-info");
|
||||||
|
Label infoLabel = new Label("ℹ Los pedidos de PrestaShop se importan en Dolibarr como comandas y facturas.");
|
||||||
|
infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565C0;");
|
||||||
|
infoLabel.setWrapText(true);
|
||||||
|
infoBanner.getChildren().add(infoLabel);
|
||||||
|
|
||||||
// Table
|
ordersTable = createTable();
|
||||||
ordersTable = createOrdersTable();
|
VBox.setVgrow(ordersTable, Priority.ALWAYS);
|
||||||
VBox.setVgrow(ordersTable, javafx.scene.layout.Priority.ALWAYS);
|
|
||||||
|
|
||||||
// Action buttons
|
|
||||||
HBox actionBox = createActionBox();
|
HBox actionBox = createActionBox();
|
||||||
|
|
||||||
root.getChildren().addAll(title, infoBox, ordersTable, actionBox);
|
root.getChildren().addAll(title, subtitle, infoBanner, ordersTable, actionBox);
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
loadOrders();
|
loadOrders();
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createInfoBox() {
|
private TableView<OrderMappingResponse> createTable() {
|
||||||
HBox box = new HBox(10);
|
TableView<OrderMappingResponse> table = new TableView<>();
|
||||||
box.setPadding(new Insets(10));
|
|
||||||
box.setStyle("-fx-background-color: #e3f2fd; -fx-border-color: #1976d2; -fx-border-width: 1;");
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
|
|
||||||
Label infoLabel = new Label(
|
|
||||||
"ℹ Orders from PrestaShop are automatically imported into Dolibarr as sales orders and invoices."
|
|
||||||
);
|
|
||||||
infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #1565c0;");
|
|
||||||
infoLabel.setWrapText(true);
|
|
||||||
|
|
||||||
box.getChildren().add(infoLabel);
|
|
||||||
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TableView<Map<String, Object>> createOrdersTable() {
|
|
||||||
TableView<Map<String, Object>> table = new TableView<>();
|
|
||||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
|
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
|
||||||
table.setStyle("-fx-font-size: 11;");
|
|
||||||
|
|
||||||
// PrestaShop Order ID column
|
TableColumn<OrderMappingResponse, String> psIdCol = new TableColumn<>("Pedido PS #");
|
||||||
TableColumn<Map<String, Object>, String> psOrderIdCol = new TableColumn<>("PS Order ID");
|
psIdCol.setCellValueFactory(cf ->
|
||||||
psOrderIdCol.setCellValueFactory(cf -> {
|
new SimpleStringProperty(String.valueOf(cf.getValue().getPrestashopOrderId())));
|
||||||
Object value = cf.getValue().get("prestashopOrderId");
|
psIdCol.setPrefWidth(100);
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
|
|
||||||
});
|
|
||||||
psOrderIdCol.setPrefWidth(120);
|
|
||||||
|
|
||||||
// Dolibarr Order ID column
|
TableColumn<OrderMappingResponse, String> dolOrderCol = new TableColumn<>("Comanda Dolibarr");
|
||||||
TableColumn<Map<String, Object>, String> dolOrderIdCol = new TableColumn<>("Dolibarr Order ID");
|
dolOrderCol.setCellValueFactory(cf -> {
|
||||||
dolOrderIdCol.setCellValueFactory(cf -> {
|
Integer v = cf.getValue().getDolibarrOrderId();
|
||||||
Object value = cf.getValue().get("dolibarrOrderId");
|
return new SimpleStringProperty(v != null ? String.valueOf(v) : "—");
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(
|
|
||||||
value != null ? String.valueOf(value) : "—"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
dolOrderIdCol.setPrefWidth(120);
|
dolOrderCol.setPrefWidth(130);
|
||||||
|
|
||||||
// Dolibarr Invoice ID column
|
TableColumn<OrderMappingResponse, String> dolInvoiceCol = new TableColumn<>("Factura");
|
||||||
TableColumn<Map<String, Object>, String> dolInvoiceIdCol = new TableColumn<>("Invoice ID");
|
dolInvoiceCol.setCellValueFactory(cf -> {
|
||||||
dolInvoiceIdCol.setCellValueFactory(cf -> {
|
Integer v = cf.getValue().getDolibarrInvoiceId();
|
||||||
Object value = cf.getValue().get("dolibarrInvoiceId");
|
return new SimpleStringProperty(v != null ? String.valueOf(v) : "—");
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(
|
|
||||||
value != null ? String.valueOf(value) : "—"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
dolInvoiceIdCol.setPrefWidth(100);
|
dolInvoiceCol.setPrefWidth(80);
|
||||||
|
|
||||||
// Status column
|
// Status chip column
|
||||||
TableColumn<Map<String, Object>, String> statusCol = new TableColumn<>("Status");
|
TableColumn<OrderMappingResponse, String> statusCol = new TableColumn<>("Estado");
|
||||||
statusCol.setCellValueFactory(cf -> {
|
statusCol.setCellValueFactory(cf -> new SimpleStringProperty(cf.getValue().getStatus()));
|
||||||
String value = (String) cf.getValue().get("status");
|
statusCol.setPrefWidth(110);
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(value);
|
statusCol.setCellFactory(c -> new TableCell<>() {
|
||||||
});
|
|
||||||
statusCol.setPrefWidth(100);
|
|
||||||
statusCol.setCellFactory(col -> new TableCell<Map<String, Object>, String>() {
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
protected void updateItem(String status, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(status, empty);
|
||||||
if (empty || item == null) {
|
if (empty || status == null) { setGraphic(null); return; }
|
||||||
|
Label chip = new Label(DateUtils.orderStatusLabel(status));
|
||||||
|
chip.getStyleClass().add(DateUtils.orderStatusCssClass(status));
|
||||||
|
setGraphic(chip);
|
||||||
setText(null);
|
setText(null);
|
||||||
setStyle("");
|
|
||||||
} else {
|
|
||||||
setText(item);
|
|
||||||
if ("INVOICED".equals(item)) {
|
|
||||||
setStyle("-fx-text-fill: #2e7d32; -fx-font-weight: bold;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Imported At column
|
TableColumn<OrderMappingResponse, String> importedCol = new TableColumn<>("Importado");
|
||||||
TableColumn<Map<String, Object>, String> importedAtCol = new TableColumn<>("Imported At");
|
importedCol.setCellValueFactory(cf ->
|
||||||
importedAtCol.setCellValueFactory(cf -> {
|
new SimpleStringProperty(DateUtils.formatDateTime(cf.getValue().getImportedAt())));
|
||||||
Object value = cf.getValue().get("importedAt");
|
importedCol.setPrefWidth(150);
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(
|
|
||||||
value != null ? String.valueOf(value).substring(0, 19) : "—"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
importedAtCol.setPrefWidth(150);
|
|
||||||
|
|
||||||
table.getColumns().addAll(psOrderIdCol, dolOrderIdCol, dolInvoiceIdCol, statusCol, importedAtCol);
|
|
||||||
|
|
||||||
|
table.getColumns().addAll(psIdCol, dolOrderCol, dolInvoiceCol, statusCol, importedCol);
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createActionBox() {
|
private HBox createActionBox() {
|
||||||
HBox box = new HBox(10);
|
HBox box = new HBox(10);
|
||||||
box.setPadding(new Insets(15));
|
box.setPadding(new Insets(10, 0, 0, 0));
|
||||||
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
|
box.setAlignment(Pos.CENTER_RIGHT);
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
|
|
||||||
Label label = new Label("Actions:");
|
Button refreshBtn = new Button("🔄 Actualizar");
|
||||||
label.setStyle("-fx-font-weight: bold;");
|
refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 16;");
|
||||||
|
refreshBtn.setOnAction(e -> loadOrders());
|
||||||
|
|
||||||
Button syncButton = new Button("🔄 Sync Orders from PrestaShop");
|
Button syncBtn = new Button("Sincronizar pedidos");
|
||||||
syncButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #f57c00; -fx-text-fill: white;");
|
syncBtn.setStyle("-fx-background-color: #E65100; -fx-text-fill: white; " +
|
||||||
syncButton.setOnAction(e -> triggerOrderSync());
|
"-fx-background-radius: 8; -fx-padding: 10 20; -fx-font-weight: bold; -fx-cursor: hand;");
|
||||||
|
syncBtn.setOnAction(e -> triggerOrderSync(syncBtn));
|
||||||
Button refreshButton = new Button("🔄 Refresh");
|
|
||||||
refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;");
|
|
||||||
refreshButton.setOnAction(e -> loadOrders());
|
|
||||||
|
|
||||||
box.getChildren().addAll(label, syncButton, refreshButton);
|
|
||||||
|
|
||||||
|
box.getChildren().addAll(refreshBtn, syncBtn);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadOrders() {
|
private void loadOrders() {
|
||||||
Thread thread = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
List<Map<String, Object>> mappings = client.getOrderMappings();
|
List<OrderMappingResponse> orders = client.getOrderMappings();
|
||||||
|
Platform.runLater(() -> ordersTable.setItems(FXCollections.observableArrayList(orders)));
|
||||||
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(mappings);
|
|
||||||
Platform.runLater(() -> ordersTable.setItems(items));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error loading orders: {}", e.getMessage());
|
log.error("Error loading orders: {}", e.getMessage());
|
||||||
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load orders: " + e.getMessage()));
|
Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los pedidos: " + e.getMessage()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.setDaemon(true);
|
t.setDaemon(true);
|
||||||
thread.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void triggerOrderSync() {
|
private void triggerOrderSync(Button btn) {
|
||||||
Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION);
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
confirmAlert.setTitle("Confirm Sync");
|
confirm.setTitle("Confirmar sincronización");
|
||||||
confirmAlert.setHeaderText("Sync Orders from PrestaShop to Dolibarr?");
|
confirm.setHeaderText("¿Sincronizar pedidos de PrestaShop a Dolibarr?");
|
||||||
confirmAlert.setContentText("This will import new orders from PrestaShop as sales orders and invoices in Dolibarr.");
|
confirm.setContentText("Se importarán los pedidos nuevos como comandas y facturas en Dolibarr.");
|
||||||
|
if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
|
||||||
|
|
||||||
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
btn.setDisable(true);
|
||||||
Thread thread = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
Map<String, Object> result = client.triggerOrderSync();
|
SyncTriggerResponse result = client.triggerOrderSync();
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
int processed = ((Number) result.get("processed")).intValue();
|
btn.setDisable(false);
|
||||||
int failed = ((Number) result.get("failed")).intValue();
|
if (result.getItemsFailed() == 0) {
|
||||||
|
AlertUtil.showInfo("Completado", "Pedidos importados: " + result.getItemsProcessed());
|
||||||
if (failed == 0) {
|
|
||||||
AlertUtil.showInfo("Success",
|
|
||||||
"Orders synced: " + processed + " orders imported");
|
|
||||||
} else {
|
} else {
|
||||||
AlertUtil.showWarn("Partial Success",
|
AlertUtil.showWarn("Completado con errores",
|
||||||
"Processed: " + processed + ", Failed: " + failed);
|
result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOrders();
|
loadOrders();
|
||||||
dashboardController.refreshDashboard();
|
dashboardController.refreshDashboard();
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() ->
|
Platform.runLater(() -> {
|
||||||
AlertUtil.showError("Sync Error", e.getMessage())
|
btn.setDisable(false);
|
||||||
);
|
AlertUtil.showError("Error de sincronización", e.getMessage());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.setDaemon(true);
|
t.setDaemon(true);
|
||||||
thread.start();
|
t.start();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,30 +1,37 @@
|
||||||
package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
|
package com.teterialosjuanjos.tfg.javafxclient.ui.controllers;
|
||||||
|
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
import com.teterialosjuanjos.tfg.javafxclient.api.SyncServiceClient;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.ProductMappingResponse;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
import com.teterialosjuanjos.tfg.javafxclient.model.SessionManager;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.model.SyncTriggerResponse;
|
||||||
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
import com.teterialosjuanjos.tfg.javafxclient.util.AlertUtil;
|
||||||
|
import com.teterialosjuanjos.tfg.javafxclient.util.DateUtils;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab para productos: ver mapeos y disparar sync.
|
* Tab de mapeos de productos con búsqueda por SKU y filtros de estado.
|
||||||
|
* Equivalente a MappingsScreen (tab Productos) de la app Android.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProductsController {
|
public class ProductsController {
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
private final SessionManager sessionManager;
|
||||||
private final DashboardController dashboardController;
|
private final DashboardController dashboardController;
|
||||||
private TableView<Map<String, Object>> productsTable;
|
|
||||||
|
private TableView<ProductMappingResponse> productsTable;
|
||||||
|
private List<ProductMappingResponse> allProducts = List.of();
|
||||||
|
private String activeStatusFilter = null;
|
||||||
|
|
||||||
public ProductsController(SessionManager sessionManager, DashboardController dashboardController) {
|
public ProductsController(SessionManager sessionManager, DashboardController dashboardController) {
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
|
|
@ -32,240 +39,197 @@ public class ProductsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public VBox createView() {
|
public VBox createView() {
|
||||||
VBox root = new VBox(15);
|
VBox root = new VBox(12);
|
||||||
root.setPadding(new Insets(20));
|
root.setPadding(new Insets(20));
|
||||||
root.setStyle("-fx-background-color: #fafafa;");
|
root.setStyle("-fx-background-color: #FAFAFA;");
|
||||||
|
|
||||||
// Title
|
// Header
|
||||||
Label title = new Label("Product Mappings");
|
Label title = new Label("Mappings");
|
||||||
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
|
title.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
|
||||||
|
Label subtitle = new Label("Productos — Dolibarr ↔ PrestaShop");
|
||||||
|
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #757575;");
|
||||||
|
|
||||||
// Filter & Actions
|
// Search bar
|
||||||
HBox filterBox = createFilterBox();
|
TextField searchField = new TextField();
|
||||||
|
searchField.setPromptText("Buscar por SKU...");
|
||||||
|
searchField.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
searchField.textProperty().addListener((obs, old, val) -> applyFilters(searchField, val));
|
||||||
|
|
||||||
|
// Filter chips
|
||||||
|
HBox chipsBox = createFilterChips(searchField);
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
productsTable = createProductsTable();
|
productsTable = createTable();
|
||||||
VBox.setVgrow(productsTable, javafx.scene.layout.Priority.ALWAYS);
|
VBox.setVgrow(productsTable, Priority.ALWAYS);
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
HBox actionBox = createActionBox();
|
HBox actionBox = createActionBox();
|
||||||
|
|
||||||
root.getChildren().addAll(title, filterBox, productsTable, actionBox);
|
root.getChildren().addAll(title, subtitle, searchField, chipsBox, productsTable, actionBox);
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
loadProducts(null);
|
|
||||||
|
|
||||||
|
loadProducts();
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createFilterBox() {
|
private HBox createFilterChips(TextField searchField) {
|
||||||
HBox box = new HBox(10);
|
ToggleGroup group = new ToggleGroup();
|
||||||
box.setPadding(new Insets(10));
|
|
||||||
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
|
|
||||||
Label filterLabel = new Label("Filter by status:");
|
ToggleButton allBtn = chip("Todos", null, group, true);
|
||||||
|
ToggleButton syncedBtn = chip("Sincronizados", "SYNCED", group, false);
|
||||||
|
ToggleButton pendingBtn = chip("Pendientes", "PENDING", group, false);
|
||||||
|
ToggleButton errorBtn = chip("Con error", "ERROR", group, false);
|
||||||
|
|
||||||
ComboBox<String> statusCombo = new ComboBox<>();
|
group.selectedToggleProperty().addListener((obs, old, newToggle) -> {
|
||||||
statusCombo.setItems(FXCollections.observableArrayList(
|
if (newToggle == null) { allBtn.setSelected(true); return; }
|
||||||
"All", "SYNCED", "PENDING", "ERROR"
|
activeStatusFilter = (String) newToggle.getUserData();
|
||||||
));
|
loadProducts();
|
||||||
statusCombo.setValue("All");
|
|
||||||
statusCombo.setPrefWidth(120);
|
|
||||||
|
|
||||||
statusCombo.setOnAction(e -> {
|
|
||||||
String selected = statusCombo.getValue();
|
|
||||||
loadProducts("All".equals(selected) ? null : selected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Region spacer = new Region();
|
HBox box = new HBox(8, allBtn, syncedBtn, pendingBtn, errorBtn);
|
||||||
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
Button refreshButton = new Button("🔄 Refresh");
|
|
||||||
refreshButton.setStyle("-fx-font-size: 11; -fx-padding: 8;");
|
|
||||||
refreshButton.setOnAction(e -> loadProducts(null));
|
|
||||||
|
|
||||||
box.getChildren().addAll(filterLabel, statusCombo, spacer, refreshButton);
|
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TableView<Map<String, Object>> createProductsTable() {
|
private ToggleButton chip(String text, String value, ToggleGroup group, boolean selected) {
|
||||||
TableView<Map<String, Object>> table = new TableView<>();
|
ToggleButton btn = new ToggleButton(text);
|
||||||
|
btn.setToggleGroup(group);
|
||||||
|
btn.setSelected(selected);
|
||||||
|
btn.setUserData(value);
|
||||||
|
btn.getStyleClass().add("filter-chip");
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableView<ProductMappingResponse> createTable() {
|
||||||
|
TableView<ProductMappingResponse> table = new TableView<>();
|
||||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
|
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
|
||||||
table.setStyle("-fx-font-size: 11;");
|
|
||||||
|
|
||||||
// SKU column
|
TableColumn<ProductMappingResponse, String> skuCol = col("SKU", p -> p.getSku());
|
||||||
TableColumn<Map<String, Object>, String> skuCol = new TableColumn<>("SKU");
|
skuCol.setPrefWidth(140);
|
||||||
skuCol.setCellValueFactory(cf -> {
|
|
||||||
String value = (String) cf.getValue().get("sku");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(value);
|
|
||||||
});
|
|
||||||
skuCol.setPrefWidth(120);
|
|
||||||
|
|
||||||
// Dolibarr ID column
|
TableColumn<ProductMappingResponse, String> dolIdCol = col("Dolibarr ID",
|
||||||
TableColumn<Map<String, Object>, String> dolIdCol = new TableColumn<>("Dolibarr ID");
|
p -> p.getDolibarrId() != null ? String.valueOf(p.getDolibarrId()) : "—");
|
||||||
dolIdCol.setCellValueFactory(cf -> {
|
|
||||||
Object value = cf.getValue().get("dolibarrId");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
|
|
||||||
});
|
|
||||||
dolIdCol.setPrefWidth(100);
|
dolIdCol.setPrefWidth(100);
|
||||||
|
|
||||||
// PrestaShop ID column
|
TableColumn<ProductMappingResponse, String> psIdCol = col("PrestaShop ID",
|
||||||
TableColumn<Map<String, Object>, String> psIdCol = new TableColumn<>("PrestaShop ID");
|
p -> p.getPrestashopId() != null ? String.valueOf(p.getPrestashopId()) : "—");
|
||||||
psIdCol.setCellValueFactory(cf -> {
|
psIdCol.setPrefWidth(110);
|
||||||
Object value = cf.getValue().get("prestashopId");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(String.valueOf(value));
|
|
||||||
});
|
|
||||||
psIdCol.setPrefWidth(100);
|
|
||||||
|
|
||||||
// Status column
|
TableColumn<ProductMappingResponse, String> lastSyncCol = col("Última sync",
|
||||||
TableColumn<Map<String, Object>, String> statusCol = new TableColumn<>("Status");
|
p -> DateUtils.formatRelative(p.getLastSyncedAt()));
|
||||||
statusCol.setCellValueFactory(cf -> {
|
lastSyncCol.setPrefWidth(130);
|
||||||
String value = (String) cf.getValue().get("syncStatus");
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(value);
|
// Status chip column
|
||||||
});
|
TableColumn<ProductMappingResponse, String> statusCol = new TableColumn<>("Estado");
|
||||||
statusCol.setPrefWidth(80);
|
statusCol.setCellValueFactory(cf -> new SimpleStringProperty(cf.getValue().getSyncStatus()));
|
||||||
statusCol.setCellFactory(col -> new TableCell<Map<String, Object>, String>() {
|
statusCol.setPrefWidth(120);
|
||||||
|
statusCol.setCellFactory(c -> new TableCell<>() {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
protected void updateItem(String status, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(status, empty);
|
||||||
if (empty || item == null) {
|
if (empty || status == null) { setGraphic(null); return; }
|
||||||
|
Label chip = new Label(DateUtils.productStatusLabel(status));
|
||||||
|
chip.getStyleClass().add(DateUtils.productStatusCssClass(status));
|
||||||
|
setGraphic(chip);
|
||||||
setText(null);
|
setText(null);
|
||||||
setStyle("");
|
|
||||||
} else {
|
|
||||||
setText(item);
|
|
||||||
switch (item) {
|
|
||||||
case "SYNCED" -> setStyle("-fx-text-fill: #2e7d32; -fx-font-weight: bold;");
|
|
||||||
case "PENDING" -> setStyle("-fx-text-fill: #f57f17; -fx-font-weight: bold;");
|
|
||||||
case "ERROR" -> setStyle("-fx-text-fill: #c62828; -fx-font-weight: bold;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error message column
|
TableColumn<ProductMappingResponse, String> errorCol = col("Error",
|
||||||
TableColumn<Map<String, Object>, String> errorCol = new TableColumn<>("Error");
|
p -> p.getErrorMessage() != null ? p.getErrorMessage() : "");
|
||||||
errorCol.setCellValueFactory(cf -> {
|
|
||||||
Object value = cf.getValue().get("errorMessage");
|
|
||||||
String text = value != null ? (String) value : "";
|
|
||||||
return new javafx.beans.property.SimpleObjectProperty<>(text);
|
|
||||||
});
|
|
||||||
errorCol.setPrefWidth(200);
|
errorCol.setPrefWidth(200);
|
||||||
|
|
||||||
table.getColumns().addAll(skuCol, dolIdCol, psIdCol, statusCol, errorCol);
|
table.getColumns().addAll(skuCol, dolIdCol, psIdCol, lastSyncCol, statusCol, errorCol);
|
||||||
|
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <T> TableColumn<ProductMappingResponse, String> col(
|
||||||
|
String header, java.util.function.Function<ProductMappingResponse, String> extractor) {
|
||||||
|
TableColumn<ProductMappingResponse, String> col = new TableColumn<>(header);
|
||||||
|
col.setCellValueFactory(cf -> new SimpleStringProperty(extractor.apply(cf.getValue())));
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
private HBox createActionBox() {
|
private HBox createActionBox() {
|
||||||
HBox box = new HBox(10);
|
HBox box = new HBox(10);
|
||||||
box.setPadding(new Insets(15));
|
box.setPadding(new Insets(10, 0, 0, 0));
|
||||||
box.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0;");
|
|
||||||
box.setAlignment(Pos.CENTER_RIGHT);
|
box.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
Button syncButton = new Button("🔄 Sync Products from Dolibarr");
|
Button refreshBtn = new Button("🔄 Actualizar");
|
||||||
syncButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #1976d2; -fx-text-fill: white;");
|
refreshBtn.setStyle("-fx-background-color: #E0E0E0; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8 16;");
|
||||||
syncButton.setOnAction(e -> triggerProductSync());
|
refreshBtn.setOnAction(e -> loadProducts());
|
||||||
|
|
||||||
Button stockButton = new Button("📊 Sync Stock");
|
Button syncProductsBtn = new Button("Sincronizar productos");
|
||||||
stockButton.setStyle("-fx-font-size: 12; -fx-padding: 10; -fx-background-color: #388e3c; -fx-text-fill: white;");
|
syncProductsBtn.setStyle("-fx-background-color: #1565C0; -fx-text-fill: white; " +
|
||||||
stockButton.setOnAction(e -> triggerStockSync());
|
"-fx-background-radius: 8; -fx-padding: 10 20; -fx-font-weight: bold; -fx-cursor: hand;");
|
||||||
|
syncProductsBtn.setOnAction(e -> triggerSync("PRODUCT_PUSH", syncProductsBtn));
|
||||||
|
|
||||||
box.getChildren().addAll(syncButton, stockButton);
|
Button syncStockBtn = new Button("Sincronizar stock");
|
||||||
|
syncStockBtn.setStyle("-fx-background-color: #2E7D32; -fx-text-fill: white; " +
|
||||||
|
"-fx-background-radius: 8; -fx-padding: 10 20; -fx-cursor: hand;");
|
||||||
|
syncStockBtn.setOnAction(e -> triggerSync("STOCK_PUSH", syncStockBtn));
|
||||||
|
|
||||||
|
box.getChildren().addAll(refreshBtn, syncProductsBtn, syncStockBtn);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadProducts(String status) {
|
private void loadProducts() {
|
||||||
Thread thread = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
List<Map<String, Object>> mappings = client.getProductMappings(status);
|
allProducts = client.getProductMappings(activeStatusFilter);
|
||||||
|
Platform.runLater(() -> productsTable.setItems(FXCollections.observableArrayList(allProducts)));
|
||||||
ObservableList<Map<String, Object>> items = FXCollections.observableArrayList(mappings);
|
|
||||||
Platform.runLater(() -> productsTable.setItems(items));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error loading products: {}", e.getMessage());
|
log.error("Error loading products: {}", e.getMessage());
|
||||||
Platform.runLater(() -> AlertUtil.showError("Error", "Failed to load products: " + e.getMessage()));
|
Platform.runLater(() -> AlertUtil.showError("Error", "No se pudieron cargar los productos: " + e.getMessage()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.setDaemon(true);
|
t.setDaemon(true);
|
||||||
thread.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void triggerProductSync() {
|
private void applyFilters(TextField searchField, String searchText) {
|
||||||
Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION);
|
String query = searchText == null ? "" : searchText.toLowerCase();
|
||||||
confirmAlert.setTitle("Confirm Sync");
|
List<ProductMappingResponse> filtered = allProducts.stream()
|
||||||
confirmAlert.setHeaderText("Sync Products from Dolibarr to PrestaShop?");
|
.filter(p -> p.getSku() != null && p.getSku().toLowerCase().contains(query))
|
||||||
confirmAlert.setContentText("This will push new/modified products from Dolibarr to PrestaShop.");
|
.collect(Collectors.toList());
|
||||||
|
productsTable.setItems(FXCollections.observableArrayList(filtered));
|
||||||
|
}
|
||||||
|
|
||||||
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
private void triggerSync(String type, Button btn) {
|
||||||
Thread thread = new Thread(() -> {
|
String label = type.equals("PRODUCT_PUSH") ? "Productos" : "Stock";
|
||||||
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
|
confirm.setTitle("Confirmar sincronización");
|
||||||
|
confirm.setHeaderText("¿Sincronizar " + label + "?");
|
||||||
|
confirm.setContentText(DateUtils.syncTypeDirection(type));
|
||||||
|
if (confirm.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
|
||||||
|
|
||||||
|
btn.setDisable(true);
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
||||||
Map<String, Object> result = client.triggerProductSync();
|
SyncTriggerResponse result = type.equals("PRODUCT_PUSH")
|
||||||
|
? client.triggerProductSync()
|
||||||
|
: client.triggerStockSync();
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
int processed = ((Number) result.get("processed")).intValue();
|
btn.setDisable(false);
|
||||||
int failed = ((Number) result.get("failed")).intValue();
|
if (result.getItemsFailed() == 0) {
|
||||||
|
AlertUtil.showInfo("Completado", label + ": " + result.getItemsProcessed() + " procesados.");
|
||||||
if (failed == 0) {
|
|
||||||
AlertUtil.showInfo("Success",
|
|
||||||
"Sync completed: " + processed + " products processed");
|
|
||||||
} else {
|
} else {
|
||||||
AlertUtil.showWarn("Partial Success",
|
AlertUtil.showWarn("Completado con errores",
|
||||||
"Processed: " + processed + ", Failed: " + failed);
|
result.getItemsProcessed() + " procesados, " + result.getItemsFailed() + " fallidos.");
|
||||||
}
|
}
|
||||||
|
loadProducts();
|
||||||
loadProducts(null);
|
|
||||||
dashboardController.refreshDashboard();
|
dashboardController.refreshDashboard();
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() ->
|
|
||||||
AlertUtil.showError("Sync Error", e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
thread.setDaemon(true);
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void triggerStockSync() {
|
|
||||||
Alert confirmAlert = new Alert(Alert.AlertType.CONFIRMATION);
|
|
||||||
confirmAlert.setTitle("Confirm Sync");
|
|
||||||
confirmAlert.setHeaderText("Sync Stock from Dolibarr to PrestaShop?");
|
|
||||||
confirmAlert.setContentText("This will update stock levels in PrestaShop from Dolibarr.");
|
|
||||||
|
|
||||||
if (confirmAlert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
|
||||||
Thread thread = new Thread(() -> {
|
|
||||||
try {
|
|
||||||
SyncServiceClient client = sessionManager.getSyncServiceClient();
|
|
||||||
Map<String, Object> result = client.triggerStockSync();
|
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
int processed = ((Number) result.get("processed")).intValue();
|
btn.setDisable(false);
|
||||||
int failed = ((Number) result.get("failed")).intValue();
|
AlertUtil.showError("Error de sincronización", e.getMessage());
|
||||||
|
|
||||||
if (failed == 0) {
|
|
||||||
AlertUtil.showInfo("Success",
|
|
||||||
"Stock sync completed: " + processed + " items updated");
|
|
||||||
} else {
|
|
||||||
AlertUtil.showWarn("Partial Success",
|
|
||||||
"Processed: " + processed + ", Failed: " + failed);
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboardController.refreshDashboard();
|
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
Platform.runLater(() ->
|
|
||||||
AlertUtil.showError("Sync Error", e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread.setDaemon(true);
|
t.setDaemon(true);
|
||||||
thread.start();
|
t.start();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pantalla de login para autenticación contra el sync-service.
|
* Pantalla de login para autenticación contra el sync-service.
|
||||||
|
* Pre-rellena los campos con los ajustes guardados en SettingsStore.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class LoginStage {
|
public class LoginStage {
|
||||||
|
|
||||||
|
private static final String PROD_URL = "https://proyectointermodular-production-a9c3.up.railway.app";
|
||||||
|
private static final String DEV_URL = "http://localhost:8080";
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
private final SessionManager sessionManager;
|
||||||
private final Stage primaryStage;
|
private final Stage primaryStage;
|
||||||
private Stage loginWindow;
|
private Stage loginWindow;
|
||||||
|
|
@ -29,9 +32,9 @@ public class LoginStage {
|
||||||
|
|
||||||
public void show() {
|
public void show() {
|
||||||
loginWindow = new Stage();
|
loginWindow = new Stage();
|
||||||
loginWindow.setTitle("TFG Sync Service — Login");
|
loginWindow.setTitle("Sync Manager — Iniciar sesión");
|
||||||
loginWindow.setWidth(400);
|
loginWindow.setWidth(420);
|
||||||
loginWindow.setHeight(500);
|
loginWindow.setHeight(520);
|
||||||
loginWindow.setResizable(false);
|
loginWindow.setResizable(false);
|
||||||
|
|
||||||
VBox root = createLoginForm();
|
VBox root = createLoginForm();
|
||||||
|
|
@ -45,129 +48,121 @@ public class LoginStage {
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox createLoginForm() {
|
private VBox createLoginForm() {
|
||||||
VBox form = new VBox(15);
|
VBox form = new VBox(0);
|
||||||
form.setPadding(new Insets(40));
|
form.setStyle("-fx-background-color: #F5F5F5;");
|
||||||
form.setAlignment(Pos.TOP_CENTER);
|
|
||||||
form.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 0;");
|
// Top color bar
|
||||||
|
HBox topBar = new HBox();
|
||||||
|
topBar.setMinHeight(6);
|
||||||
|
topBar.setStyle("-fx-background-color: #1565C0;");
|
||||||
|
|
||||||
|
// Card
|
||||||
|
VBox card = new VBox(14);
|
||||||
|
card.setPadding(new Insets(36, 40, 36, 40));
|
||||||
|
card.setAlignment(Pos.TOP_CENTER);
|
||||||
|
card.setStyle(
|
||||||
|
"-fx-background-color: white;" +
|
||||||
|
"-fx-background-radius: 0 0 12 12;" +
|
||||||
|
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.08), 8, 0, 0, 3);"
|
||||||
|
);
|
||||||
|
VBox.setVgrow(card, Priority.ALWAYS);
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Label title = new Label("Sync Service Client");
|
Label title = new Label("Sync Manager");
|
||||||
title.setStyle("-fx-font-size: 24; -fx-font-weight: bold; -fx-text-fill: #1976d2;");
|
title.setStyle("-fx-font-size: 26; -fx-font-weight: bold; -fx-text-fill: #1565C0;");
|
||||||
|
|
||||||
Label subtitle = new Label("Dolibarr ↔ PrestaShop Integration");
|
Label subtitle = new Label("Dolibarr · PrestaShop");
|
||||||
subtitle.setStyle("-fx-font-size: 12; -fx-text-fill: #666666;");
|
subtitle.setStyle("-fx-font-size: 13; -fx-text-fill: #757575;");
|
||||||
|
|
||||||
// URL input
|
VBox titleBox = new VBox(4, title, subtitle);
|
||||||
Label urlLabel = new Label("Base URL:");
|
titleBox.setAlignment(Pos.CENTER);
|
||||||
urlLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;");
|
|
||||||
TextField urlField = new TextField();
|
Separator sep = new Separator();
|
||||||
urlField.setText("https://proyectointermodular-production-a9c3.up.railway.app");
|
sep.setPadding(new Insets(4, 0, 4, 0));
|
||||||
|
|
||||||
|
// URL field
|
||||||
|
Label urlLabel = new Label("URL del servidor");
|
||||||
|
urlLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;");
|
||||||
|
TextField urlField = new TextField(sessionManager.getBaseUrl());
|
||||||
|
urlField.setPromptText("https://tu-servidor.up.railway.app");
|
||||||
urlField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
|
urlField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
|
||||||
|
|
||||||
// Username input
|
// Username
|
||||||
Label usernameLabel = new Label("Username:");
|
Label userLabel = new Label("Usuario");
|
||||||
usernameLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;");
|
userLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;");
|
||||||
TextField usernameField = new TextField();
|
TextField userField = new TextField(
|
||||||
usernameField.setPromptText("admin");
|
sessionManager.getUsername() != null ? sessionManager.getUsername() : "");
|
||||||
usernameField.setText("admin");
|
userField.setPromptText("admin");
|
||||||
usernameField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
|
userField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
|
||||||
|
|
||||||
// Password input
|
// Password
|
||||||
Label passwordLabel = new Label("Password:");
|
Label passLabel = new Label("Contraseña");
|
||||||
passwordLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11;");
|
passLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold; -fx-text-fill: #546E7A;");
|
||||||
PasswordField passwordField = new PasswordField();
|
PasswordField passField = new PasswordField();
|
||||||
passwordField.setPromptText("••••••••");
|
passField.setText(sessionManager.getPassword() != null ? sessionManager.getPassword() : "");
|
||||||
passwordField.setText("admin123");
|
passField.setPromptText("••••••••");
|
||||||
passwordField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
|
passField.setStyle("-fx-font-size: 12; -fx-padding: 8;");
|
||||||
|
|
||||||
// Login button
|
|
||||||
Button loginButton = new Button("Login");
|
|
||||||
loginButton.setStyle(
|
|
||||||
"-fx-font-size: 12; " +
|
|
||||||
"-fx-padding: 10; " +
|
|
||||||
"-fx-background-color: #1976d2; " +
|
|
||||||
"-fx-text-fill: white; " +
|
|
||||||
"-fx-font-weight: bold; " +
|
|
||||||
"-fx-cursor: hand;"
|
|
||||||
);
|
|
||||||
loginButton.setPrefWidth(Double.MAX_VALUE);
|
|
||||||
|
|
||||||
loginButton.setOnAction(e -> handleLogin(
|
|
||||||
urlField.getText(),
|
|
||||||
usernameField.getText(),
|
|
||||||
passwordField.getText()
|
|
||||||
));
|
|
||||||
|
|
||||||
// Dev mode toggle
|
// Dev mode toggle
|
||||||
CheckBox devModeCheckBox = new CheckBox("Use local dev server (http://localhost:8080)");
|
CheckBox devCheckBox = new CheckBox("Usar servidor local (localhost:8080)");
|
||||||
devModeCheckBox.setStyle("-fx-font-size: 11;");
|
devCheckBox.setStyle("-fx-font-size: 11; -fx-text-fill: #546E7A;");
|
||||||
devModeCheckBox.selectedProperty().addListener((obs, old, newVal) -> {
|
devCheckBox.selectedProperty().addListener((obs, old, val) ->
|
||||||
if (newVal) {
|
urlField.setText(val ? DEV_URL : PROD_URL));
|
||||||
urlField.setText("http://localhost:8080");
|
|
||||||
} else {
|
|
||||||
urlField.setText("https://proyectointermodular-production-a9c3.up.railway.app");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Separator
|
// Login button
|
||||||
Separator separator = new Separator();
|
Button loginButton = new Button("Conectar");
|
||||||
|
loginButton.getStyleClass().add("btn-primary");
|
||||||
|
loginButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
loginButton.setPrefHeight(42);
|
||||||
|
loginButton.setStyle(loginButton.getStyle() +
|
||||||
|
"-fx-font-size: 13; -fx-font-weight: bold; -fx-cursor: hand;");
|
||||||
|
loginButton.setOnAction(e -> handleLogin(urlField, userField, passField));
|
||||||
|
|
||||||
// Info label
|
// Allow Enter key in password field
|
||||||
Label infoLabel = new Label("Tip: Default credentials are admin / admin123");
|
passField.setOnAction(e -> handleLogin(urlField, userField, passField));
|
||||||
infoLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #999999; -fx-font-style: italic;");
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
card.getChildren().addAll(
|
||||||
title,
|
titleBox, sep,
|
||||||
subtitle,
|
urlLabel, urlField,
|
||||||
new Separator(),
|
userLabel, userField,
|
||||||
urlLabel,
|
passLabel, passField,
|
||||||
urlField,
|
devCheckBox,
|
||||||
usernameLabel,
|
new Region(),
|
||||||
usernameField,
|
loginButton
|
||||||
passwordLabel,
|
|
||||||
passwordField,
|
|
||||||
separator,
|
|
||||||
devModeCheckBox,
|
|
||||||
new Region(), // spacer
|
|
||||||
loginButton,
|
|
||||||
infoLabel
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
form.getChildren().addAll(topBar, card);
|
||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogin(String baseUrl, String username, String password) {
|
private void handleLogin(TextField urlField, TextField userField, PasswordField passField) {
|
||||||
if (baseUrl.isEmpty() || username.isEmpty() || password.isEmpty()) {
|
String url = urlField.getText().trim();
|
||||||
AlertUtil.showError("Validation Error", "All fields are required");
|
String user = userField.getText().trim();
|
||||||
|
String pass = passField.getText();
|
||||||
|
|
||||||
|
if (url.isEmpty() || user.isEmpty() || pass.isEmpty()) {
|
||||||
|
AlertUtil.showError("Validación", "Todos los campos son obligatorios.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecuta en background para no freezear UI
|
Thread t = new Thread(() -> {
|
||||||
Thread loginThread = new Thread(() -> {
|
|
||||||
try {
|
try {
|
||||||
boolean success = sessionManager.authenticate(baseUrl, username, password);
|
boolean success = sessionManager.authenticate(url, user, pass);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (success) {
|
if (success) {
|
||||||
AlertUtil.showInfo("Success", "Authentication successful!");
|
|
||||||
loginWindow.close();
|
loginWindow.close();
|
||||||
openMainStage();
|
new MainStage(sessionManager, primaryStage).show();
|
||||||
} else {
|
} else {
|
||||||
AlertUtil.showError("Login Failed",
|
AlertUtil.showError("Autenticación fallida",
|
||||||
"Could not authenticate. Check credentials and URL.");
|
"No se pudo conectar. Verifica URL y credenciales.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Platform.runLater(() -> {
|
log.error("Login error: {}", ex.getMessage());
|
||||||
AlertUtil.showError("Connection Error", ex.getMessage());
|
Platform.runLater(() -> AlertUtil.showError("Error de conexión", ex.getMessage()));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loginThread.setDaemon(true);
|
t.setDaemon(true);
|
||||||
loginThread.start();
|
t.start();
|
||||||
}
|
|
||||||
|
|
||||||
private void openMainStage() {
|
|
||||||
MainStage mainStage = new MainStage(sessionManager, primaryStage);
|
|
||||||
mainStage.show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,27 +6,18 @@ import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pantalla principal con navegación por tabs y dashboard.
|
* Ventana principal con navegación por tabs: Panel, Productos, Pedidos, Logs, Ajustes.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class MainStage {
|
public class MainStage {
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
private final SessionManager sessionManager;
|
||||||
private final Stage primaryStage;
|
private final Stage primaryStage;
|
||||||
private Stage mainWindow;
|
|
||||||
|
|
||||||
private DashboardController dashboardController;
|
|
||||||
private ProductsController productsController;
|
|
||||||
private OrdersController ordersController;
|
|
||||||
private LogsController logsController;
|
|
||||||
|
|
||||||
public MainStage(SessionManager sessionManager, Stage primaryStage) {
|
public MainStage(SessionManager sessionManager, Stage primaryStage) {
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
|
|
@ -34,12 +25,14 @@ public class MainStage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show() {
|
public void show() {
|
||||||
mainWindow = new Stage();
|
Stage mainWindow = new Stage();
|
||||||
mainWindow.setTitle("TFG Sync Service — Dashboard");
|
mainWindow.setTitle("Sync Manager");
|
||||||
mainWindow.setWidth(1200);
|
mainWindow.setWidth(1200);
|
||||||
mainWindow.setHeight(700);
|
mainWindow.setHeight(720);
|
||||||
|
mainWindow.setMinWidth(900);
|
||||||
|
mainWindow.setMinHeight(600);
|
||||||
|
|
||||||
BorderPane root = createMainLayout();
|
BorderPane root = createMainLayout(mainWindow);
|
||||||
|
|
||||||
Scene scene = new Scene(root);
|
Scene scene = new Scene(root);
|
||||||
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
|
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
|
||||||
|
|
@ -48,92 +41,83 @@ public class MainStage {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private BorderPane createMainLayout() {
|
private BorderPane createMainLayout(Stage window) {
|
||||||
BorderPane root = new BorderPane();
|
BorderPane root = new BorderPane();
|
||||||
|
root.setTop(createHeader(window));
|
||||||
// Top: Header con usuario y logout
|
|
||||||
root.setTop(createHeader());
|
|
||||||
|
|
||||||
// Center: Tabs
|
|
||||||
root.setCenter(createTabPane());
|
root.setCenter(createTabPane());
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox createHeader() {
|
private HBox createHeader(Stage window) {
|
||||||
VBox header = new VBox();
|
HBox header = new HBox(12);
|
||||||
header.setPadding(new Insets(15));
|
header.setPadding(new Insets(12, 20, 12, 20));
|
||||||
header.setStyle("-fx-background-color: #1976d2; -fx-border-color: #1565c0; -fx-border-width: 0 0 2 0;");
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
header.setStyle("-fx-background-color: #1565C0;");
|
||||||
|
|
||||||
HBox topBar = new HBox(10);
|
Label appName = new Label("Sync Manager");
|
||||||
topBar.setAlignment(Pos.CENTER_LEFT);
|
appName.setStyle("-fx-font-size: 17; -fx-font-weight: bold; -fx-text-fill: white;");
|
||||||
|
|
||||||
Label title = new Label("Sync Service Control Panel");
|
Label sep = new Label("·");
|
||||||
title.setStyle("-fx-font-size: 18; -fx-font-weight: bold; -fx-text-fill: white;");
|
sep.setStyle("-fx-text-fill: rgba(255,255,255,0.5); -fx-font-size: 14;");
|
||||||
|
|
||||||
|
Label connectedLabel = new Label("✓ " + sessionManager.getBaseUrl());
|
||||||
|
connectedLabel.setStyle("-fx-font-size: 11; -fx-text-fill: rgba(255,255,255,0.75);");
|
||||||
|
|
||||||
Region spacer = new Region();
|
Region spacer = new Region();
|
||||||
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
Label userLabel = new Label("User: " + sessionManager.getUsername());
|
Label userChip = new Label(sessionManager.getUsername());
|
||||||
userLabel.setStyle("-fx-font-size: 12; -fx-text-fill: white;");
|
userChip.setStyle(
|
||||||
|
"-fx-font-size: 11; -fx-text-fill: white;" +
|
||||||
|
"-fx-background-color: rgba(255,255,255,0.15);" +
|
||||||
|
"-fx-background-radius: 12; -fx-padding: 4 10;"
|
||||||
|
);
|
||||||
|
|
||||||
Button logoutButton = new Button("Logout");
|
Button logoutBtn = new Button("Salir");
|
||||||
logoutButton.setStyle("-fx-font-size: 11; -fx-padding: 5;");
|
logoutBtn.setStyle(
|
||||||
logoutButton.setOnAction(e -> handleLogout());
|
"-fx-background-color: rgba(255,255,255,0.15); -fx-text-fill: white;" +
|
||||||
|
"-fx-background-radius: 8; -fx-padding: 5 12; -fx-cursor: hand; -fx-font-size: 11;"
|
||||||
topBar.getChildren().addAll(title, spacer, userLabel, logoutButton);
|
);
|
||||||
|
logoutBtn.setOnAction(e -> handleLogout(window));
|
||||||
HBox statusBar = new HBox(20);
|
|
||||||
statusBar.setPadding(new Insets(10, 15, 10, 15));
|
|
||||||
statusBar.setStyle("-fx-background-color: #f5f5f5;");
|
|
||||||
|
|
||||||
Label statusLabel = new Label("✓ Connected to: " + sessionManager.getBaseUrl());
|
|
||||||
statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #2e7d32;");
|
|
||||||
|
|
||||||
statusBar.getChildren().add(statusLabel);
|
|
||||||
|
|
||||||
header.getChildren().addAll(topBar, statusBar);
|
|
||||||
|
|
||||||
|
header.getChildren().addAll(appName, sep, connectedLabel, spacer, userChip, logoutBtn);
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TabPane createTabPane() {
|
private TabPane createTabPane() {
|
||||||
TabPane tabPane = new TabPane();
|
DashboardController dashboardController = new DashboardController(sessionManager);
|
||||||
|
ProductsController productsController = new ProductsController(sessionManager, dashboardController);
|
||||||
|
OrdersController ordersController = new OrdersController(sessionManager, dashboardController);
|
||||||
|
LogsController logsController = new LogsController(sessionManager);
|
||||||
|
SettingsController settingsController = new SettingsController(sessionManager);
|
||||||
|
|
||||||
|
Tab dashboardTab = tab("📊 Panel", dashboardController.createView());
|
||||||
|
Tab productsTab = tab("📦 Productos", productsController.createView());
|
||||||
|
Tab ordersTab = tab("🛒 Pedidos", ordersController.createView());
|
||||||
|
Tab logsTab = tab("📋 Historial", logsController.createView());
|
||||||
|
Tab settingsTab = tab("⚙ Ajustes", settingsController.createView());
|
||||||
|
|
||||||
|
TabPane tabPane = new TabPane(dashboardTab, productsTab, ordersTab, logsTab, settingsTab);
|
||||||
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||||
tabPane.setStyle("-fx-font-size: 12;");
|
tabPane.setStyle("-fx-font-size: 12;");
|
||||||
|
|
||||||
// Dashboard tab
|
|
||||||
dashboardController = new DashboardController(sessionManager);
|
|
||||||
Tab dashboardTab = new Tab("📊 Dashboard", dashboardController.createView());
|
|
||||||
dashboardTab.setStyle("-fx-padding: 10;");
|
|
||||||
|
|
||||||
// Products tab
|
|
||||||
productsController = new ProductsController(sessionManager, dashboardController);
|
|
||||||
Tab productsTab = new Tab("📦 Products", productsController.createView());
|
|
||||||
|
|
||||||
// Orders tab
|
|
||||||
ordersController = new OrdersController(sessionManager, dashboardController);
|
|
||||||
Tab ordersTab = new Tab("🛒 Orders", ordersController.createView());
|
|
||||||
|
|
||||||
// Logs tab
|
|
||||||
logsController = new LogsController(sessionManager);
|
|
||||||
Tab logsTab = new Tab("📋 Logs", logsController.createView());
|
|
||||||
|
|
||||||
tabPane.getTabs().addAll(dashboardTab, productsTab, ordersTab, logsTab);
|
|
||||||
|
|
||||||
return tabPane;
|
return tabPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogout() {
|
private Tab tab(String label, javafx.scene.Node content) {
|
||||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
Tab t = new Tab(label, content);
|
||||||
alert.setTitle("Logout");
|
t.setStyle("-fx-padding: 8 12;");
|
||||||
alert.setHeaderText("Are you sure?");
|
return t;
|
||||||
alert.setContentText("You will be logged out and returned to the login screen.");
|
}
|
||||||
|
|
||||||
|
private void handleLogout(Stage window) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
|
alert.setTitle("Cerrar sesión");
|
||||||
|
alert.setHeaderText("¿Cerrar sesión?");
|
||||||
|
alert.setContentText("Volverás a la pantalla de inicio de sesión.");
|
||||||
|
if (alert.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) return;
|
||||||
|
|
||||||
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
|
|
||||||
sessionManager.logout();
|
sessionManager.logout();
|
||||||
mainWindow.close();
|
window.close();
|
||||||
new LoginStage(sessionManager, primaryStage).show();
|
new LoginStage(sessionManager, primaryStage).show();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
<logger name="com.teterialosjuanjos.tfg.javafx_client" level="DEBUG"/>
|
<logger name="com.teterialosjuanjos.tfg.javafxclient" level="DEBUG"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
@ -1,93 +1,261 @@
|
||||||
/* Material Design Inspired CSS for TFG Sync Service Client */
|
/* TFG Sync Service Client — Material 3 (Android palette) */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
-fx-font-family: "Segoe UI", "Ubuntu", sans-serif;
|
-fx-font-family: "Segoe UI", "Roboto", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
-fx-background-color: #fafafa;
|
-fx-background-color: #FAFAFA;
|
||||||
-fx-font-size: 12;
|
-fx-font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* === BUTTONS === */
|
||||||
.button {
|
.button {
|
||||||
-fx-padding: 8;
|
-fx-padding: 8 16 8 16;
|
||||||
-fx-font-size: 12;
|
-fx-font-size: 12px;
|
||||||
-fx-text-fill: #333;
|
-fx-background-radius: 8;
|
||||||
-fx-background-color: #e0e0e0;
|
-fx-background-color: #E0E0E0;
|
||||||
-fx-background-radius: 4;
|
-fx-text-fill: #212121;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-border-width: 0;
|
||||||
}
|
}
|
||||||
|
.button:hover { -fx-background-color: #BDBDBD; }
|
||||||
|
.button:pressed { -fx-background-color: #9E9E9E; }
|
||||||
|
|
||||||
.button:hover {
|
.btn-primary {
|
||||||
-fx-background-color: #d0d0d0;
|
-fx-background-color: #1565C0;
|
||||||
}
|
-fx-text-fill: white;
|
||||||
|
|
||||||
/* Text Fields */
|
|
||||||
.text-field,
|
|
||||||
.password-field {
|
|
||||||
-fx-padding: 8;
|
|
||||||
-fx-border-color: #bdbdbd;
|
|
||||||
-fx-border-width: 0 0 1 0;
|
|
||||||
-fx-font-size: 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Labels */
|
|
||||||
.label {
|
|
||||||
-fx-text-fill: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.table-view {
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-border-color: #e0e0e0;
|
|
||||||
-fx-font-size: 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-view .column-header {
|
|
||||||
-fx-background-color: #f5f5f5;
|
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.btn-primary:hover { -fx-background-color: #1976D2; }
|
||||||
|
|
||||||
.table-row-cell {
|
.btn-success {
|
||||||
-fx-font-size: 11;
|
-fx-background-color: #2E7D32;
|
||||||
-fx-background-color: white;
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
.btn-success:hover { -fx-background-color: #388E3C; }
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
-fx-background-color: #E65100;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
.btn-warning:hover { -fx-background-color: #EF6C00; }
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
-fx-background-color: #546E7A;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { -fx-background-color: #607D8B; }
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: #1565C0;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-text-fill: #1565C0;
|
||||||
|
}
|
||||||
|
.btn-outline:hover { -fx-background-color: #E3F2FD; }
|
||||||
|
|
||||||
|
/* === FILTER CHIPS (ToggleButton) === */
|
||||||
|
.filter-chip {
|
||||||
|
-fx-background-color: #ECEFF1;
|
||||||
|
-fx-text-fill: #546E7A;
|
||||||
|
-fx-background-radius: 20;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-padding: 6 16 6 16;
|
||||||
|
-fx-font-size: 11px;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: #CFD8DC;
|
||||||
|
}
|
||||||
|
.filter-chip:selected {
|
||||||
|
-fx-background-color: #1565C0;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-border-color: #1565C0;
|
||||||
|
}
|
||||||
|
.filter-chip:hover {
|
||||||
|
-fx-background-color: #CFD8DC;
|
||||||
|
}
|
||||||
|
.filter-chip:selected:hover {
|
||||||
|
-fx-background-color: #1976D2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ComboBox */
|
/* === STATUS CHIPS (Label) === */
|
||||||
.combo-box,
|
.chip-synced {
|
||||||
.combo-box-base {
|
-fx-background-color: #E8F5E9;
|
||||||
|
-fx-text-fill: #2E7D32;
|
||||||
|
-fx-background-radius: 16;
|
||||||
|
-fx-padding: 2 10 2 10;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 10px;
|
||||||
|
}
|
||||||
|
.chip-pending {
|
||||||
|
-fx-background-color: #FFF3E0;
|
||||||
|
-fx-text-fill: #E65100;
|
||||||
|
-fx-background-radius: 16;
|
||||||
|
-fx-padding: 2 10 2 10;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 10px;
|
||||||
|
}
|
||||||
|
.chip-error-status {
|
||||||
|
-fx-background-color: #FFEBEE;
|
||||||
|
-fx-text-fill: #C62828;
|
||||||
|
-fx-background-radius: 16;
|
||||||
|
-fx-padding: 2 10 2 10;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 10px;
|
||||||
|
}
|
||||||
|
.chip-imported {
|
||||||
|
-fx-background-color: #ECEFF1;
|
||||||
|
-fx-text-fill: #546E7A;
|
||||||
|
-fx-background-radius: 16;
|
||||||
|
-fx-padding: 2 10 2 10;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 10px;
|
||||||
|
}
|
||||||
|
.chip-invoiced {
|
||||||
|
-fx-background-color: #E3F2FD;
|
||||||
|
-fx-text-fill: #1565C0;
|
||||||
|
-fx-background-radius: 16;
|
||||||
|
-fx-padding: 2 10 2 10;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === BANNERS === */
|
||||||
|
.banner-error {
|
||||||
|
-fx-background-color: #FFEBEE;
|
||||||
|
-fx-border-color: #EF9A9A;
|
||||||
|
-fx-border-width: 0 0 0 4;
|
||||||
|
-fx-background-radius: 4;
|
||||||
|
-fx-padding: 12;
|
||||||
|
}
|
||||||
|
.banner-warning {
|
||||||
|
-fx-background-color: #FFF8E1;
|
||||||
|
-fx-border-color: #FFD54F;
|
||||||
|
-fx-border-width: 0 0 0 4;
|
||||||
|
-fx-background-radius: 4;
|
||||||
|
-fx-padding: 12;
|
||||||
|
}
|
||||||
|
.banner-info {
|
||||||
|
-fx-background-color: #E3F2FD;
|
||||||
|
-fx-border-color: #90CAF9;
|
||||||
|
-fx-border-width: 0 0 0 4;
|
||||||
|
-fx-background-radius: 4;
|
||||||
|
-fx-padding: 12;
|
||||||
|
}
|
||||||
|
.banner-success {
|
||||||
|
-fx-background-color: #E8F5E9;
|
||||||
|
-fx-border-color: #A5D6A7;
|
||||||
|
-fx-border-width: 0 0 0 4;
|
||||||
|
-fx-background-radius: 4;
|
||||||
|
-fx-padding: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === TEXT FIELDS === */
|
||||||
|
.text-field, .password-field {
|
||||||
|
-fx-padding: 10;
|
||||||
-fx-background-color: white;
|
-fx-background-color: white;
|
||||||
-fx-border-color: #bdbdbd;
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-color: #BDBDBD;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
}
|
||||||
|
.text-field:focused, .password-field:focused {
|
||||||
|
-fx-border-color: #1565C0;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === LABELS === */
|
||||||
|
.label { -fx-text-fill: #212121; }
|
||||||
|
|
||||||
|
/* === TABLES === */
|
||||||
|
.table-view {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-border-color: #E0E0E0;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-font-size: 11px;
|
||||||
|
}
|
||||||
|
.table-view .column-header {
|
||||||
|
-fx-background-color: #F5F5F5;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-padding: 10 8 10 8;
|
||||||
|
-fx-text-fill: #546E7A;
|
||||||
|
-fx-font-size: 11px;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
.table-view .column-header-background {
|
||||||
|
-fx-background-color: #F5F5F5;
|
||||||
|
-fx-border-color: #E0E0E0;
|
||||||
|
-fx-border-width: 0 0 1 0;
|
||||||
|
}
|
||||||
|
.table-row-cell {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-padding: 4 0 4 0;
|
||||||
|
-fx-border-color: transparent transparent #F5F5F5 transparent;
|
||||||
|
-fx-border-width: 0 0 1 0;
|
||||||
|
}
|
||||||
|
.table-row-cell:odd { -fx-background-color: #FAFAFA; }
|
||||||
|
.table-row-cell:hover { -fx-background-color: #E3F2FD; }
|
||||||
|
.table-row-cell:selected { -fx-background-color: #BBDEFB; }
|
||||||
|
|
||||||
|
/* === TEXT AREA === */
|
||||||
|
.text-area {
|
||||||
|
-fx-background-color: #F5F5F5;
|
||||||
|
-fx-text-fill: #333;
|
||||||
|
-fx-font-size: 11px;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #E0E0E0;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
}
|
||||||
|
.text-area .content {
|
||||||
|
-fx-background-color: #F5F5F5;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === COMBO BOX === */
|
||||||
|
.combo-box {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-border-color: #BDBDBD;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-background-radius: 8;
|
||||||
-fx-padding: 5;
|
-fx-padding: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TextArea */
|
/* === TAB PANE === */
|
||||||
.text-area {
|
.tab-pane > .tab-header-area > .tab-header-background {
|
||||||
-fx-background-color: #f5f5f5;
|
-fx-background-color: #F5F5F5;
|
||||||
-fx-text-fill: #333;
|
-fx-border-color: #E0E0E0;
|
||||||
-fx-font-size: 10;
|
-fx-border-width: 0 0 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Separator */
|
|
||||||
.separator {
|
|
||||||
-fx-text-fill: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TabPane */
|
|
||||||
.tab-pane {
|
|
||||||
-fx-padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
-fx-padding: 10;
|
-fx-background-color: transparent;
|
||||||
|
-fx-padding: 10 20 10 20;
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
.tab:selected {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-border-color: #1565C0;
|
||||||
|
-fx-border-width: 0 0 3 0;
|
||||||
|
}
|
||||||
|
.tab .tab-label { -fx-text-fill: #757575; }
|
||||||
|
.tab:selected .tab-label { -fx-text-fill: #1565C0; -fx-font-weight: bold; }
|
||||||
|
.tab-pane { -fx-tab-min-height: 44px; }
|
||||||
|
|
||||||
.tab-header-background {
|
/* === SCROLL PANE === */
|
||||||
-fx-background-color: #f5f5f5;
|
.scroll-pane { -fx-background-color: transparent; -fx-border-color: transparent; }
|
||||||
}
|
.scroll-pane .viewport { -fx-background-color: transparent; }
|
||||||
|
.scroll-pane > .scroll-bar { -fx-background-color: transparent; }
|
||||||
|
|
||||||
/* CheckBox */
|
/* === SEPARATOR === */
|
||||||
.check-box {
|
.separator .line { -fx-border-color: #E0E0E0; -fx-border-width: 1; }
|
||||||
-fx-font-size: 11;
|
|
||||||
-fx-text-fill: #333;
|
/* === CHECK BOX === */
|
||||||
}
|
.check-box { -fx-font-size: 11px; -fx-text-fill: #212121; }
|
||||||
|
|
|
||||||
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