Appearance
Gestión de Campos Personalizados — Frontend
Módulo: CRM Tipo: View Estado: Planificado Fecha: 2026-03-31 Referencia: crm-field.md · field-options.md
Descripción
Los campos personalizados (CRM Fields) permiten extender el formulario de registro de CRM con campos informativos adicionales, configurados por tipo de CRM y por empresa, sin necesidad de desarrollo. El backend está completamente implementado.
Este documento define el requerimiento frontend en dos apartados:
- Configuración de módulo: sección nueva en el CRM donde el administrador gestiona qué campos existen para cada tipo.
- Formulario de CRM: cómo esos campos aparecen y se guardan al crear o editar un registro de CRM.
Tipos de dato soportados
Los únicos tipos válidos son los que acepta el backend. El tipo CrmFieldDataType debe reflejar exactamente estos siete:
| Tipo | Descripción | Componente de entrada |
|---|---|---|
text | Texto libre | TextInput |
number | Número | TextInput type=number |
date | Fecha | DateInput |
moneda | Importe monetario | TextInput con formato moneda |
porcentaje | Porcentaje | TextInput con sufijo % |
select | Selección de una opción | Select con opciones cargadas |
autocomplete | Referencia a entidad del sistema | Autocomplete contra recurso externo |
Corrección requerida antes de implementar:
CrmFieldDataTypeencrm-field.types.tsincluye tipos que el backend rechaza (textarea,decimal,datetime,checkbox) y omite tipos válidos (moneda,porcentaje). Este es el primer cambio a realizar.
Apartado 1: Configuración de módulo
Dónde vive
El CRM tiene cuatro secciones en el sidebar: Bases, Movimientos, Informes y Utilidades (declarada pero vacía). La gestión de campos personalizados se incorpora como nuevo item dentro de la sección Utilidades, accesible desde cualquier tipo de CRM que tenga campos configurados o que permita configurarlos.
El item se llama "Campos personalizados" y su ruta es /configuracion/campos.
La visibilidad del item en la matriz del sidebar (typeConfigurationMatrix) debe activarse al menos para los tipos que usen campos personalizados. Si todos los tipos pueden tener campos, el item se agrega a todos.
Ruta y contexto de tipo
Los campos siempre pertenecen a un tipo de CRM específico. El tipo activo ya está disponible en el contexto global del módulo (TipoCRMProvider). Al entrar a la vista de campos, se trabaja automáticamente con el tipo activo seleccionado.
Ruta: /configuracion/campos
No se necesita un parámetro de tipo en la URL porque el contexto ya lo provee.
Vista: Listado de campos del tipo activo
Muestra los campos personalizados configurados para el tipo de CRM activo.
Encabezado:
- Título: "Campos personalizados — {nombre del tipo}"
- Botón "Nuevo campo" (solo administradores)
Tabla:
| Columna | Descripción |
|---|---|
| Etiqueta | label — texto que ve el usuario en el formulario |
| Nombre técnico | field_name — identificador del campo |
| Tipo | data_type — badge por tipo (texto, número, fecha, etc.) |
| Requerido | Badge Sí / No |
| Visible | Badge Visible / Oculto |
| Acciones | Botón Editar |
Estado vacío: si el tipo no tiene campos, se muestra un estado vacío con mensaje explicativo y acceso directo a crear el primero.
API: GET /mod-crm/crm-types/:crmTypeId/crm_fields
Vista: Formulario de alta de campo
Crea un nuevo campo personalizado para el tipo activo.
Campos del formulario:
| Campo | Input | Reglas |
|---|---|---|
Nombre técnico (field_name) | TextInput | Requerido, máx. 20 caracteres, sin espacios ni caracteres especiales, único en todo el sistema |
Etiqueta (label) | TextInput | Requerido, máx. 50 caracteres |
Tipo de dato (data_type) | Select | Requerido, las 7 opciones disponibles |
Es requerido (is_required) | Checkbox | Por defecto desactivado |
Es visible (is_visible) | Checkbox | Por defecto activado |
Valor por defecto (default_value) | TextInput | Opcional |
Recurso (resource) | Select de recursos | Solo visible y requerido si data_type = autocomplete |
Sección de opciones (aparece solo al seleccionar data_type = select):
Al elegir el tipo select, se expande una sección para definir las opciones del campo. El formulario no puede guardarse sin al menos una opción definida.
Cada opción tiene:
value— número entero, único dentro del campo, requeridolabel— etiqueta visible para el usuario, requerida, máx. 100 caracteresorder— posición en la lista, opcional
La sección permite agregar filas y eliminar las que todavía no se guardaron.
API: POST /mod-crm/crm-types/:crmTypeId/crm_fields
Payload para tipo select:
json
{
"field_name": "tipo_consulta",
"label": "Tipo de Consulta",
"data_type": "select",
"is_required": true,
"is_visible": true,
"default_value": null,
"resource": null,
"options": [
{ "value": 1, "label": "Consulta de Precio", "order": 1 },
{ "value": 2, "label": "Consulta de Stock", "order": 2 },
{ "value": 3, "label": "Reclamo", "order": 3 }
]
}Payload para tipo autocomplete:
json
{
"field_name": "cliente_referido",
"label": "Cliente Referido",
"data_type": "autocomplete",
"is_required": false,
"is_visible": true,
"default_value": null,
"resource": "clientes",
"options": []
}Vista: Formulario de edición de campo
Modifica las propiedades editables de un campo existente. El nombre técnico y el tipo de dato no pueden cambiarse una vez creado — se muestran como texto informativo.
Campos editables:
| Campo | Editable | Observación |
|---|---|---|
field_name | No | Solo lectura, mostrar como texto |
data_type | No | Solo lectura, mostrar como badge |
label | Sí | |
is_required | Sí | |
is_visible | Sí | |
default_value | Sí | |
resource | No | Solo lectura si es autocomplete |
Sección de opciones en edición (solo si el campo es tipo select):
Muestra las opciones activas con posibilidad de:
- Editar
labelyorderde opciones existentes - Agregar nuevas opciones
- Eliminar opciones existentes (soft delete)
Las opciones eliminadas no se muestran en la sección de edición. La sincronización la maneja el backend: lo que se envíe en el array options define el estado final activo.
API:
GET /mod-crm/crm-types/:crmTypeId/crm_fields/:fieldId?include=options— cargar datos actuales con sus opcionesPUT /mod-crm/crm-types/:crmTypeId/crm_fields/:fieldId— guardar cambios
Payload de PUT:
json
{
"label": "Tipo de Consulta",
"is_required": true,
"is_visible": true,
"default_value": null,
"options": [
{ "id": 1, "value": 1, "label": "Consulta de Precio", "order": 1 },
{ "id": 2, "value": 2, "label": "Consulta de Stock", "order": 2 }
]
}Cambios de infraestructura para este apartado
| Archivo | Cambio |
|---|---|
ts/crm/config/routes.tsx | Agregar ruta /configuracion/campos → CrmCamposView |
ts/crm/config/sidebar.ts | Agregar item "Campos personalizados" en sección Utilidades y activarlo en typeConfigurationMatrix |
ts/crm/views/ | Crear CrmCamposView, CrmCamposTable, CrmCampoForm, CrmCampoOptionsEditor |
Apartado 2: Campos en el formulario de CRM
Contexto actual
El formulario de registro de CRM (CrmRecordForm) ya incluye la sección "Campos Adicionales" usando el hook useCrmFields y el componente CrmDynamicFields. Los tipos text, number, date y checkbox renderizan correctamente. Los tipos select y autocomplete muestran un alert de "pendiente". El guardado de valores no está conectado.
Carga de campos con opciones
El hook useCrmFields debe solicitar los campos con sus opciones incluidas usando el parámetro ?include=options. Esto es necesario para que los campos tipo select tengan sus opciones disponibles sin llamadas adicionales.
Renderizado del tipo select
Para un campo de tipo select:
- Renderizar como componente Select
- Las opciones del campo llegan en
field.options[](filtrar activas: sindeleted_at) - Ordenar por
order(nulls al final) - El valor que se guarda en el form es el
valuenumérico de la opción elegida (como string)
Renderizado del tipo autocomplete
Para un campo de tipo autocomplete:
- Renderizar como componente Autocomplete
field.resourceindica la entidad contra la que buscar (clientes,proveedores,vendedores,productos)- El componente necesita un mapeo de
resource→ endpoint de búsqueda - El valor guardado es el ID de la entidad seleccionada (como string)
Si algún recurso no tiene endpoint de búsqueda disponible, puede diferirse. El tipo
selectes prioritario.
Guardado de valores al crear un registro
Los valores de los campos adicionales se envían dentro del payload de creación del CRM Record, no como llamada separada. El formato que espera el backend:
json
{
"date": "2026-03-31 10:00:00",
"contact_id": 42,
"title": "Consulta técnica",
"fields": [
{ "id": 1, "value": "texto libre" },
{ "id": 3, "value": "2" }
]
}Donde:
ides elidde la fila encrm_fields(no elfield_name)valuees siempre string (o null si el campo es opcional y no se completó)
Antes del submit, el formulario debe transformar el namespace fields.* al formato array:
ts
const fieldsPayload = crmFields
.filter(field => field.is_visible)
.map(field => ({
id: field.id,
value: formValues.fields[field.field_name] != null
? String(formValues.fields[field.field_name])
: null
}))Guardado de valores al editar un registro
Al actualizar, la estrategia del backend es delete-all + reinsert: hay que enviar todos los campos en cada actualización, incluso los que no cambiaron.
El payload del PUT es idéntico al del POST: se incluyen todos los campos con sus valores actuales.
Carga de valores al editar un registro existente
Al abrir un registro existente para editar, los valores guardados se obtienen solicitando el record con ?include[]=fields. El response incluye los campos con su definición y el valor almacenado.
Al recibir la respuesta, se hidratan los valores en el namespace fields.* del formulario:
ts
// Del record existente:
// record.fields = [{ id: 1, field_id: 1, value: "texto libre" }, ...]
// La definición del campo (field_name) se obtiene correlacionando con crmFields
record.fields.forEach(fieldValue => {
const field = crmFields.find(f => f.id === fieldValue.field_id)
if (field) {
form.setValue(`fields.${field.field_name}`, fieldValue.value)
}
})Validación dinámica
El schema de Zod del formulario de CRM Record debe construirse dinámicamente en base a los campos activos y visibles:
- Campos con
is_required = trueyis_visible = true: requeridos en el schema - Campos con
is_required = falseois_visible = false: opcionales o excluidos del schema - El schema se reconstruye cuando cambia la lista de campos del tipo
Invalidación de caché
Cuando el administrador crea o edita un campo desde la configuración, debe invalidarse el query crmQueryKeys.fields(crmTypeId). Esto asegura que el formulario de registro muestre los campos actualizados sin necesidad de recargar la página.
Cambios de infraestructura para este apartado
| Archivo | Cambio |
|---|---|
crm-field.types.ts | Corregir CrmFieldDataType a los 7 tipos válidos del backend |
useCrmFields.ts | Solicitar ?include=options para traer opciones de campos select |
CrmDynamicFields.tsx | Implementar select y autocomplete, eliminar alerts de pendiente |
CrmRecordForm | Serializar fields.* al hacer submit (POST/PUT) con formato [{id, value}] |
CrmRecordForm | Hidratar fields.* al cargar un record existente |
CrmRecordForm | Construir schema Zod dinámico respetando is_required por campo |
Recursos disponibles para tipo autocomplete
Según la documentación del recurso (crm-field.md):
clientesproveedoresvendedoresproductos
El componente CrmDynamicFields necesita un mapeo explícito de nombre de recurso → endpoint o componente de autocomplete.
Orden de implementación sugerido
- Corregir
CrmFieldDataTypeencrm-field.types.ts - Agregar ruta e item de sidebar para configuración de campos
- Implementar listado de campos del tipo (solo lectura)
- Agregar formulario de alta para tipos simples (
text,number,date,moneda,porcentaje) - Agregar soporte a opciones en el formulario (tipo
select) - Agregar formulario de edición con soporte a modificación de opciones
- Actualizar
useCrmFieldspara incluir opciones (?include=options) - Implementar tipo
selectenCrmDynamicFields - Conectar guardado de valores en submit del CRM Record
- Hidratar valores al editar un record existente con campos adicionales
- Implementar tipo
autocompleteenCrmDynamicFields
Criterios de Aceptación
Apartado 1: Configuración de módulo
- [ ] AC-CFG-001: El item "Campos personalizados" aparece en la sección Utilidades del sidebar del CRM
- [ ] AC-CFG-002: La vista muestra los campos del tipo de CRM activo con etiqueta, nombre técnico, tipo, obligatoriedad y visibilidad
- [ ] AC-CFG-003: Si el tipo no tiene campos, se muestra un estado vacío con acceso a crear el primero
- [ ] AC-CFG-004: El administrador puede crear un campo de tipo
text,number,date,monedaoporcentaje - [ ] AC-CFG-005: El sistema rechaza nombres de campo duplicados con mensaje de error claro
- [ ] AC-CFG-006: Al seleccionar tipo
select, aparece la sección de opciones en el formulario. No se puede guardar sin al menos una opción - [ ] AC-CFG-007: El administrador puede crear un campo de tipo
autocompleteseleccionando el recurso externo - [ ] AC-CFG-008: El administrador puede editar la etiqueta, obligatoriedad, visibilidad y valor por defecto de un campo existente
- [ ] AC-CFG-009: El nombre técnico y el tipo de dato se muestran como solo lectura en el formulario de edición
- [ ] AC-CFG-010: El administrador puede agregar nuevas opciones a un campo
selectexistente - [ ] AC-CFG-011: El administrador puede modificar la etiqueta y el orden de opciones existentes
- [ ] AC-CFG-012: El administrador puede eliminar opciones (soft delete). Las eliminadas dejan de aparecer en el listado activo
- [ ] AC-CFG-013: Al crear o editar un campo, el caché de campos del tipo se invalida
Apartado 2: Formulario de CRM
- [ ] AC-FORM-001: Al crear un CRM Record, la sección "Campos Adicionales" muestra los campos visibles configurados para ese tipo
- [ ] AC-FORM-002: Los campos con
is_visible = falseno aparecen en el formulario de registro - [ ] AC-FORM-003: Los campos con
is_required = trueson obligatorios en la validación del formulario - [ ] AC-FORM-004: Los campos de tipo
selectmuestran un dropdown con las opciones activas ordenadas - [ ] AC-FORM-005: Los campos de tipo
autocompletemuestran un autocomplete funcional contra el recurso configurado - [ ] AC-FORM-006: Al guardar el registro, los valores se incluyen en el payload como
fields: [{id, value}] - [ ] AC-FORM-007: Al editar un CRM Record existente, los campos adicionales se pre-cargan con los valores guardados
- [ ] AC-FORM-008: Al actualizar el registro, todos los campos se envían con sus valores actuales
- [ ] AC-FORM-009: No aparece ningún alert de "pendiente" o tipo no soportado en el formulario de registro
Alineación de tipos
- [ ] AC-TYP-001:
CrmFieldDataTypecontiene exactamentetext | number | date | moneda | porcentaje | select | autocomplete - [ ] AC-TYP-002: Los tipos
textarea,decimal,datetimeycheckboxno existen enCrmFieldDataType