feat(nakui): Fase 5 del ERP — tablero de KPIs

View::Dashboard: grilla de tarjetas de agregados. Metric Count/Sum/
GroupBy con filtro opcional (CardFilter), computado por compute_metric
en meta-runtime (MetricResult Scalar/Breakdown). meta-form render_dashboard
pinta cada tarjeta con el número grande formateado o un breakdown con
barras de texto. El CRM gana una vista «Panorama»: clientes,
oportunidades, pipeline, ganadas, y breakdowns por etapa y canal.

Tests de compute_metric; verificación del panorama en nakui-ui. Clippy
limpio en las libs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 19:29:27 +00:00
parent ab1cf9998a
commit ab2b8f6638
10 changed files with 386 additions and 12 deletions
@@ -121,6 +121,8 @@ pub enum View {
Form(FormView),
/// Ficha de un record: sus campos + listas de records relacionados.
Detail(DetailView),
/// Tablero de KPIs: una grilla de tarjetas de agregados.
Dashboard(DashboardView),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -173,6 +175,51 @@ pub struct RelatedList {
pub columns: Vec<Column>,
}
/// Tablero de KPIs: una grilla de tarjetas de agregados.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardView {
pub title: String,
pub cards: Vec<DashboardCard>,
}
/// Una tarjeta de KPI del tablero.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardCard {
/// Etiqueta de la tarjeta.
pub label: String,
/// Entity sobre cuyos records se computa el agregado.
pub entity: String,
/// Qué se computa.
pub metric: Metric,
/// Filtro opcional: sólo entran los records que lo cumplen.
#[serde(default)]
pub filter: Option<CardFilter>,
/// Formato del número resultante (`Currency` para sumas de dinero).
/// Ignorado por `GroupBy`.
#[serde(default)]
pub format: ValueFormat,
}
/// El agregado que computa una [`DashboardCard`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Metric {
/// Cantidad de records.
Count,
/// Suma de un campo numérico.
Sum { field: String },
/// Conteo de records por cada valor distinto de un campo.
GroupBy { field: String },
}
/// Filtro de una [`DashboardCard`]: el record entra si el valor de
/// `field` (como texto) es igual a `equals`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardFilter {
pub field: String,
pub equals: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Column {
/// Path del campo dentro del record. Para tipos planos: nombre.
@@ -473,7 +520,7 @@ impl Module {
}
}
}
View::Detail(_) => {}
View::Detail(_) | View::Dashboard(_) => {}
}
}
Ok(())