Saltar al contenido principal

Console module section tables — columns + joins

:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::

reference · designer · updated 2026-06-27 · source

How each console module/section list page (/org/<slug>/<module>/<section>, effort #963) presents its rows: readable columns, human names instead of FK ids, formatted numbers/dates, and categorical badges. The reference is the shipped admin screen language — packages/ui/src/admin/screens/enrollments-view.tsx and payments-view.tsx already solved this; the console tables align to it (on the Amber/ink console matiz, color-scope.md). Sibling: console-nav-ia-spec.md (nav/IA), console-structure-spec.md (IA model).

Frontend implements off this spec. Backend supplies the joined fields (see § Contract impact).

The problem this repeals

The seeded console renders ONE generic shape for every entity — ConsoleEntityRow { id, label, secondary, meta, fields } → columns Nombre / Detalle / Meta / ID (apps/console/src/components/datos-browser.tsx RegistrosTab + the SectionDataTable in #965/#966, fed by apps/api/.../console-routes.ts). It is unreadable because:

  • enrollment/payment send label: <uuid> — a row leads with an id, not a person.
  • secondary mixes free strings ("card · pending"); meta is "$"+amount (no grouping).
  • Headers Detalle / Meta / ID name nothing — "¿Monto o amount? ID no sé a qué se refiere."

The cure is per-section columns (not one normalized row), with joins resolving FK ids to names and Intl formatting everywhere. The DetailPanel (fields[]) is fine — keep it.

Cross-section rules (apply to every table)

RuleDecision
Column namesReal, per-section names. Kill generic Detalle / Meta.
ID placementFirst column, de-emphasized: type-mono text-fg-muted, 8-char short (id.slice(0,8)), full id in title=. Never the row's headline.
Names over idsAny FK column shows the joined human name; FK-id fallback only when the join is null (shortId).
Integersnew Intl.NumberFormat('es-MX')19610 renders 19,610. Counts in cards AND table cells.
MoneyIntl.NumberFormat('es-MX',{style:'currency',currency:'MXN',maximumFractionDigits:0})$19,610. Right-aligned, type-mono (tabular). Centavos only if the source carries them.
DatesIntl.DateTimeFormat('es-MX',{day:'numeric',month:'short',year:'numeric'})27 jun 2026. type-mono text-fg-muted (tabular).
Badges / tagsCategorical fields render as Badge (@tedos/ui/admin ui/badge.tsx) — variants success info warn danger muted. Categories, never the accent (color-scope: console accent = Amber).
Accent budgetThese are read-only glances → default zero Amber moments. If one is wanted, it lives in module nav (active section under-rule), never in a table cell (G17).
Row interactionRow click → DetailPanel (keep). hover:bg-elevated, full-row <button>, focus-visible.
4 statesloading (skeletal, layout-matched) · empty (honest, per § below) · error (ReadError) · success. Distinguish "no rows at all" from "no filter matches."

Component pattern

Two valid hosts, pick per surface:

  • #963 today ships SectionDataTable (the DataTable TanStack family) — keep it; it gives search/sort/columns + the server pagination already wired (#965/#966). The redesign below changes the columns, not the host.
  • The admin grid-table (hand-rolled header+grid, as enrollments-view/payments-view) is the alternative for a small capped sample. Either way the column language below is the contract.

Grid/tokens: bg-surface rows in a rounded-lg border border-border-default shell; text-fg-primary headline, text-fg-secondary secondary, text-fg-muted for id/meta. Column headers = type-meta. Headline cell = text-sm font-medium. Grid-over-flex (Grid v2 G1).


Educación

Alumnos — person (no join)

#ColumnSourceTypeNotes
1IDididFirst, muted mono, short8
2Nombrefull_nameprimarytext-sm font-medium; email as the secondary line under it
3Contactoemail ?? phonesecondaryem-dash when both null
4Teléfonophone_verifiedbadgesuccess "Verificado" · muted "Sin verificar"
5Altacreated_atdatees-MX, mono tabular, hide < sm

Section count: total formatted es-MX (19,610).

Cursos — offering (no join)

#ColumnSourceTypeNotes
1IDididshort8, muted mono
2Cursotitleprimary
3Tipokindbadge infocategory (En vivo / Grabado / Posgrado)
4Estadostatusbadgesuccess activo · warn borrador · muted archivado

Inscripciones — enrollment ⋈ person ⋈ offering

Joined — this is the headline fix ("en nombre solo veo un id").

#ColumnSourceTypeNotes
1IDididshort8, muted mono
2Alumno · Cursoperson.full_name (join) + offering.title (join)primary + secondaryname headline, course as the secondary line
3Canalchannelchiplabel map (En vivo / Grabado / Posgrado)
4Fechacreated_atdatees-MX, hide < sm
5Estadostatebadgesuccess active · warn pending (extend completed=info / cancelled=danger)

Finanzas

Pagos — payment ⋈ person ⋈ offering (via enrollment) ⋈ advisor

The richest join; mirrors payments-view plus the new Asesor (#983).

#ColumnSourceTypeNotes
1IDididshort8, muted mono
2Alumno · Cursoperson.full_name (join) + offering.title (join via enrollment_id)primary + secondaryname headline, course secondary line
3Asesoradvisor.name (join via advisor_id)textem-dash when null
4Métodomethodbadgecard→Tarjeta · spei→SPEI · bank→Transferencia · cash→Efectivo
5Conciliaciónreconciliation_statebadgesuccess reconciled · info matched · warn pending · danger unmatched
6Montoamountmoneyrenames the ambiguous "Meta" → "Monto"; $19,610 MXN, right-aligned, mono tabular
7Fechacreated_atdatees-MX, hide < sm

Optional CFDI badge (cfdi_state) per payments-view CFDI_META. Optional summary tiles above the table derive from the page — label them as the page sample, not the full ledger, or omit until a server aggregate exists (never imply a total we don't have).

Facturas / Conciliación — honest empty

No invoice/reconciliation source is wired yet. Render the empty state only (EmptyState) — never a fake table. Copy: "Facturas — próximamente. La conciliación de CFDI por cliente aún no está conectada."


Contract impact (for Backend — drives the spec, not Designer's to build)

Today's ConsoleEntityRow { label, secondary, meta } can't carry typed columns or badges. Two paths; recommend B:

  • A (minimum): keep the shape but (1) enrollment.label/payment.label = the joined person name, (2) secondary = the joined course title, (3) meta = the raw amount/date (FE formats), (4) move id out of the headline. Cheapest; loses per-column badges.
  • B (recommended): give each entity kind a typed column set — joins resolved server-side (person.full_name, offering.title, advisor.name) and a badges: {label, tone}[] field per row so categorical fields (state, method, reconciliation, phone-verified) carry tone, not strings. The console-routes.ts queries become leftJoins instead of raw select().from(...). Formatting stays client-side (Frontend owns Intl); Backend supplies raw values + resolved names + the badge taxonomy.

Either way the column language above is fixed; the contract just delivers names + tones instead of ids + free strings. Files: console-routes.ts (the :resource handler), packages/shared/src/api/console.ts (ConsoleEntityRow), reference designs enrollments-view.tsx / payments-view.tsx.

Module nav — readability quick-wins (full refactor = #969, not designed here)

  • Active section = the screen's one Amber moment: accent-soft active row + a 2px accent under-rule (G17). Inactive = text-fg-secondary, hover text-fg-primary.
  • Nav labels use type-nav-label (mono, UPPER, 0.1em); active appends font-semibold.
  • Breadcrumb reads Org / Módulo / Sección; group sections under their module label, not a flat list.