From 54fd68aa0837c41da25d31ef66302303fdab1504 Mon Sep 17 00:00:00 2001 From: Daniel Arroyo Date: Sat, 15 Aug 2026 21:30:01 -0400 Subject: [PATCH] feat(historial): comprehensive enrichment of history page 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 --- .../com/l2/shots/history/HistoryService.java | 60 +- .../com/l2/shots/history/HistoryStats.java | 12 +- .../java/com/l2/shots/history/RunSummary.java | 29 +- .../META-INF/resources/static/css/app.css | 673 +++++++++++++++++ .../META-INF/resources/static/js/format.js | 17 + .../templates/PageResource/historial.html | 691 ++++++++++++++++++ 6 files changed, 1472 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/META-INF/resources/static/css/app.css create mode 100644 src/main/resources/META-INF/resources/static/js/format.js create mode 100644 src/main/resources/templates/PageResource/historial.html diff --git a/src/main/java/com/l2/shots/history/HistoryService.java b/src/main/java/com/l2/shots/history/HistoryService.java index 9db887d..abf25a7 100644 --- a/src/main/java/com/l2/shots/history/HistoryService.java +++ b/src/main/java/com/l2/shots/history/HistoryService.java @@ -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 listForUser(UUID userId) { return ProductionRun.list("userId = ?1 ORDER BY createdAt DESC", userId) .stream() - .map(RunSummary::from) + .map(r -> { + List 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 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 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 bestItems = parseItems(best.itemsJson); + List 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 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(); + } } } diff --git a/src/main/java/com/l2/shots/history/HistoryStats.java b/src/main/java/com/l2/shots/history/HistoryStats.java index e65178d..92084db 100644 --- a/src/main/java/com/l2/shots/history/HistoryStats.java +++ b/src/main/java/com/l2/shots/history/HistoryStats.java @@ -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 tiposBreakdown) { } diff --git a/src/main/java/com/l2/shots/history/RunSummary.java b/src/main/java/com/l2/shots/history/RunSummary.java index 223fd4a..7fe572d 100644 --- a/src/main/java/com/l2/shots/history/RunSummary.java +++ b/src/main/java/com/l2/shots/history/RunSummary.java @@ -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 tiposUsados, + boolean wasProfitable) { - public static RunSummary from(ProductionRun r) { + public static RunSummary from(ProductionRun r, List items) { + List 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); } } diff --git a/src/main/resources/META-INF/resources/static/css/app.css b/src/main/resources/META-INF/resources/static/css/app.css new file mode 100644 index 0000000..0dab478 --- /dev/null +++ b/src/main/resources/META-INF/resources/static/css/app.css @@ -0,0 +1,673 @@ +/* ============================================ + PICO BASE — Flat size, no viewport scaling + ============================================ */ +:root { + color-scheme: light; + + --pico-font-size: 100%; + + --pico-primary: #059669; + --pico-primary-background: #059669; + --pico-primary-border: #059669; + --pico-primary-color: #ffffff; + --pico-primary-underline: rgba(5, 150, 105, 0.5); + --pico-primary-hover: #047857; + --pico-primary-hover-background: #047857; + --pico-primary-hover-border: #047857; + --pico-primary-hover-color: #ffffff; + --pico-primary-focus: rgba(5, 150, 105, 0.25); + --pico-primary-inverse: #ffffff; + + --pico-background-color: #ffffff; + --pico-color: #1f2937; + --pico-card-background-color: #ffffff; + --pico-card-sectioning-background-color: #f9fafb; + --pico-card-border-color: #e5e7eb; + + --pico-h1-color: #111827; + --pico-h2-color: #1f2937; + --pico-h3-color: #1f2937; + --pico-h4-color: #1f2937; + --pico-h5-color: #1f2937; + --pico-h6-color: #1f2937; + + --pico-muted-color: #6b7280; + --pico-muted-border-color: #e5e7eb; + + --pico-form-element-background-color: #ffffff; + --pico-form-element-border-color: #d1d5db; + --pico-form-element-color: #1f2937; + --pico-form-element-placeholder-color: #9ca3af; + --pico-form-element-focus-color: #059669; + --pico-form-element-active-border-color: #059669; + --pico-form-element-disabled-background-color: #f9fafb; + --pico-form-element-disabled-border-color: #d1d5db; + --pico-form-element-disabled-opacity: 0.5; + + --pico-table-border-color: #e5e7eb; + --pico-table-row-stripped-background-color: #f9fafb; + + --pico-code-background-color: #f3f4f6; + --pico-code-color: #1f2937; + + --pico-spacing: 0.75rem; + --pico-block-spacing-vertical: 1rem; + --pico-block-spacing-horizontal: 1rem; + --pico-typography-spacing-vertical: 0.75rem; + --pico-typography-spacing-top: 0.5rem; + + --pico-card-box-shadow: none; +} + +@media (prefers-color-scheme: dark) { + :root { + --pico-primary-background: #059669; + --pico-primary-border: #059669; + --pico-primary-color: #ffffff; + --pico-background-color: #ffffff; + --pico-color: #1f2937; + --pico-card-background-color: #ffffff; + --pico-card-sectioning-background-color: #f9fafb; + --pico-card-border-color: #e5e7eb; + --pico-h1-color: #111827; + --pico-h2-color: #1f2937; + --pico-muted-color: #6b7280; + --pico-muted-border-color: #e5e7eb; + --pico-form-element-background-color: #ffffff; + --pico-form-element-border-color: #d1d5db; + --pico-form-element-color: #1f2937; + --pico-table-border-color: #e5e7eb; + } +} + +/* ============================================ + TYPOGRAPHY — Compact heading scale + ============================================ */ +h1 { font-size: 1.5rem; margin: 0 0 0.75rem; line-height: 1.25; font-weight: 700; } +h2 { font-size: 1.125rem; margin: 0 0 0.5rem; line-height: 1.3; font-weight: 600; } +h3 { font-size: 1rem; margin: 0 0 0.375rem; line-height: 1.35; font-weight: 600; } +h4 { font-size: 0.875rem; margin: 0 0 0.25rem; line-height: 1.4; font-weight: 600; } +h5, h6 { font-size: 0.875rem; margin: 0 0 0.25rem; line-height: 1.4; font-weight: 600; } + +/* ============================================ + FORM ELEMENTS — Compact + ============================================ */ +input, select, textarea { + font-size: 0.875rem; + padding: 0.375rem 0.625rem; + margin: 0; + height: auto; + line-height: 1.4; +} + +label { + font-size: 0.8125rem; + font-weight: 500; + margin-bottom: 0.25rem; + line-height: 1.3; +} + +label > span { + font-size: 0.8125rem; + color: var(--pico-muted-color); +} + +input[type="number"] { + font-family: monospace; +} + +/* ============================================ + BUTTONS — Compact + ============================================ */ +button { + font-size: 0.875rem; + padding: 0.375rem 0.875rem; + margin: 0; + font-weight: 500; + border-radius: 0.25rem; +} + +/* ============================================ + ARTICLES — Flat, no card shadow + ============================================ */ +article { + background: transparent; + box-shadow: none; + padding: 0; + margin-bottom: 1.5rem; + border: none; +} + +article > header { + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--pico-muted-border-color); +} + +article > header > h2 { + margin-bottom: 0.125rem; +} + +article > header > p { + margin-bottom: 0; + font-size: 0.8125rem; + color: var(--pico-muted-color); +} + +/* ============================================ + TABLES — Compact + ============================================ */ +table { + font-size: 0.875rem; + width: 100%; +} + +table th, table td { + padding: 0.375rem 0.5rem; +} + +table th { + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--pico-muted-color); + border-bottom: 1px solid var(--pico-muted-border-color); + background: var(--pico-card-sectioning-background-color); +} + +table tbody tr:hover { background: var(--pico-tr-hover-background-color); } +table tfoot tr { background: var(--pico-muted-border-color); } + +/* ============================================ + CONTAINER — Wider for data tables + ============================================ */ +.container { + max-width: 1200px; +} + +/* ============================================ + ANIMATIONS + ============================================ */ +@keyframes sparkle { + 0%, 100% { opacity: 0.6; transform: scale(0.9); } + 50% { opacity: 1; transform: scale(1.15); } +} + +.sparkle { + animation: sparkle 2.5s ease-in-out infinite; + transform-origin: center; +} + +/* ============================================ + UTILITIES + ============================================ */ +[x-cloak] { display: none !important; } + +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-mono { font-family: monospace; } +.text-sm { font-size: 0.875rem; } +.text-xs { font-size: 0.75rem; } +.text-muted { color: var(--pico-muted-color); } +.text-success { color: #059669; } +.text-error { color: #dc2626; } +.text-bold { font-weight: 600; } + +.flex-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } +.flex-col { display: flex; flex-direction: column; gap: 8px; } +.justify-between { justify-content: space-between; } +.justify-end { justify-content: flex-end; } + +.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; } +.grid-5 { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.75rem; } +.grid-8 { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0.75rem; } +.grid-auto { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); } + +.mb-0 { margin-bottom: 0; } +.mb-2 { margin-bottom: 0.5rem; } +.mb-3 { margin-bottom: 0.75rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } +.mt-1 { margin-top: 0.25rem; } +.mt-2 { margin-top: 0.5rem; } +.mt-4 { margin-top: 1rem; } +.p-4 { padding: 1rem; } +.p-6 { padding: 1.5rem; } +.px-4 { padding-left: 1rem; padding-right: 1rem; } +.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; } +.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } + +.w-full { width: 100%; } +.max-w-md { max-width: 28rem; margin-inline: auto; } + +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* ============================================ + HEADER — Compact global header + ============================================ */ +header h1 { + font-size: 1.125rem !important; + margin-top: 0 !important; + margin-bottom: 0.25rem !important; + line-height: 1.2; + font-weight: 600; +} + +header hgroup { + margin-bottom: 0 !important; +} + +header { + margin-bottom: 1rem; +} + +.header-meta { + font-size: 0.8125rem; + color: var(--pico-muted-color); + margin: 0; +} + +.header-nav { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + width: 100%; + box-sizing: border-box; +} +@media (min-width: 640px) { + .header-nav { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +.header-actions { + display: flex; + width: auto; + justify-content: flex-start; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} +@media (min-width: 640px) { + .header-actions { + justify-content: flex-end; + } +} + +.header-actions button.link-action { + background: none; + border: none; + color: var(--pico-primary); + cursor: pointer; + text-decoration: underline; + padding: 0; + font-size: 0.8125rem; +} + +.header-actions button.link-action:hover { + color: var(--pico-primary-hover); +} + +.link-action { + background: none; + border: none; + color: var(--pico-primary); + cursor: pointer; + font-size: 0.875rem; + text-decoration: underline; + padding: 0; +} +.link-action:hover { + color: var(--pico-primary-hover); +} + +/* ============================================ + MODAL + ============================================ */ +.modal-backdrop { + position: fixed; inset: 0; + background: rgba(0,0,0,0.4); + display: flex; align-items: center; justify-content: center; + z-index: 50; padding: 1rem; +} +.modal-content { + background: var(--pico-card-background-color); + border-radius: 8px; + max-width: 90vw; max-height: 90vh; + overflow-y: auto; + width: 100%; +} + +/* ============================================ + BADGES + ============================================ */ +.badge { + display: inline-block; + padding: 2px 8px; + font-size: 0.75rem; + border-radius: 4px; + font-weight: 500; +} +.badge-admin { background: #dbeafe; color: #1e3a8a; } +.badge-warning { background: #fef3c7; color: #92400e; } +.badge-success { background: #d1fae5; color: #065f46; } +.badge-error { background: #fee2e2; color: #991b1b; } + +/* ============================================ + TABS + ============================================ */ +.tabs { + display: flex; gap: 4px; + border-bottom: 1px solid var(--pico-muted-border-color); + padding: 0; + margin-bottom: 1rem; +} +.tabs li { display: contents; } +.tabs a { + padding: 8px 12px; + text-decoration: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + color: var(--pico-muted-color); + font-size: 0.875rem; +} +.tabs a.active { + border-bottom-color: var(--pico-primary); + color: var(--pico-primary); + font-weight: 500; +} + +/* ============================================ + TABLE CELLS — For grid-based inputs + ============================================ */ +.grade-badge { + display: inline-flex; align-items: center; justify-content: center; + width: 1.75rem; height: 1.75rem; + border-radius: 50%; + background: var(--pico-muted-border-color); + font-size: 0.75rem; font-weight: 600; +} + +.cell-label { + font-size: 0.75rem; font-weight: 500; + color: var(--pico-muted-color); + text-transform: uppercase; letter-spacing: 0.05em; +} +.cell-input { padding: 0.375rem 0.5rem; } +.cell-calculated { font-family: monospace; } + +/* ============================================ + ALERTS & MESSAGES + ============================================ */ +.bg-warning { background: #fef3c7; border: 1px solid #fcd34d; color: #92400e; padding: 0.75rem 1rem; border-radius: 4px; margin-bottom: 1rem; } +.bg-success-light { background: #d1fae5; border: 1px solid #6ee7b7; color: #065f46; padding: 0.75rem 1rem; border-radius: 4px; } +.bg-error-light { background: #fee2e2; border: 1px solid #fca5a5; color: #991b1b; padding: 0.75rem 1rem; border-radius: 4px; } + +.error-msg { color: #991b1b; background: #fee2e2; border: 1px solid #fca5a5; padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.875rem; } +.success-msg { color: #065f46; background: #d1fae5; border: 1px solid #6ee7b7; padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.875rem; } + +/* ============================================ + STATS GRID + ============================================ */ +.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; } +@media (max-width: 768px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } } + +.w-max { width: max-content; } + +/* ============================================ + LOGIN PAGE — centered card with branding + ============================================ */ +.login-page { + background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 50%, #f0f9ff 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.login-card { + width: 100%; + max-width: 26rem; + padding: 2.5rem 2rem; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.05); + background: white; + margin: 0; +} + +.login-logo { + width: 72px; + height: 72px; + margin: 0 auto 1rem; + display: block; + filter: drop-shadow(0 2px 4px rgba(5, 150, 105, 0.2)); +} + +.login-footer { + text-align: center; + margin-top: 1.5rem; + font-size: 0.75rem; + color: var(--pico-muted-color); +} + +/* ============================================ + HISTORIAL — SPARKLINE + ============================================ */ +.sparkline-container { + background: var(--pico-card-sectioning-background-color); + border: 1px solid var(--pico-muted-border-color); + border-radius: 6px; + padding: 0.75rem 1rem; + margin-bottom: 1rem; +} + +.sparkline { + display: flex; + align-items: flex-end; + gap: 4px; + height: 64px; + width: 100%; +} + +.sparkline-bar-wrap { + flex: 1; + height: 100%; + display: flex; + align-items: flex-end; + cursor: default; +} + +.sparkline-bar { + width: 100%; + min-height: 4px; + border-radius: 2px 2px 0 0; + transition: height 0.3s ease; +} + +.bar-positive { background: #059669; } +.bar-negative { background: #dc2626; } + +.sparkline-labels { + display: flex; + justify-content: space-between; + margin-top: 4px; +} + +/* ============================================ + HISTORIAL — TIPO BREAKDOWN + ============================================ */ +.tipos-breakdown { + background: var(--pico-card-sectioning-background-color); + border: 1px solid var(--pico-muted-border-color); + border-radius: 6px; + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; +} + +.tipos-bars { display: flex; flex-direction: column; gap: 0.5rem; } + +.tipo-bar-row { + display: grid; + grid-template-columns: 120px 1fr 80px; + align-items: center; + gap: 0.75rem; +} + +.tipo-bar-track { + height: 8px; + background: var(--pico-muted-border-color); + border-radius: 4px; + overflow: hidden; +} + +.tipo-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.fill-positive { background: #059669; } +.fill-negative { background: #dc2626; } + +/* ============================================ + HISTORIAL — SORTABLE COLUMNS + ============================================ */ +.sortable { + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +.sortable:hover { color: var(--pico-primary); } + +.sort-icon { + font-size: 0.65rem; + opacity: 0.6; + margin-left: 2px; +} + +/* ============================================ + HISTORIAL — DELTA / VARIANCE + ============================================ */ +.delta-badge { + display: inline-block; + font-size: 0.65rem; + padding: 1px 4px; + border-radius: 3px; + margin-left: 4px; + font-family: monospace; +} + +.delta-up { background: #d1fae5; color: #065f46; } +.delta-down { background: #fee2e2; color: #991b1b; } + +/* ============================================ + HISTORIAL — TIPO CHIPS + ============================================ */ +.tipo-chip { + display: inline-block; + font-size: 0.6rem; + padding: 1px 5px; + border-radius: 3px; + background: var(--pico-muted-border-color); + color: var(--pico-muted-color); + font-weight: 500; +} + +/* ============================================ + HISTORIAL — METRIC CARDS + ============================================ */ +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; } +.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; } + +.metric-card { + background: var(--pico-card-sectioning-background-color); + border: 1px solid var(--pico-muted-border-color); + padding: 0.625rem 0.75rem; + border-radius: 6px; +} + +.metric-card-sm { + background: transparent; + border: none; + border-top: 1px solid var(--pico-muted-border-color); + border-radius: 0; + padding: 0.375rem 0; +} + +.text-md { font-size: 1rem; } + +/* ============================================ + HISTORIAL — BADGE RUN + ============================================ */ +.badge-run { + display: inline-block; + padding: 2px 8px; + font-size: 0.7rem; + border-radius: 4px; + font-weight: 600; +} + +.badge-profit { background: #d1fae5; color: #065f46; } +.badge-loss { background: #fee2e2; color: #991b1b; } + +/* ============================================ + HISTORIAL — SNAPSHOT SECTION + ============================================ */ +.snapshot-section { + background: var(--pico-card-sectioning-background-color); + border: 1px solid var(--pico-muted-border-color); + border-radius: 6px; + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; +} + +.snapshot-grid { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.snapshot-row { + display: grid; + grid-template-columns: 80px 60px auto; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; +} + +/* ============================================ + HISTORIAL — DIFF BADGES + ============================================ */ +.diff-badge { + font-size: 0.65rem; + padding: 1px 4px; + border-radius: 3px; + font-family: monospace; + white-space: nowrap; +} + +.diff-up { background: #fee2e2; color: #991b1b; } +.diff-down { background: #d1fae5; color: #065f46; } +.diff-same { background: var(--pico-muted-border-color); color: var(--pico-muted-color); } + +/* ============================================ + MODAL — LARGER + ============================================ */ +.modal-lg .modal-content { + max-width: 900px; +} + +/* ============================================ + GAPS + ============================================ */ +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.gap-4 { gap: 1rem; } + diff --git a/src/main/resources/META-INF/resources/static/js/format.js b/src/main/resources/META-INF/resources/static/js/format.js new file mode 100644 index 0000000..0f5ee0b --- /dev/null +++ b/src/main/resources/META-INF/resources/static/js/format.js @@ -0,0 +1,17 @@ +window.fmt = { + adena(n) { + if (n == null) return '—'; + if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; + if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; + return n.toLocaleString('es-CL'); + }, + number(n) { + if (n == null) return '—'; + return n.toLocaleString('es-CL'); + }, + percent(pct) { + if (pct == null) return '—'; + const sign = pct > 0 ? '+' : ''; + return sign + pct.toFixed(1).replace('.', ',') + '%'; + }, +}; diff --git a/src/main/resources/templates/PageResource/historial.html b/src/main/resources/templates/PageResource/historial.html new file mode 100644 index 0000000..0e0fc54 --- /dev/null +++ b/src/main/resources/templates/PageResource/historial.html @@ -0,0 +1,691 @@ + + + + + + + Historial — Calculadora de Craft de Shots + + + + + + {#include partials/header.html /} + {#include partials/tab-bar.html /} + +
+ +
+

Cargando historial…

+
+ + + + + +
+
+

Corridas guardadas

+
+ + +
+
+ +
+

No hay corridas para mostrar.

+
+ +
+ + + + + + + + + + + + + + + + + +
+ Fecha + + Label + + Costo + + Venta + + Ganancia + + Margen + Costo/shotItemsAccion
+
+
+ + + +
+ +
+

+ Auto-guardado activo · Cambios persistidos en el servidor cada ~500ms +

+
+ + + + + + + + + + +