# 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 ${mapeo.importe} 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 ${IM.esc(d.label)} · ${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=""` 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 `