feat(calculadora): cost split breakdown + plan por cristales mode
CI / Build Native (push) Has been cancelled
CI / Build Native (push) Has been cancelled
- Add costoCristales/costoOre/costoDwarven fields to calc.js results and
RunItem/RunIn/RunSummary/HistoryStats records (no DB migration needed —
items stored as JSON in TEXT column).
- Show 4 explicit columns (C. Cristales / C. Ore / C. Fee / Costo total)
in all calculator tables: Cristales mode, Presupuesto (simple/maximizado/
mezcla), and Objetivo. Totales globales now includes a 3-segment
distribution bar with %.
- Historial: 3 new stats cards + global distribution bar; per-run modal
shows breakdown + bar; per-item table has 3 sub-columns; CSV export
extended. CSS: cost-split-bar, cost-split-segment-{cristales,ore,fee},
cost-legend-dot.
- New 'Plan por cristales' mode (4th): single-grade + quantity input with
greedy max-ROI/cristal algorithm and shared cost split columns.
Verificado end-to-end en navegador.
This commit is contained in:
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -75,7 +74,9 @@ public class HistoryService {
|
||||
|
||||
if (runs.isEmpty()) {
|
||||
return new HistoryStats(
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0.0, 0.0, 0.0,
|
||||
0, 0, 0, 0.0, 0, 0,
|
||||
null, null, 0, 0, 0, Map.of());
|
||||
}
|
||||
|
||||
@@ -85,6 +86,9 @@ public class HistoryService {
|
||||
long totalShots = 0;
|
||||
long totalCristalesUsed = 0;
|
||||
long totalOreUsed = 0;
|
||||
long totalCostCristales = 0;
|
||||
long totalCostOre = 0;
|
||||
long totalCostDwarven = 0;
|
||||
int totalItems = 0;
|
||||
ProductionRun best = runs.get(0);
|
||||
ProductionRun worst = runs.get(0);
|
||||
@@ -103,6 +107,9 @@ public class HistoryService {
|
||||
|
||||
for (RunItem item : items) {
|
||||
tiposBreakdown.merge(item.tipo(), item.ganancia(), Long::sum);
|
||||
totalCostCristales += item.costoCristales();
|
||||
totalCostOre += item.costoOre();
|
||||
totalCostDwarven += item.costoDwarven();
|
||||
}
|
||||
|
||||
if (r.totalProfit > best.totalProfit) best = r;
|
||||
@@ -125,6 +132,10 @@ public class HistoryService {
|
||||
long avgCostPerShot = totalShots > 0 ? totalCost / totalShots : 0;
|
||||
long avgProfitPerShot = totalShots > 0 ? totalProfit / totalShots : 0;
|
||||
|
||||
double pctC = totalCost > 0 ? (double) totalCostCristales * 100.0 / totalCost : 0.0;
|
||||
double pctO = totalCost > 0 ? (double) totalCostOre * 100.0 / totalCost : 0.0;
|
||||
double pctD = totalCost > 0 ? (double) totalCostDwarven * 100.0 / totalCost : 0.0;
|
||||
|
||||
List<RunItem> bestItems = parseItems(best.itemsJson);
|
||||
List<RunItem> worstItems = parseItems(worst.itemsJson);
|
||||
|
||||
@@ -136,6 +147,12 @@ public class HistoryService {
|
||||
totalShots,
|
||||
totalCristalesUsed,
|
||||
totalOreUsed,
|
||||
totalCostCristales,
|
||||
totalCostOre,
|
||||
totalCostDwarven,
|
||||
round1(pctC),
|
||||
round1(pctO),
|
||||
round1(pctD),
|
||||
totalProfit / n,
|
||||
totalCost / n,
|
||||
totalSale / n,
|
||||
@@ -150,6 +167,10 @@ public class HistoryService {
|
||||
tiposBreakdown);
|
||||
}
|
||||
|
||||
private static double round1(double v) {
|
||||
return Math.round(v * 10.0) / 10.0;
|
||||
}
|
||||
|
||||
private List<RunItem> parseItems(String json) {
|
||||
if (json == null || json.isBlank()) return List.of();
|
||||
try {
|
||||
@@ -159,4 +180,4 @@ public class HistoryService {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,12 @@ public record HistoryStats(
|
||||
long totalShots,
|
||||
long totalCristalesUsed,
|
||||
long totalOreUsed,
|
||||
long totalCostCristales,
|
||||
long totalCostOre,
|
||||
long totalCostDwarven,
|
||||
double costCristalesPct,
|
||||
double costOrePct,
|
||||
double costDwarvenPct,
|
||||
long avgProfit,
|
||||
long avgCost,
|
||||
long avgSale,
|
||||
@@ -26,4 +32,4 @@ public record HistoryStats(
|
||||
long last10Avg,
|
||||
int totalItems,
|
||||
Map<String, Long> tiposBreakdown) {
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ public record RunIn(
|
||||
long totalShots,
|
||||
long totalCristalesUsed,
|
||||
long totalOreUsed,
|
||||
long totalCostCristales,
|
||||
long totalCostOre,
|
||||
long totalCostDwarven,
|
||||
List<RunItem> items,
|
||||
RunSnapshot snapshot) {
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,10 @@ public record RunItem(
|
||||
int oreNecesario,
|
||||
int crafteosPosibles,
|
||||
int shotsObtenidos,
|
||||
long costoCristales,
|
||||
long costoOre,
|
||||
long costoDwarven,
|
||||
long costoTotal,
|
||||
long valorVenta,
|
||||
long ganancia) {
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ public record RunSummary(
|
||||
long totalShots,
|
||||
long totalCristalesUsed,
|
||||
long totalOreUsed,
|
||||
long costCristales,
|
||||
long costOre,
|
||||
long costDwarven,
|
||||
double profitMarginPct,
|
||||
long costPerShot,
|
||||
long profitPerShot,
|
||||
@@ -25,6 +28,10 @@ public record RunSummary(
|
||||
boolean wasProfitable) {
|
||||
|
||||
public static RunSummary from(ProductionRun r, List<RunItem> items) {
|
||||
return from(r, items, sumItems(items));
|
||||
}
|
||||
|
||||
public static RunSummary from(ProductionRun r, List<RunItem> items, ItemCostSplit itemCosts) {
|
||||
List<String> tipos = items.stream()
|
||||
.map(i -> i.tipo())
|
||||
.distinct()
|
||||
@@ -45,6 +52,9 @@ public record RunSummary(
|
||||
r.totalShots,
|
||||
r.totalCristalesUsed,
|
||||
r.totalOreUsed,
|
||||
itemCosts.cristales(),
|
||||
itemCosts.ore(),
|
||||
itemCosts.dwarven(),
|
||||
Math.round(marginPct * 10.0) / 10.0,
|
||||
cps,
|
||||
pps,
|
||||
@@ -52,4 +62,16 @@ public record RunSummary(
|
||||
tipos,
|
||||
r.totalProfit > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemCostSplit sumItems(List<RunItem> items) {
|
||||
long c = 0, o = 0, d = 0;
|
||||
for (RunItem i : items) {
|
||||
c += i.costoCristales();
|
||||
o += i.costoOre();
|
||||
d += i.costoDwarven();
|
||||
}
|
||||
return new ItemCostSplit(c, o, d);
|
||||
}
|
||||
|
||||
public record ItemCostSplit(long cristales, long ore, long dwarven) {}
|
||||
}
|
||||
Reference in New Issue
Block a user