feat/energia-ui
needs attentionviewing older commit3da3216 · incrementalPR #303reviewed 2026-07-13 17:15 UTC5H · 14M · 14L · 4I- Purpose
- Preview/demo branch for the 7-screen energía enterprise energy management module. Not mergeable as-is: soft-launch.ts is temp-forced live for everyone, and pages use illustrative mock data from lib/demo/axoMockData.ts rather than the live ledger/detectors.
- Goal
- Show the energía feature running in-app on a Vercel preview with fully interactive buttons — no dead-end CTAs.
- Sub-goals
- SG-1: Port 7 pages (resumen, hallazgos, demanda, pagos, conciliación, esg, medición, sitios) as Next.js client pages under /energia/*
- SG-2: Reground to minimal v2 design + real Axo/Neto mock data + org switcher
- SG-3: Make demo fully interactive — all buttons open sheets (AclaracionSheet, InfoSheet) or navigate to sub-screens; CSV export wired; stat cards clickable
- What
- Adds 4 new _components files (AclaracionSheet, InfoSheet, csv.ts, sheetContent) implementing a full sheet system for the energía demo, plus incremental wiring across hallazgos, medicion, pagos, and resumen pages to replace dead-end buttons with interactive sheets and navigation.
- Why
- Commit 3da32163 completes the interactivity pass: every CTA in the demo now does something — finding rows open CFE claim sheets or cotización/sites sheets, 'Exportar ERP' triggers a real CSV download, stat cards navigate to sub-screens, and 'Agregar medidor' opens a context sheet.
- Areas
- apps/platform/src/app/[locale]/(dashboard)/energia+4820−1apps/platform/src/lib+25−0apps/platform/src/messages+28−0apps/platform/src/components+10−0packages/api+1−1domains/core+1−1
- Blast
- 29 files, +4875/-3 across full branch. Contained within apps/platform/energia/* and lib/demo/*. No backend, no migrations, no API surface changes. soft-launch.ts override is scoped to this branch (TEMP comment, revert before merge).
Findings · 36
correctness5
illustrativeRpu pads with '4' instead of '0' — all demo RPUs start with '44'
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:28
String(h).padStart(12, '4').slice(0, 12): the 32-bit hash max is 10 digits, so pad always fires and inserts leading '4' chars. Every generated RPU starts with '44' (e.g. '443523329657'). Real CFE RPUs use zero-padded sequences — a CFE-knowledgeable demo audience will notice. Fix: padStart(12, '0').
Progress bar under-fills: clamped minimums leave a visible gap at the right end
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:18
pct = (n) => Math.max(1.5, (n/v.identified)*100). For Neto, recovered=53k (0.34%) and inProcess=110k (0.71%) both clamp to 1.5%, making the total rendered width ~98.68% — a visible gap. The three segment values also don't sum to identified (15,013,000 vs 15,520,000). Fix: make operational the residual (identified − recovered − inProcess) or drop the floor.
Space key on clickable cards causes page scroll before navigation
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:72
onKeyDown={(e) => (e.key === ' ') && router.push(...)} — space bar default scroll isn't suppressed. router.push fires but the browser also scrolls. Fix: add e.preventDefault() when e.key === ' ' before calling router.push. Affects both stat cards (lines 72 and 89).
Non-urgent pagos CTA hardcodes 'Un solo sitio vencido' regardless of actual p.overdue value
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/page.tsx:144
When isUrgent is false, the sheet description renders 'Un solo sitio vencido — resuélvelo antes del próximo corte.' This is true only because Axo currently has p.overdue===1. When demonstrating with another org that has 0 or 3+ overdue, the text is factually wrong. The urgent branch above correctly uses p.overdue.toLocaleString('es-MX').
SitesListContent: conditional rand() call in jitter branch creates fragile call-count contract
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:74
rand() is skipped for jitter when finding.mxn is null, but still called for the city. Uniform within one invocation so no current collision, but the PRNG call contract is implicit. A future per-row condition varying rand() consumption mid-loop would silently break RPU uniqueness. Low severity for a demo, but flag for productionisation.
conventions14
Hardcoded es-MX strings bypass next-intl in AclaracionSheet
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:18
ui-patterns.md: all user-facing strings must come from messages/{locale}.json via next-intl. STEPS array, 'Estado del folio', 'Carta de aclaración (borrador)', 'Descargar carta (.txt)', and all body copy are hardcoded Spanish. Known/acknowledged for this demo branch — must be wired before merge.
Hardcoded es-MX strings bypass next-intl in sheetContent
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:39
ui-patterns.md: all user-facing strings must come from messages/{locale}.json via next-intl. 'Solicitud enviada a la red de instaladores Batu', 'Te contactamos en 48 h', column headers, and all copy are hardcoded. Known/acknowledged for demo branch.
Hardcoded es-MX strings bypass next-intl in hallazgos/page
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:29
STATUS_LABEL map, LIFECYCLE_STEPS labels, category labels ('Error CFE', 'Optimización') hardcoded. The energia layout.tsx already uses useTranslations('energia.nav'), confirming the namespace exists — this page should follow the same pattern before merge.
Hardcoded es-MX strings bypass next-intl in resumen/page
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:27
All body copy, labels, and CTA text hardcoded. Must use next-intl before merge.
Hardcoded es-MX strings bypass next-intl in pagos/page
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/page.tsx:33
All heading, label, body, and status text hardcoded. Must use next-intl before merge.
Em dash used as prose punctuation in resumen/page (ui-patterns.md violation)
apps/platform/src/app/[locale]/(dashboard)/energia/resumen/page.tsx:36
ui-patterns.md prohibits em dashes (—) in user-facing copy as punctuation. 'sitios — entre errores de CFE' uses it as a prose separator. Replace with a comma or rephrase as two sentences.
Em dashes used as prose punctuation in hallazgos/page
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:112
Lines 112 and 115 use '—' as sentence-level punctuation ('sitios —{' '}...' and 'con CFE — el resto'). ui-patterns.md prohibits this. Rephrase to avoid the em dash.
Em dashes used as prose punctuation in pagos/page
apps/platform/src/app/[locale]/(dashboard)/energia/pagos/page.tsx:146
Line 146 ('vencido — resuélvelo antes del próximo corte') and line 224 ('por recibo — fecha de vencimiento ... — vive en la lista') use '—' as prose punctuation. ui-patterns.md prohibits this. Rephrase.
Native <button> elements in medicion/page missing type="button"
apps/platform/src/app/[locale]/(dashboard)/energia/medicion/page.tsx:166
Site selector buttons at line 166 lack type="button". Browsers default to type="submit" inside a form, risking accidental submission. The pagos/page.tsx fix (adding type="button") in this very PR establishes the pattern — apply it consistently here.
KpiCard and MiniStat defined inline in medicion/page.tsx instead of _components/
apps/platform/src/app/[locale]/(dashboard)/energia/medicion/page.tsx:375
ui-patterns.md: route-scoped components go in _components/ with underscore prefix. KpiCard (line 375) and MiniStat (line 414) are reusable presentational components defined after the page export. Move to _components/kpi.tsx.
Hardcoded hex colors in Recharts config instead of CSS variables
apps/platform/src/app/[locale]/(dashboard)/energia/medicion/page.tsx:201
Lines 201, 202, 221, 222, 226 use hardcoded #7C3AED, #F1F5F9. Brand palette is defined as CSS variables (hsl(var(--primary)), etc.) for dark mode compatibility. Other chart usages in the codebase use 'hsl(var(--primary))' — follow the same pattern.
What-not-why JSDoc on InfoSheet
apps/platform/src/app/[locale]/(dashboard)/energia/_components/InfoSheet.tsx:13
CLAUDE.md: only comment when the WHY is non-obvious. The JSDoc block describes what the component does and lists callers — information readable from the props and callers. Remove it.
What-not-why JSDoc on pagosCsvRows
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:132
JSDoc 'Illustrative ERP-export rows (columns: sitio/rpu/periodo/monto/estatus)' describes the return shape (what), which is evident from the Record<string, string|number> return type. Remove per CLAUDE.md convention.
What-not-why JSDoc on SitesListContent
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:67
JSDoc 'Illustrative site list for "Ver sitios" / "Ver detalle"' describes what the component renders. Obvious from the function name and usage. Remove per CLAUDE.md convention.
tests8
escapeCell has no unit tests — CSV quoting bugs would silently corrupt exports
apps/platform/src/app/[locale]/(dashboard)/energia/_components/csv.ts:16
escapeCell is load-bearing for exportCsv. Needed cases: comma → quoted, double-quote → doubled, newline → quoted, number → stringified, undefined → empty string. Test via exportCsv rows since escapeCell is private.
exportCsv: no tests for CSV building or empty-rows edge case
apps/platform/src/app/[locale]/(dashboard)/energia/_components/csv.ts:26
Testable pure logic before the downloadFile side-effect: column order from first-row keys, BOM prefix, empty-array → empty file. Mock downloadFile and assert on the CSV string content.
illustrativeRpu: no unit tests for deterministic hash output
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:22
Pure function used in both letter text and SheetDescription. Tests: same input → same 12-char string, output is exactly 12 digits, empty string doesn't crash, two titles produce different RPUs. Export or test via component render.
seeded PRNG: no unit tests for determinism and range
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:10
seeded(seed) drives all illustrative data generation. Tests: same seed → same sequence, different seeds diverge, output always in [0, 1), rpuFrom always returns 12 digits. Locks the stable contract and catches future PRNG drift.
pagosCsvRows: no tests for row-count caps or monto derivation
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:133
Test: pending > 40 → exactly 40 rows; unknown > 20 → exactly 20 rows; monto is a positive integer; all rows have exactly {sitio, rpu, periodo, monto, estatus} keys. PRNG is deterministic so snapshot tests are feasible.
No __tests__ directory for energia _components/ utilities
apps/platform/src/app/[locale]/(dashboard)/energia/_components/csv.ts
canonical-form.md targets a co-located __tests__/ directory for every module. The energia _components/ has four files with testable pure logic and zero tests. Add before the utilities are promoted to production.
buildLetter uses new Date() — not deterministic, cannot snapshot-test as-is
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:31
buildLetter calls new Date() for the letter date. Inject date as a parameter before productionising so the full letter body can be asserted. No action needed on the demo branch.
No existing tests deleted in this diff
apps/platform/src/app/[locale]/(dashboard)/energia
Confirmed: the energia directory had no pre-existing test files before this commit. No regressions introduced.
improvement9
Dynamic import of csv.ts in AclaracionSheet creates a redundant bundle chunk
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:84
AclaracionSheet uses void import('./csv').then(...) for downloadFile, but sheetContent.tsx (co-bundled on the same route) statically imports './csv'. Next.js bundles csv.ts twice — once in the main chunk and once as a dynamic chunk. The async split adds a microtask delay with no benefit. Use a static import.
useMemo(buildLetter, [finding]) uses object identity — memo busts on every org refetch
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:78
The memo depends on the finding object reference. If the parent re-renders (org switch, query refetch), the object identity changes and the full letter is recomputed. The actual inputs are finding.title, finding.desc, finding.mxn, finding.sites — use those as deps.
BillsTableContent useMemo deps include values that don't affect the PRNG seed
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:180
The seed is `${data.id}-bills` — only data.id determines the row sequence. The other deps (p.overdue, p.overdueMxn, p.pending, p.pendingMxn) affect monto amounts correctly, but a future maintainer may 'simplify' to [data.id] and silently freeze the amounts. Add a brief inline comment explaining the dep split: seed from id, amounts from p.*.
Two separate illustrative RPU generators will produce different values for the same finding
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:22
illustrativeRpu(title) in AclaracionSheet (polynomial hash) and rpuFrom(rand) in sheetContent (PRNG draw) both produce 12-digit illustrative RPUs. They will disagree for the same finding. Extract a shared illustrativeRpu to _lib/demoUtils.ts or _lib/realData.ts.
FindingActionSheet dispatches on regex of a copy string — brittle routing
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:17
/cotiza/i.test(finding.action) couples UI routing to a Spanish copy label. A copy edit ('Pedir cotización') silently switches the component. Add an explicit sheetType discriminant ('cotizacion' | 'sites' | 'aclaracion') to the Finding type instead.
FindingActionSheet defined inside hallazgos/page.tsx — move to _components/ before it's reused
apps/platform/src/app/[locale]/(dashboard)/energia/hallazgos/page.tsx:13
FindingActionSheet is a non-trivial composite component (routing on category + regex). Per ui-patterns.md colocation rules, move to _components/FindingActionSheet.tsx before resumen or medicion needs it.
pagosCsvRows is a pure data function exported from a UI component file
apps/platform/src/app/[locale]/(dashboard)/energia/_components/sheetContent.tsx:133
pagosCsvRows is only used in pagos/page.tsx for CSV export — no JSX. The file name implies UI content. Move to _lib/demoData.ts or co-locate with csv.ts to clarify what is UI and what is logic.
PERIODO is hardcoded to 'mayo–junio 2026' in three separate literals that already differ
apps/platform/src/app/[locale]/(dashboard)/energia/_components/AclaracionSheet.tsx:29
The period string appears as 'mayo–junio 2026' (AclaracionSheet.tsx:29), 'may-jun 2026' (sheetContent.tsx:145), and 'may–jun 2026' (sheetContent.tsx:175) — already inconsistent (full months vs abbreviated, em-dash vs en-dash). Export a single DEMO_PERIOD constant from _lib/realData.ts.
exportCsv silently produces a 0-byte file when rows is empty
apps/platform/src/app/[locale]/(dashboard)/energia/_components/csv.ts:28
When rows is empty, downloadFile is called with an empty string, producing a 0-byte file with no header row. When real data flows through, consider: header-only row, early no-op return, or a toast warning the user the export was empty.