← all branches

feat/ui-impact

needs attentionviewing older commit
a7f8d64 · incrementalpre-PRreviewed 2026-07-25 01:56 UTC0H · 4M · 5L · 2I
The branch
Purpose
Build the Energia module — energy analytics dashboard for enterprise multi-site consumers, with findings, YoY like-for-like, and Comparativo drill-down, demoed on real Grupo Axo data.
Goal
Produce a complete energy analytics UI (Hallazgos, Resumen, Comparativo, Demanda, Medición, ESG, Pagos) backed by the findings domain entity and wired to production bill data.
Sub-goals
  • P1: Detection engine — 8 corpus-verified detectors, findings ledger domain entity
  • P2: Findings API (ts-rest, energia-entitled, handler/mapper/contract)
  • P3: Wire Hallazgos/Resumen/Demanda to real findings API
  • P4: Comparativo with real Axo consumption, like-for-like, DiD, scope/unit controls
  • P5: YoY like-for-like by complete calendar month + same-store
  • P6: Drill into a single store's year-over-year (this commit)
The changes (whole branch)
What
Added per-store YoY drill-down: clicking a store row in the 'Año vs año' table selects that store and switches scope to 'sucursal', surfacing the store's own month-by-month KPIs and bar chart. Scope selector now visible in all views. Amber warning for non-comparable stores.
Why
Complete the YoY view — portfolio totals were already there but users had no way to inspect individual stores. Row-click drill-down is the natural affordance (matches the Cohort Ranking pattern already on the chart view).
Areas
apps/platform/src/api+4160apps/platform/src/app/[locale]/(dashboard)/energia+33885domains/utility/src/finding+38790apps/platform/src/lib/demo+5911docs/specs+1220
Blast
71 files, +26063/-4 lines across the branch; all within apps/platform and domains/utility — no shared packages or infra touched. Risk is confined to the energia module and the finding domain entity.
demo-data-only no-migrations no-api-changes-this-commit
typecheck· node_modules not installed on runnertests· vitest not available on runnercoderabbit· no .coderabbit.yaml

Findings · 11

correctness3

medium

Tooltip formatter shows $0/0 kWh for null (no-data) months

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:707

yoyStoreChart emits null for months without complete coverage. Recharts Bar renders those slots at 0 height and still fires the Tooltip with v=null. The formatter does fmtCfe(Number(null), unit) = fmtCfe(0, unit), displaying '$0' or '0 kWh'. This fabricates a zero where the truth is 'no data', violating P5. Fix: guard the formatter — if (v == null) return '—'; return fmtCfe(Number(v), unit).

low

deltaPct precision inconsistency: KPI uses .toFixed(1), table uses .toFixed(0)

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:670

The per-store YoY KPI renders deltaPct with .toFixed(1), matching the portfolio KPI. The same value in the table row at line 775 uses .toFixed(0). A user can see '+3.4%' in the KPI and '+3%' in the table for the same store. Align to one precision.

low

Silent blank KPI+chart when scope='sucursal' and storeId unresolvable in yoy.rows

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:664

When scope='sucursal' and yoyStore is null, the KPI ternary renders null, the amber banner is suppressed, and the chart falls back to empty yoyStoreChart=[]. The Alcance toggle still shows 'Sucursal' and the store selector shows the selected store — the user sees a blank KPI+chart with no explanation. Cannot happen with static STORES today, but a future dynamic dataset would hit this. A fallback with three dash Kpi tiles and a note would make the failure explicit.

conventions2

medium

Clickable <tr> has no keyboard handler — inaccessible to non-mouse users

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:759

The YoY table row uses onClick to drill into a store (setStoreId + setScope), and renders cursor-pointer, but <tr> is not focusable and there is no onKeyDown/role='button'. Keyboard-only users cannot activate the drill-down. Fix: add role='button' tabIndex={0} and onKeyDown that fires on Enter/Space, or put the trigger inside the first <td> as an actual <button>.

info

P5 honest data: non-comparable stores correctly emit null bars with amber explanation

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:278

yoyStoreChart maps null for incomplete months, producing honest gaps in the BarChart. The amber banner (line 678) explains 'los huecos son meses sin cobertura verificable, no meses en cero'. Full P5 compliance. No action needed.

tests1

info

No component-level tests — consistent with energia/ page pattern

No component tests exist for any page.tsx in energia/. The correctness-critical logic (likeForLike, curMonths/prevMonths) is tested in _lib/__tests__/likeForLike.test.ts. The UI derivation in yoyStoreChart is a trivial index-lookup that doesn't warrant a separate test. Pattern is consistent — no action needed.

improvement5

medium

YoY bar chart has no empty-state guard — renders blank container silently

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:700

The BarChart renders unconditionally with data={scope==='sucursal' ? yoyStoreChart : yoyChart}. When yoyStoreChart is empty (no complete months for the selected store), recharts renders axes with no bars and no explanation. By contrast, the store LineChart (line 473) and portfolio DiD LineChart (line 510) both have an explicit empty-state <p>. Add the same guard here, consistent with the existing pattern.

medium

Scattered `scope === 'sucursal' && yoyStore` predicate — should be a named boolean

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:692

The compound condition scope === 'sucursal' && yoyStore is evaluated independently at lines 678, 692, 695, and 702. If the semantics of 'drill-down is active' change, all four sites must be updated. A single `const showStoreDrillDown = view === 'yoy' && scope === 'sucursal' && yoyStore != null` near the other memos eliminates the duplication and names the intent.

low

Index-aligned array pairing between yoy.window.cur and yoyStore.curMonths is implicit

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:276

yoyStoreChart zips yoy.window.cur[i] with yoyStore.curMonths[i] by position. The alignment holds because analyzeOne() builds curMonths as w.cur.map(...), but this is invisible at the call site. If likeForLike.ts ever filters or reorders curMonths, the index join breaks silently. Using a keyed structure (Map<ym, value>) or asserting length equality makes the invariant explicit.

low

Dead !yoy.window guard inside yoyStoreChart memo

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:275

The memo guards `if (!yoy.window || !yoyStore) return []`. In the rendered path, yoyStoreChart is only consumed inside the `yoy.window == null ? <empty card> : <...>` branch (line 630), so yoy.window is always non-null when yoyStoreChart is referenced. The !yoy.window branch is dead. It's harmless but misleads readers into thinking the memo can be consumed outside that guard.

low

yoyStoreChart dep array lists redundant `yoy` alongside derived `yoyStore`

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:281

dep array [yoy, yoyStore] invalidates the memo on any field change in the YoYResult object, including unrelated fields like monthly/excluded. Since yoyStore already derives from [yoy, storeId], the dep is equivalent to [yoy, storeId]. Simplifying to [yoyStore] (or [yoy, storeId]) makes the actual dependencies explicit and avoids spurious invalidation on unrelated yoy fields.

History · 15 commits

  1. 96ca7d7needs attentionincremental0H · 9M · 12L2026-07-25 16:40
  2. 25516f7needs attentionincremental5H · 7M · 8L2026-07-25 03:29
  3. a7f8d64needs attentionincremental0H · 4M · 5L2026-07-25 01:56current
  4. 7fc4ef0needs attentionincremental2H · 6M · 8L2026-07-24 21:04
  5. 027e5eaneeds attentionincremental4H · 9M · 8L2026-07-24 20:04
  6. 95a101eneeds attentionincremental3H · 6M · 5L2026-07-24 16:09
  7. 5d0d186needs attentionincremental1H · 3M · 5L2026-07-24 15:36
  8. c3c5121needs attentionincremental2H · 1M · 5L2026-07-24 15:17
  9. ded4e61needs attentionincremental2H · 3M · 9L2026-07-24 14:22
  10. 312a1f4needs attentionincremental1H · 4M · 4L2026-07-24 04:01
  11. b48af56needs attentionincremental1H · 6M · 6L2026-07-24 03:14
  12. 6d07cc8needs attentionincremental2H · 4M · 5L2026-07-24 00:50
  13. 261b55eneeds attentionincremental5H · 11M · 6L2026-07-24 00:38
  14. 5b8a252needs attentionincremental3H · 6M · 9L2026-07-24 00:19
  15. f29cc5bneeds attentionfull9H · 17M · 11L2026-07-23 23:07