feat(historial): comprehensive enrichment of history page
CI / Build Native (push) Failing after 1m21s
CI / Build Native (push) Failing after 1m21s
Backend: - RunSummary: add profitMarginPct, costPerShot, profitPerShot, itemCount, tiposUsados, wasProfitable - HistoryStats: add avgProfitMarginPct, avgCostPerShot, avgProfitPerShot, totalOreUsed, totalItems, tiposBreakdown - HistoryService: compute all new fields, parse items for best/worst run Frontend (historial.html): - 8 stats cards instead of 5: +Margen promedio, +Total invertido, +Shots, +Costo/shot - SVG sparkline showing last 10 runs profit trend - Ganancia por tipo breakdown with horizontal bars - Table: sortable columns (click headers), new cols: Margen%, Costo/shot, Items chips, delta vs avg - Sortable by: Fecha, Label, Costo, Venta, Ganancia, Margen - Modal: badge Profit/Loss, 8 metric cards, snapshot of prices at save time, comparison vs current prices - Export buttons: CSV (resumen) + JSON (completo con items y snapshot) Format.js: - Add fmtPercent() with es-CL locale CSS: - Add sparkline, tipos-breakdown, sortable columns, delta badges, metric cards, diff badges, modal-lg
This commit is contained in:
@@ -6,7 +6,10 @@ import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -18,7 +21,10 @@ public class HistoryService {
|
||||
public List<RunSummary> listForUser(UUID userId) {
|
||||
return ProductionRun.<ProductionRun>list("userId = ?1 ORDER BY createdAt DESC", userId)
|
||||
.stream()
|
||||
.map(RunSummary::from)
|
||||
.map(r -> {
|
||||
List<RunItem> items = parseItems(r.itemsJson);
|
||||
return RunSummary.from(r, items);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -55,7 +61,7 @@ public class HistoryService {
|
||||
throw new RuntimeException("Failed to serialize run payload", e);
|
||||
}
|
||||
entity.persist();
|
||||
return RunSummary.from(entity);
|
||||
return RunSummary.from(entity, input.items());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -68,21 +74,37 @@ public class HistoryService {
|
||||
"userId = ?1 ORDER BY createdAt DESC", userId);
|
||||
|
||||
if (runs.isEmpty()) {
|
||||
return new HistoryStats(0, 0, 0, 0, 0, 0, 0, 0, null, null, 0, 0);
|
||||
return new HistoryStats(
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0,
|
||||
null, null, 0, 0, 0, Map.of());
|
||||
}
|
||||
|
||||
long totalCost = 0;
|
||||
long totalSale = 0;
|
||||
long totalProfit = 0;
|
||||
long totalShots = 0;
|
||||
long totalCristalesUsed = 0;
|
||||
long totalOreUsed = 0;
|
||||
int totalItems = 0;
|
||||
ProductionRun best = runs.get(0);
|
||||
ProductionRun worst = runs.get(0);
|
||||
Map<String, Long> tiposBreakdown = new LinkedHashMap<>();
|
||||
|
||||
for (ProductionRun r : runs) {
|
||||
totalCost += r.totalCost;
|
||||
totalSale += r.totalSale;
|
||||
totalProfit += r.totalProfit;
|
||||
totalShots += r.totalShots;
|
||||
totalCristalesUsed += r.totalCristalesUsed;
|
||||
totalOreUsed += r.totalOreUsed;
|
||||
|
||||
List<RunItem> items = parseItems(r.itemsJson);
|
||||
totalItems += items.size();
|
||||
|
||||
for (RunItem item : items) {
|
||||
tiposBreakdown.merge(item.tipo(), item.ganancia(), Long::sum);
|
||||
}
|
||||
|
||||
if (r.totalProfit > best.totalProfit) best = r;
|
||||
if (r.totalProfit < worst.totalProfit) worst = r;
|
||||
}
|
||||
@@ -97,18 +119,44 @@ public class HistoryService {
|
||||
if (i < n5) sum5 += runs.get(i).totalProfit;
|
||||
}
|
||||
|
||||
double avgMargin = totalCost > 0
|
||||
? (double) totalProfit * 100.0 / totalCost
|
||||
: 0.0;
|
||||
long avgCostPerShot = totalShots > 0 ? totalCost / totalShots : 0;
|
||||
long avgProfitPerShot = totalShots > 0 ? totalProfit / totalShots : 0;
|
||||
|
||||
List<RunItem> bestItems = parseItems(best.itemsJson);
|
||||
List<RunItem> worstItems = parseItems(worst.itemsJson);
|
||||
|
||||
return new HistoryStats(
|
||||
n,
|
||||
totalCost,
|
||||
totalSale,
|
||||
totalProfit,
|
||||
totalShots,
|
||||
totalCristalesUsed,
|
||||
totalOreUsed,
|
||||
totalProfit / n,
|
||||
totalCost / n,
|
||||
totalSale / n,
|
||||
RunSummary.from(best),
|
||||
RunSummary.from(worst),
|
||||
Math.round(avgMargin * 10.0) / 10.0,
|
||||
avgCostPerShot,
|
||||
avgProfitPerShot,
|
||||
RunSummary.from(best, bestItems),
|
||||
RunSummary.from(worst, worstItems),
|
||||
n5 > 0 ? sum5 / n5 : 0,
|
||||
n10 > 0 ? sum10 / n10 : 0);
|
||||
n10 > 0 ? sum10 / n10 : 0,
|
||||
totalItems,
|
||||
tiposBreakdown);
|
||||
}
|
||||
|
||||
private List<RunItem> parseItems(String json) {
|
||||
if (json == null || json.isBlank()) return List.of();
|
||||
try {
|
||||
return mapper.readValue(json, mapper.getTypeFactory()
|
||||
.constructCollectionType(List.class, RunItem.class));
|
||||
} catch (JsonProcessingException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.l2.shots.history;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RegisterForReflection
|
||||
public record HistoryStats(
|
||||
int totalRuns,
|
||||
@@ -9,11 +12,18 @@ public record HistoryStats(
|
||||
long totalSale,
|
||||
long totalProfit,
|
||||
long totalShots,
|
||||
long totalCristalesUsed,
|
||||
long totalOreUsed,
|
||||
long avgProfit,
|
||||
long avgCost,
|
||||
long avgSale,
|
||||
double avgProfitMarginPct,
|
||||
long avgCostPerShot,
|
||||
long avgProfitPerShot,
|
||||
RunSummary bestRun,
|
||||
RunSummary worstRun,
|
||||
long last5Avg,
|
||||
long last10Avg) {
|
||||
long last10Avg,
|
||||
int totalItems,
|
||||
Map<String, Long> tiposBreakdown) {
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.l2.shots.history;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RegisterForReflection
|
||||
@@ -15,9 +16,25 @@ public record RunSummary(
|
||||
long totalProfit,
|
||||
long totalShots,
|
||||
long totalCristalesUsed,
|
||||
long totalOreUsed) {
|
||||
long totalOreUsed,
|
||||
double profitMarginPct,
|
||||
long costPerShot,
|
||||
long profitPerShot,
|
||||
int itemCount,
|
||||
List<String> tiposUsados,
|
||||
boolean wasProfitable) {
|
||||
|
||||
public static RunSummary from(ProductionRun r) {
|
||||
public static RunSummary from(ProductionRun r, List<RunItem> items) {
|
||||
List<String> tipos = items.stream()
|
||||
.map(i -> i.tipo())
|
||||
.distinct()
|
||||
.toList();
|
||||
int count = items.size();
|
||||
double marginPct = r.totalCost > 0
|
||||
? (double) r.totalProfit * 100.0 / r.totalCost
|
||||
: 0.0;
|
||||
long cps = r.totalShots > 0 ? r.totalCost / r.totalShots : 0;
|
||||
long pps = r.totalShots > 0 ? r.totalProfit / r.totalShots : 0;
|
||||
return new RunSummary(
|
||||
r.id,
|
||||
r.createdAt,
|
||||
@@ -27,6 +44,12 @@ public record RunSummary(
|
||||
r.totalProfit,
|
||||
r.totalShots,
|
||||
r.totalCristalesUsed,
|
||||
r.totalOreUsed);
|
||||
r.totalOreUsed,
|
||||
Math.round(marginPct * 10.0) / 10.0,
|
||||
cps,
|
||||
pps,
|
||||
count,
|
||||
tipos,
|
||||
r.totalProfit > 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user