Files
Sergio fc72726666 feat(nakui-ui): FieldKind::EntityRef — selector clickable de records existentes
Cierra el principal trade-off documentado del commit anterior:
"Inputs UUID a mano (no dropdown)". Los formularios pueden declarar
un campo entity_ref que apunta a una entity y el runtime renderea
una lista clickable de records existentes; click selecciona, el UUID
queda guardado para el submit.

Schema:
- Nueva variante FieldKind::EntityRef (serializa como "entity_ref").
- FieldSpec.ref_entity: Option<String> nuevo. validate() chequea que
  cualquier field con kind=entity_ref tenga ref_entity set.
- Nuevo SchemaError::EntityRefMissingTarget.

Runtime:
- render_entity_ref_selector helper: lista clickable debajo del input,
  cada item con etiqueta humana (heuristica: name > label > title >
  sku > sku_id > UUID corto) y click handler via cx.listener que
  setea el TextInput con el UUID completo. Highlight en accent color
  para el seleccionado.
- parse_field_value(EntityRef) devuelve string raw — validacion como
  Uuid es responsabilidad de commit_morphism downstream.
- Mensaje "(sin {entity}: crea uno antes...)" cuando lista vacia —
  el user sabe que hacer.

Demo actualizado sales_engine: vender_form.stock_id_input y
caja_id_input cambian a kind=entity_ref. Flujo nuevo: click en Stock
listado bajo input, click en Caja, escribir venta_id/cantidad/precio/
timestamp, submit. Sin copiar UUIDs.

Tests: 2 nuevos schema (validate detecta EntityRef sin ref_entity y
acepta con ref_entity) + 4 nuevos runtime (parse, human_label cubre
todos los key fallbacks). 29 tests totales (16 + 8 + 5).

Pendientes: confirmacion de delete, snapshot/compaction del log,
edit delta-only, validacion estricta de params del morphism via
FieldKind del FieldSpec en lugar de infer_param_value.
2026-05-09 20:48:59 +00:00

150 lines
6.0 KiB
JSON

{
"id": "sales_engine",
"label": "Ventas (con morphism)",
"description": "Módulo conectado al manifest nakui-core 'sales': el form 'Vender' dispara el morphism que valida + mueve stock y caja con KCL post-checks reales.",
"nakui_module_dir": "../../../crates/modules/nakui/modules/sales",
"entities": [
{
"name": "Stock",
"label": "Stock",
"fields": [
{ "name": "id", "label": "ID", "kind": "text" },
{ "name": "sku_id", "label": "SKU", "kind": "text", "required": true },
{ "name": "ubicacion", "label": "Ubicación", "kind": "text" },
{ "name": "cantidad", "label": "Cantidad", "kind": "number", "required": true }
]
},
{
"name": "Caja",
"label": "Caja",
"fields": [
{ "name": "id", "label": "ID", "kind": "text" },
{ "name": "name", "label": "Nombre", "kind": "text", "required": true },
{ "name": "currency", "label": "Moneda", "kind": "text", "default": "USD" },
{ "name": "saldo", "label": "Saldo", "kind": "number", "default": "0" }
]
},
{
"name": "Venta",
"label": "Venta",
"fields": [
{ "name": "id", "label": "ID", "kind": "text" },
{ "name": "stock_id", "label": "Stock ref", "kind": "text" },
{ "name": "caja_id", "label": "Caja ref", "kind": "text" },
{ "name": "cantidad", "label": "Cantidad vendida", "kind": "number" },
{ "name": "precio_unitario", "label": "Precio unit.", "kind": "number" },
{ "name": "total", "label": "Total", "kind": "number" }
]
}
],
"menu": [
{ "label": "Stock", "view": "stock_list", "icon": "📦" },
{ "label": "+ Stock", "view": "stock_form", "icon": "✚" },
{ "label": "Cajas", "view": "caja_list", "icon": "💰" },
{ "label": "+ Caja", "view": "caja_form", "icon": "✚" },
{ "label": "Ventas registradas", "view": "venta_list", "icon": "🧾" },
{ "label": "Vender", "view": "vender_form", "icon": "⚡" }
],
"views": {
"stock_list": {
"kind": "list",
"title": "Stock",
"entity": "Stock",
"columns": [
{ "field": "sku_id", "label": "SKU", "weight": 1.5 },
{ "field": "ubicacion", "label": "Ubicación", "weight": 1.5 },
{ "field": "cantidad", "label": "Cantidad", "weight": 1.0 }
],
"actions": [
{ "kind": "open_view", "view": "stock_form", "label": "✚ Stock" }
],
"search_in": ["sku_id", "ubicacion"]
},
"stock_form": {
"kind": "form",
"title": "Nuevo stock",
"entity": "Stock",
"fields": [
{ "name": "id", "label": "ID interno", "kind": "text", "help": "El UUID lo genera el runtime; este field es para el campo `id` de la entity (Nakui lo usa)." },
{ "name": "sku_id", "label": "SKU", "kind": "text", "required": true },
{ "name": "ubicacion", "label": "Ubicación", "kind": "text", "required": true, "default": "loc-default" },
{ "name": "cantidad", "label": "Cantidad inicial", "kind": "number", "required": true, "default": "0" }
],
"on_submit": {
"kind": "seed_entity",
"entity": "Stock",
"next_view": "stock_list"
}
},
"caja_list": {
"kind": "list",
"title": "Cajas",
"entity": "Caja",
"columns": [
{ "field": "name", "label": "Nombre", "weight": 2.0 },
{ "field": "currency", "label": "Moneda", "weight": 0.5 },
{ "field": "saldo", "label": "Saldo", "weight": 1.0 }
],
"actions": [
{ "kind": "open_view", "view": "caja_form", "label": "✚ Caja" }
],
"search_in": ["name", "currency"]
},
"caja_form": {
"kind": "form",
"title": "Nueva caja",
"entity": "Caja",
"fields": [
{ "name": "id", "label": "ID interno", "kind": "text" },
{ "name": "name", "label": "Nombre", "kind": "text", "required": true },
{ "name": "currency", "label": "Moneda", "kind": "text", "default": "USD" },
{ "name": "saldo", "label": "Saldo inicial", "kind": "number", "default": "0" }
],
"on_submit": {
"kind": "seed_entity",
"entity": "Caja",
"next_view": "caja_list"
}
},
"venta_list": {
"kind": "list",
"title": "Ventas",
"entity": "Venta",
"columns": [
{ "field": "stock_id", "label": "Stock ref", "weight": 1.0 },
{ "field": "caja_id", "label": "Caja ref", "weight": 1.0 },
{ "field": "cantidad", "label": "Cant.", "weight": 0.6 },
{ "field": "precio_unitario", "label": "Precio", "weight": 0.8 },
{ "field": "total", "label": "Total", "weight": 1.0 }
],
"actions": [
{ "kind": "open_view", "view": "vender_form", "label": "⚡ Vender" }
],
"search_in": ["stock_id", "caja_id"]
},
"vender_form": {
"kind": "form",
"title": "Vender (morphism)",
"entity": "Venta",
"fields": [
{ "name": "stock_id_input", "label": "Stock", "kind": "entity_ref", "ref_entity": "Stock", "required": true, "help": "Click en un Stock de la lista para seleccionar." },
{ "name": "caja_id_input", "label": "Caja", "kind": "entity_ref", "ref_entity": "Caja", "required": true, "help": "Click en una Caja de la lista para seleccionar." },
{ "name": "venta_id", "label": "Venta UUID (idempotencia)", "kind": "text", "required": true, "help": "UUID nuevo por cada intento; mismo UUID = idempotente." },
{ "name": "cantidad", "label": "Cantidad a vender", "kind": "number", "required": true, "default": "1" },
{ "name": "precio_unitario", "label": "Precio unitario", "kind": "number", "required": true, "default": "0" },
{ "name": "timestamp", "label": "Timestamp ISO", "kind": "text", "required": true, "default": "2026-05-09T12:00:00Z" }
],
"on_submit": {
"kind": "morphism",
"name": "vender",
"inputs": {
"stock": "stock_id_input",
"caja": "caja_id_input"
},
"params": ["venta_id", "cantidad", "precio_unitario", "timestamp"],
"next_view": "venta_list"
}
}
}
}