# Cómo crear una herramienta Guía práctica para añadir una herramienta a la suite. Pensada tanto para ti como para un agente IA (Claude Code / Desktop) que te ayude a construirla. Antes de empezar, revisa `catalog.json` para no duplicar algo que ya exista. ## Paso 1 — Copia la plantilla ``` cp -r tools/_plantilla tools/mi-herramienta ``` ## Paso 2 — Identifica la herramienta En `tools/mi-herramienta/index.html`, edita el `` y la llamada a `IM.init`: ```js IM.init({ id: 'mi-herramienta', titulo: 'Mi herramienta', subtitulo: 'Qué analiza, en una línea', mapeo }); ``` ## Paso 3 — Mapea las columnas del Excel El `mapeo` traduce nombres genéricos a las columnas reales de tu Excel. La clave `area` es la que alimenta el filtro vertical. Los nombres de columna de abajo son **solo un ejemplo**: el formato de cada Excel se irá definiendo herramienta a herramienta, así que sustitúyelos por los de tu fichero. ```js const mapeo = { area: 'División', // columna que segmenta por área clave: 'Razón social', // (opcional) campo para descartar filas vacías / totales importe: 'Base total', fecha: 'F. factura proveedor' }; ``` Si no indicas `clave`, la librería intenta usar `Razón social`, `Proveedor`, etc. ### Declara qué Excel necesita (datos compartidos) Los Excel se cargan una vez y se comparten entre herramientas durante la sesión del navegador. Para que tu herramienta se abra sola con ellos, di **cómo se reconoce** el fichero que necesita: ```js IM.init({ titulo: 'Mi herramienta', mapeo, tituloFuente: 'Situación laboral', acepta: /situaci|laboral/i, // regex (o función) sobre el nombre del fichero ejemplo: 'elastic_SituacionLaboral.xlsx', // se muestra en el aviso si falta }); ``` Con varias fuentes, cada una lleva su `acepta` / `ejemplo` (y `opcional: true` si no es imprescindible). Si tu herramienta prefiere clasificar los ficheros por su cuenta, pasa `autoCargar: files => cargarFicheros(files)` y recibirás los Excel de la sesión. Si falta algún Excel, la suite muestra el aviso automáticamente; si están todos, la herramienta se abre con los datos puestos. No tienes que programarlo. ### Varios Excel (cruces) Si la herramienta cruza más de un fichero, declara varias **fuentes** en `IM.init`. Cada una tiene su `id`, su título y su propio mapeo, y aparece con su badge y su carga independiente en la barra superior: ```js IM.init({ titulo: 'Mi herramienta', subtitulo: '…', fuentes: [ { id: 'facturas', titulo: 'Facturas', mapeo: { area:'División', clave:'Razón social', importe:'Base total', fecha:'F. factura proveedor' } }, { id: 'costes', titulo: 'Costes', mapeo: { clave:'Proyecto', importe:'Coste' } } ] }); ``` Trabajas cada fuente por su `id` y las cruzas conservando la trazabilidad: ```js const facturas = IM.registros('facturas'); // filtrado por área const costes = IM.registrosTodos('costes'); const cruce = IM.cruzar(facturas, costes, 'Proyecto', 'Proyecto', { prefijo: 'coste_' }); // cada fila del cruce mantiene __row (izquierda) y añade __rowDer + campos coste_* ``` `render()` se dispara igual; comprueba con `IM.fuente('costes').cargada()` si una fuente aún no está cargada y muestra un aviso si hace falta. ## Paso 4 — Escribe `render()` `render()` se ejecuta al cargar datos y cada vez que cambia el filtro de área. Trabaja siempre sobre `IM.registros()` (ya filtrados por área). ### KPI con drill-down ```js const regs = IM.registros(); const total = IM.suma(regs, mapeo.importe); IM.kpi({ label: 'Importe total', valor: total, formato: 'euro', // 'euro' | 'euro2' | 'num' | 'num2' | 'pct' | función sub: `${IM.fmtNum(regs.length)} registros`, drill: { titulo: 'Registros que suman el total', proc: IM.proc(`Suma de <code>${mapeo.importe}</code> sobre ${IM.fmtNum(regs.length)} registros.`), registros: regs, // los registros de origen columnas: ['Proveedor', mapeo.importe, mapeo.fecha] // columnas a mostrar en el detalle } }); ``` ### Agrupar y graficar (con drill al hacer clic) ```js const porProveedor = IM.agrupar(regs, 'Proveedor'); const datos = [...porProveedor.entries()] .map(([p, rs]) => ({ label: p, valor: IM.suma(rs, mapeo.importe), registros: rs })) .sort((a, b) => b.valor - a.valor).slice(0, 8); document.getElementById('graf').innerHTML = IM.grafBarras({ id: 'gProv', datos, formato: 'euro', onClic: d => IM.drill({ titulo: d.label, proc: IM.proc(`Registros de <b>${IM.esc(d.label)}</b> · ${IM.fmtEuro(d.valor)}.`), registros: d.registros, columnas: ['Factura', mapeo.importe, mapeo.fecha] }) }); ``` ### Tabla ordenable con drill por fila Genera la tabla con la clase `im-tabla`, cabeceras `th.im-sort`, celdas numéricas con `class="im-num" data-v="<número>"` para que ordene bien, y llama a `IM.tablaOrdenable(tabla)`. Asigna a cada fila un `onclick` que abra `IM.drill(...)` con sus registros. (Tienes un esqueleto funcionando en `tools/_plantilla/index.html`.) ### Apartado de datos en bruto (obligatorio) Toda herramienta incluye una vista de *raw data*. Una línea basta: ```js document.getElementById('tab-datos').innerHTML = IM.bloqueRaw(); // una sola fuente: IM.tablaRaw({ fuente:'facturas' }) ``` Muestra las filas tal cual se leyeron (todas las columnas, ordenables, exportables a CSV), una sección por fuente cargada. Respeta el filtro de área salvo que pases `{ filtrarArea:false }`. > Más allá de esto, tienes libertad para montar las vistas, KPIs y gráficos que mejor cuenten tu análisis, siempre con los componentes de la suite y con drill-down en cada cifra. ## Paso 5 — Registra la herramienta Añade una entrada a `catalog.json` (esquema en `AGENTS.md`, sección 5). Así aparece en el portal y el agente IA sabe que existe. ## Paso 6 — Documenta Copia `tools/_plantilla/herramienta.md` a `tools/mi-herramienta/mi-herramienta.md` y rellénalo: qué analiza, qué Excel espera, qué columnas usa, qué preguntas responde. --- ## Referencia rápida de `IM` | Función | Para qué | |---|---| | `IM.init(cfg)` | Monta el shell (barra, logo, filtro de área, badge, pie). | | `IM.onDatos(fn)` / `IM.onArea(fn)` | Ejecuta `fn` al cargar datos / cambiar área. | | `IM.registros(id?)` | Registros filtrados por el área seleccionada (úsalo para calcular). Con `id`, de esa fuente. | | `IM.registrosTodos(id?)` | Todos los registros cargados (de la fuente `id`). | | `IM.meta(id?)` | Origen de datos: fichero, hoja, filas, filas ignoradas, columnas, mapeo. | | `IM.fuentes()` / `IM.fuente(id)` | Fuentes declaradas / acceso a una (`.registros()`, `.todos()`, `.meta()`, `.cargada()`). | | `IM.abrirSelector(id?)` | Abre el diálogo de fichero (para la fuente `id`). | | `IM.area()` / `IM.areasPresentes()` | Áreas seleccionadas / presentes en los ficheros. | | `IM.suma(regs, campo)` | Suma numérica de una columna. | | `IM.agrupar(regs, campo)` | `Map` de valor → registros. | | `IM.porMes(regs, campoFecha)` | `Map` `AAAA-MM` → registros, ordenado. | | `IM.num(v)` / `IM.aFecha(v)` | Parseo robusto de número / fecha. | | `IM.kpi(cfg)` | HTML de una tarjeta KPI con drill. | | `IM.drill(cfg)` | Abre el drawer con los registros de origen. | | `IM.proc(html)` | Caja "proceso de cálculo" para el drill. | | `IM.grafBarras / grafDonut / grafLinea / grafHeatmap(cfg)` | Gráficos SVG de marca con `onClic` para drill. | | `IM.bloqueRaw(cfg?)` / `IM.tablaRaw(cfg?)` | Apartado de datos en bruto (obligatorio): todas las columnas, ordenable, export CSV. | | `IM.datosGuardar(files)` / `IM.datosFicheros()` / `IM.datosBorrar(n?)` | Almacén de Excel de la sesión, compartido entre herramientas. | | `IM.datosModo()` / `IM.faltantes()` | Dónde se guardan (`sesion`/`local`/`memoria`) / fuentes declaradas que siguen sin datos. | | `IM.indice(regs, campo)` | `Map` clave → registros (para cruces). | | `IM.cruzar(izq, der, cIzq, cDer, opts?)` | Cruza dos fuentes conservando trazabilidad (`opts.prefijo`, `opts.left`). | | `IM.tablaOrdenable(tabla)` | Hace ordenable una tabla `im-tabla`. | | `IM.descargarCSV(regs, cols, nombre)` | Descarga un CSV con `#fila` de origen. | | `IM.fmtEuro / fmtEuro2 / fmtNum / fmtNum2 / fmtPct` | Formato es-ES. | | `IM.esc(s)` / `IM.recorta(s, n)` | Escapar HTML / recortar texto. | ## Publicar como artefacto Si en vez de abrirla desde el repo quieres publicar una herramienta como artefacto de Claude (un único fichero con URL), **incrusta** el contenido de `imatia-suite.css` e `imatia-suite.js` dentro del `index.html` (en lugar de enlazarlos por ruta relativa) y mantén el `<script>` de SheetJS por CDN. El resto del código no cambia.