feat(calculadora): complete cost-split coverage in all modes
CI / Build Native (push) Successful in 6m56s

- Ranking de rentabilidad: add 3 columns (C. Cristales / C. Ore / C. Fee)
  alongside the existing Costo column. Update rankingFiltrado() to include
  the 3 sub-costs in its map output.
- Totales Presupuesto (3 sub-vistas: simple/maximizado/mezcla): add
  Distribución del costo block with bar + legend under existing 3 cards.
- Totales Objetivo: extend objetivoTotales() to aggregate sub-costs, then
  add the same distribution block.
- Totales Plan cristales: same distribution block (planTotalesCristales
  already aggregated sub-costs).
- Add 3 generic helpers pctCOf/pctOOf/pctFOf that take a totals object
  (handles both 'costoTotal' and 'costo' field names).
- Fix x-show condition for Presupuesto distribution: use .costo (the field
  actually returned by planTotalesPresupuesto) instead of .costoTotal.

Verified in browser across all 5 distribution points; percentages sum to
100% and math matches per-row calculations.
This commit is contained in:
2026-08-21 10:00:11 -04:00
parent 5c100024d7
commit e0ac9e844f
@@ -271,6 +271,9 @@
<th class="cell-label text-right sortable" @click="rankingOrdenarPor('shotsObtenidos')">
Shots <span class="sort-icon" x-text="rankingSortIcon('shotsObtenidos')"></span>
</th>
<th class="cell-label text-right" title="Costo en cristales">C. Cristales</th>
<th class="cell-label text-right" title="Costo en ore">C. Ore</th>
<th class="cell-label text-right" title="Fee Dwarven">C. Fee</th>
<th class="cell-label text-right">Costo</th>
<th class="cell-label text-right">Venta</th>
<th class="cell-label text-center">vs Mercado</th>
@@ -294,6 +297,9 @@
:class="r.margenPct > 0 ? 'text-success' : r.margenPct < 0 ? 'text-error' : ''"
x-text="fmtPct(r.margenPct)"></td>
<td class="text-mono text-right" x-text="r.shotsObtenidos"></td>
<td class="text-mono text-right" x-text="fmtAdena(r.costoCristales)"></td>
<td class="text-mono text-right" x-text="fmtAdena(r.costoOre)"></td>
<td class="text-mono text-right text-muted" x-text="fmtAdena(r.costoDwarven)"></td>
<td class="text-mono text-right" x-text="fmtAdena(r.costo)"></td>
<td class="text-mono text-right" x-text="fmtAdena(r.venta)"></td>
<td class="text-center">
@@ -473,6 +479,19 @@
<p class="text-lg text-mono text-bold" x-text="fmtAdena(Math.max(0, presupuestoAdena - planTotalesPresupuesto().costo))"></p>
</div>
</div>
<div class="mt-4" x-show="planTotalesPresupuesto().costo > 0">
<p class="text-xs text-muted mb-2">Distribución del costo</p>
<div class="cost-split-bar" style="width:100%;max-width:32rem">
<div class="cost-split-segment-cristales" :style="'width:' + pctCOf(planTotalesPresupuesto()) + '%'" :title="'Cristales: ' + fmtAdena(planTotalesPresupuesto().costoCristales) + ' (' + pctCOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-ore" :style="'width:' + pctOOf(planTotalesPresupuesto()) + '%'" :title="'Ore: ' + fmtAdena(planTotalesPresupuesto().costoOre) + ' (' + pctOOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-fee" :style="'width:' + pctFOf(planTotalesPresupuesto()) + '%'" :title="'Fee: ' + fmtAdena(planTotalesPresupuesto().costoDwarven) + ' (' + pctFOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
</div>
<div class="flex-row gap-3 mt-2" style="font-size:0.75rem;flex-wrap:wrap">
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-cristales"></span> Cristales <span x-text="pctCOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-ore"></span> Ore <span x-text="pctOOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-fee"></span> Fee <span x-text="pctFOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
</div>
</div>
</div>
</template>
<template x-if="presupuestoMaximizar && !presupuestoMezclar && planMaximizado().length > 0">
@@ -533,6 +552,19 @@
<p class="text-lg text-mono text-bold" x-text="fmtAdena(Math.max(0, presupuestoAdena - planTotalesPresupuesto().costo))"></p>
</div>
</div>
<div class="mt-4" x-show="planTotalesPresupuesto().costo > 0">
<p class="text-xs text-muted mb-2">Distribución del costo</p>
<div class="cost-split-bar" style="width:100%;max-width:32rem">
<div class="cost-split-segment-cristales" :style="'width:' + pctCOf(planTotalesPresupuesto()) + '%'" :title="'Cristales: ' + fmtAdena(planTotalesPresupuesto().costoCristales) + ' (' + pctCOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-ore" :style="'width:' + pctOOf(planTotalesPresupuesto()) + '%'" :title="'Ore: ' + fmtAdena(planTotalesPresupuesto().costoOre) + ' (' + pctOOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-fee" :style="'width:' + pctFOf(planTotalesPresupuesto()) + '%'" :title="'Fee: ' + fmtAdena(planTotalesPresupuesto().costoDwarven) + ' (' + pctFOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
</div>
<div class="flex-row gap-3 mt-2" style="font-size:0.75rem;flex-wrap:wrap">
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-cristales"></span> Cristales <span x-text="pctCOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-ore"></span> Ore <span x-text="pctOOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-fee"></span> Fee <span x-text="pctFOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
</div>
</div>
</div>
</template>
<template x-if="presupuestoMezclar && planMezcla().length > 0">
@@ -595,6 +627,19 @@
<p class="text-lg text-mono text-bold" x-text="fmtAdena(Math.max(0, presupuestoAdena - planTotalesPresupuesto().costo))"></p>
</div>
</div>
<div class="mt-4" x-show="planTotalesPresupuesto().costo > 0">
<p class="text-xs text-muted mb-2">Distribución del costo</p>
<div class="cost-split-bar" style="width:100%;max-width:32rem">
<div class="cost-split-segment-cristales" :style="'width:' + pctCOf(planTotalesPresupuesto()) + '%'" :title="'Cristales: ' + fmtAdena(planTotalesPresupuesto().costoCristales) + ' (' + pctCOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-ore" :style="'width:' + pctOOf(planTotalesPresupuesto()) + '%'" :title="'Ore: ' + fmtAdena(planTotalesPresupuesto().costoOre) + ' (' + pctOOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-fee" :style="'width:' + pctFOf(planTotalesPresupuesto()) + '%'" :title="'Fee: ' + fmtAdena(planTotalesPresupuesto().costoDwarven) + ' (' + pctFOf(planTotalesPresupuesto()).toFixed(1) + '%)'"></div>
</div>
<div class="flex-row gap-3 mt-2" style="font-size:0.75rem;flex-wrap:wrap">
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-cristales"></span> Cristales <span x-text="pctCOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-ore"></span> Ore <span x-text="pctOOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-fee"></span> Fee <span x-text="pctFOf(planTotalesPresupuesto()).toFixed(1) + '%'"></span></span>
</div>
</div>
</div>
</template>
<template x-if="presupuestoAdena > 0 && planActual().length === 0">
@@ -724,6 +769,19 @@
<p class="text-lg text-mono text-bold" x-text="objetivoTotales().shots"></p>
</div>
</div>
<div class="mt-4" x-show="objetivoTotales().costo > 0">
<p class="text-xs text-muted mb-2">Distribución del costo</p>
<div class="cost-split-bar" style="width:100%;max-width:32rem">
<div class="cost-split-segment-cristales" :style="'width:' + pctCOf(objetivoTotales()) + '%'" :title="'Cristales: ' + fmtAdena(objetivoTotales().costoCristales) + ' (' + pctCOf(objetivoTotales()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-ore" :style="'width:' + pctOOf(objetivoTotales()) + '%'" :title="'Ore: ' + fmtAdena(objetivoTotales().costoOre) + ' (' + pctOOf(objetivoTotales()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-fee" :style="'width:' + pctFOf(objetivoTotales()) + '%'" :title="'Fee: ' + fmtAdena(objetivoTotales().costoDwarven) + ' (' + pctFOf(objetivoTotales()).toFixed(1) + '%)'"></div>
</div>
<div class="flex-row gap-3 mt-2" style="font-size:0.75rem;flex-wrap:wrap">
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-cristales"></span> Cristales <span x-text="pctCOf(objetivoTotales()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-ore"></span> Ore <span x-text="pctOOf(objetivoTotales()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-fee"></span> Fee <span x-text="pctFOf(objetivoTotales()).toFixed(1) + '%'"></span></span>
</div>
</div>
</div>
</article>
@@ -836,6 +894,19 @@
x-text="fmtAdena(planTotalesCristales().ganancia)"></p>
</div>
</div>
<div class="mt-4" x-show="planTotalesCristales().costo > 0">
<p class="text-xs text-muted mb-2">Distribución del costo</p>
<div class="cost-split-bar" style="width:100%;max-width:32rem">
<div class="cost-split-segment-cristales" :style="'width:' + pctCOf(planTotalesCristales()) + '%'" :title="'Cristales: ' + fmtAdena(planTotalesCristales().costoCristales) + ' (' + pctCOf(planTotalesCristales()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-ore" :style="'width:' + pctOOf(planTotalesCristales()) + '%'" :title="'Ore: ' + fmtAdena(planTotalesCristales().costoOre) + ' (' + pctOOf(planTotalesCristales()).toFixed(1) + '%)'"></div>
<div class="cost-split-segment-fee" :style="'width:' + pctFOf(planTotalesCristales()) + '%'" :title="'Fee: ' + fmtAdena(planTotalesCristales().costoDwarven) + ' (' + pctFOf(planTotalesCristales()).toFixed(1) + '%)'"></div>
</div>
<div class="flex-row gap-3 mt-2" style="font-size:0.75rem;flex-wrap:wrap">
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-cristales"></span> Cristales <span x-text="pctCOf(planTotalesCristales()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-ore"></span> Ore <span x-text="pctOOf(planTotalesCristales()).toFixed(1) + '%'"></span></span>
<span class="text-mono"><span class="cost-legend-dot cost-split-segment-fee"></span> Fee <span x-text="pctFOf(planTotalesCristales()).toFixed(1) + '%'"></span></span>
</div>
</div>
</div>
</template>
@@ -972,6 +1043,21 @@
return t.costoTotal > 0 ? (t.costoDwarven * 100.0) / t.costoTotal : 0;
},
pctCOf(t) {
const total = t.costoTotal != null ? t.costoTotal : t.costo;
return total > 0 ? (t.costoCristales * 100.0) / total : 0;
},
pctOOf(t) {
const total = t.costoTotal != null ? t.costoTotal : t.costo;
return total > 0 ? (t.costoOre * 100.0) / total : 0;
},
pctFOf(t) {
const total = t.costoTotal != null ? t.costoTotal : t.costo;
return total > 0 ? (t.costoDwarven * 100.0) / total : 0;
},
canSave() {
return this.totalesGlobal().cristalesUsados > 0 && this.saveStatus !== 'saving';
},
@@ -1016,6 +1102,9 @@
margenPct,
shotsObtenidos: c.shotsObtenidos,
costo: c.costoTotal,
costoCristales: c.costoCristales,
costoOre: c.costoOre,
costoDwarven: c.costoDwarven,
venta: c.valorVenta,
precioMercado,
costoPorShot,
@@ -1141,16 +1230,20 @@
objetivoTotales() {
const s = this.store().state;
if (!s) return { costo: 0, venta: 0, ganancia: 0, shots: 0 };
if (!s) return { costo: 0, venta: 0, ganancia: 0, shots: 0, costoCristales: 0, costoOre: 0, costoDwarven: 0 };
let costo = 0, venta = 0, shots = 0;
let costoCristales = 0, costoOre = 0, costoDwarven = 0;
for (const f of s.formulas) {
const s2 = this.objetivoShots[f.id] || 0;
if (s2 <= 0) continue;
shots += s2;
costo += this.objetivoCosto(f);
venta += this.objetivoVenta(f);
costoCristales += this.objetivoCostoCristales(f);
costoOre += this.objetivoCostoOre(f);
costoDwarven += this.objetivoCostoDwarven(f);
}
return { costo, venta, ganancia: venta - costo, shots };
return { costo, venta, ganancia: venta - costo, shots, costoCristales, costoOre, costoDwarven };
},
planPorPresupuesto() {