<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Constrúyeme esta herramienta: Klassenfahrt

*Todo lo que sigue es la especificación funcional. Síguela tal como está; donde calle, decide tú y di qué supusiste.*

## Punto de partida

Usa la plantilla **openToolbox**: https://github.com/m-dohmen/openToolbox. Lee primero `AGENTS.md` en ese repositorio — es la autoridad sobre la forma del esquema, los tipos de campo y las reglas que rompen una compilación de archivo único. Todo lo específico del dominio va en un solo archivo, `src/domain.js`.

> Si tienes instalado el skill de openToolbox (`claude plugin install opentoolbox@opentoolbox`), basta con pegar este archivo — él mismo se trae la plantilla.

## El problema que resuelve

Se acerca un viaje de fin de curso. Salen 28 autorizaciones, vuelven 19, tres sin firma, una con una alergia anotada al dorso. Dos días antes de salir falta el dinero de cuatro familias. La tutora lo lleva en una hoja que no puede compartir, porque contiene alergias y si el niño sabe nadar. Casi nada de esto es un número: son estados, y la única suma que importa es lo que queda pendiente. Las preguntas de cada lunes ("¿de quién falta el justificante?", “¿quién no ha pagado?”) son las mismas semana tras semana — pertenecen a la cabecera como vistas guardadas, no se reconstruyen a mano cada vez. El propio flujo del consentimiento es un Kanban pequeño: pendiente → recibido → rechazado, barrido de izquierda a derecha según llegan las hojas.

## Qué debe quedar al final

Un único archivo HTML autocontenido, que se abre con doble clic, sin servidor y sin instalación. El archivo es también la base de datos: guardar escribe un nuevo HTML con los registros incrustados.

## El registro

Un registro es **Kind**, varios son **Kinder**.

**Campos**

| Clave | Etiqueta | Tipo | Detalle |
| --- | --- | --- | --- |
| `name` | Kind | text | obligatorio |
| `guardian` | Erziehungsberechtigte | text | cabecera de tabla `Eltern` |
| `phone` | Telefon für Notfälle | text | cabecera de tabla `Telefon` |
| `consent` | Einverständnis | enum | uno de: ausstehend · liegt vor · verweigert, cabecera de tabla `Einv.` |
| `consentForm` | Unterschriebener Zettel | attachment | archivo adjunto, guardado en el registro, cabecera de tabla `Zettel` |
| `payment` | Zahlung | enum | uno de: offen · teilweise · vollständig · Zuschuss beantragt · erlassen |
| `paid` | Bezahlt (€) | number | cabecera de tabla `€` |
| `swim` | Schwimmabzeichen | enum | uno de: ja · nein · unbekannt, cabecera de tabla `Schwimmen` |
| `diet` | Essen (Allergien, vegetarisch …) | text | cabecera de tabla `Essen` |
| `medical` | Medizinisches | text | varias líneas, cabecera de tabla `Medizin` |
| `roomWish` | Zimmerwunsch | text | cabecera de tabla `Zimmer` |
| `note` | Notiz | text | varias líneas |
| `open` | Noch offen (€) | computed | calculado, nunca almacenado, cabecera de tabla `Offen` |

**Presentación**

- Columna principal: `name`
- Segunda línea debajo: `guardian`
- Columnas de la tabla, en este orden: `name`, `guardian`, `consent`, `payment`, `paid`, `open`, `swim`
- Filtros de la barra lateral: `consent`, `payment`, `swim`
- Sumado en el resumen: `paid`
- Deja de contar como abierto cuando: `r.consent === 'verweigert' || (r.consent === 'liegt vor' && (r.payment === 'vollständig' || r.payment === 'erlassen'))`
- Se marca en rojo cuando: `!isDone(r)`

**Campos calculados**

Se derivan en cada renderizado y nunca se escriben en el registro: una derivación almacenada queda obsoleta en cuanto cambia una de sus entradas.

- `open` (Noch offen (€)):

  ```js
  if (r.payment === 'erlassen') return 0
  return Math.max(0, FEE - (Number(r.paid) || 0))
  ```

**Reglas de validación**

Condiciones entre campos. Deben aplicarse en un único lugar para que el formulario, la importación CSV y lo que proponga la IA pasen por la misma comprobación.

- **Cuando** `r.consent === 'liegt vor'` → **Entonces** `guardian`, `phone`
  **Mensaje:** „Zum Einverständnis gehört, wer unterschrieben hat und wie man diese Person erreicht.“
- **Cuando** `r.payment === 'vollständig'` → **Entonces** `Number(r.paid) >= FEE`
  **Mensaje:** „Vollständig heißt 185 € — sonst stimmt die Kassenaufstellung nicht.“
- **Cuando** `r.payment === 'erlassen'` → **Entonces** `note`
  **Mensaje:** „Ein Erlass gehört begründet — die Kasse wird geprüft.“
- **Cuando** `Boolean(r.medical?.trim())` → **Entonces** `phone`
  **Mensaje:** „Wo Medizinisches steht, muss eine Telefonnummer daneben stehen.“

**Vistas guardadas**

Combinaciones con nombre de búsqueda, filtros y orden, que ofrece el desplegable de la cabecera. Lo que aquí se declara es lo que la herramienta lleva de serie; las propias vistas del destinatario viven bajo `settings.views`. Fusión: mismo nombre = gana la última edición.

`name` (único), `query` (como la caja de búsqueda), `filters` (`{ campo: spec }`, donde un `spec` con solo `v` activa el filtro rápido del mismo nombre y un `spec` con `op` es un filtro de campo) y `sort` (`{ key, dir }`, `dir` vale `1` o `-1`). `entity` es opcional y queda para múltiples entidades.

- propuesta: **Alle** — —; sort: `name ↑`
- propuesta: **Zettel ausstehend** — Einverständnis = ausstehend; sort: `name ↑`
- propuesta: **Geld offen** — Zahlung = offen; sort: `open ↓`

**Tablero**

Un Kanban opcional por entidad, abierto desde la barra de pestañas junto a *Lista* y *Panel*. Sin esta declaración la vista no existe — la misma postura que el Panel y la captura guiada: es la declaración en el esquema lo que la habilita.

`columnField` (clave de un campo enum existente — sus `values` definen las columnas en ese orden, de modo que el primer valor queda a la izquierda), `cardFields` (hasta tres claves adicionales que aparecen en cada tarjeta bajo el título; omitir toma los tres primeros campos que no sean título, columna, calculado ni adjunto) y `limit` (tope de tarjetas por columna, por defecto `50`). Arrastrar una tarjeta a otra columna pasa por el mismo `mutate` que el formulario, así que el movimiento entra en la pila de deshacer y en el registro de cambios. Las copias de solo lectura muestran el tablero sin arrastre.

- `columnField` — `consent` (Einverständnis); las columnas vienen de los `values` del enum, en el orden declarado.
- `cardFields` — *Erziehungsberechtigte*, *Telefon für Notfälle*, *Zahlung*.
- los registros con valor vacío o que ya no está en `values` caen en un pequeño depósito *Sin asignar* a la derecha.

## Panel

Fichas sobre todo el conjunto de registros, no sobre la vista filtrada.

- Un número: **Kinder** — el número de registros · „in der Klasse“
- Un número: **Noch offen** — el número de registros (solo registros que cumplen un filtro): `!isDone(r)` · „Zettel oder Geld fehlt“
- Un número: **Eingegangen** — `paid` (Bezahlt (€)) · „€ von 2590 €“
- Un anillo por valor de `payment`
- Barras por valor de `consent`, midiendo el número de registros — **Einverständnisse**
- Barras por valor de `swim`, midiendo el número de registros — **Schwimmabzeichen**

## Captura guiada

Una secuencia breve de pasos para quien tiene que reportar una cosa y no conoce la herramienta. No se escribe nada hasta confirmar el último paso: abandonarla no debe dejar rastro.

Título: **Rückläufer aufnehmen**

> Ein Kind nach dem anderen. Was noch fehlt, einfach leer lassen — die Übersicht zeigt es nachher von selbst an.

1. **Paso 1** — Kind: campos: `name`, `guardian`, `phone`
2. **Paso 2** — Zettel und Geld: campos: `consent`, `payment`, `paid`
3. **Paso 3** — Besonderheiten: campos: `swim`, `diet`, `medical`, `roomWish`
   solo cuando: `drafts.records?.consent !== 'verweigert'`
4. **Paso 4** — Klassenliste einlesen: subida de CSV, que alimenta la misma sesión
5. **Paso 5** — Prüfen: resumen generado a partir del esquema

Pantalla final: «Aufgenommen. Nicht vergessen: Datei speichern.»

## Valores por defecto

Ponlos en `DEFAULT_SETTINGS`, `DEFAULT_COLORS` y `DEFAULT_HOME` en `src/app.jsx`.

- Título: **Klassenfahrt**
- Subtítulo: Rückläufer, Zahlungen und wer noch fehlt
- Nombre de archivo: `klassenfahrt`
- Versión: `1.0`
- Idioma de la interfaz: `de`
- Se abre como: herramienta completa
- Colores: `accent` #a33a63 · `band` #2b1823 · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## Página de inicio

La aplicación abre con este texto. Es un subconjunto de Markdown: títulos, listas, citas, negrita, cursiva, código en línea y enlaces. Úsalo literalmente:

```markdown
# Klassenfahrt

28 Zettel gehen raus, 19 kommen zurück, drei ohne Unterschrift, einer mit einer Allergie auf der
Rückseite. Zwei Tage vor Abfahrt fehlt das Geld von vier Familien.

## Was diese Demo zeigt

- **Ein Bestand fast ohne Zahlen**: Zustände, Ja/Nein, ein offener Restbetrag, der sich selbst
  ausrechnet.
- **Gespeicherte Ansichten** — drei Vorschläge im Dropdown am Listenkopf („Alle", „Zettel
  ausstehend", „Geld offen"), die morgens und vor jeder Überweisung dieselbe Tastaturabfolge
  ersparen. Eigene Sichten legt man in den Einstellungen an; eine davon als Start-Ansicht markiert
  öffnet die Datei immer in genau diesem Zustand.
- **Kanban-Board** — der Reiter „Board" ordnet die Kinder nach Einverständnis (ausstehend, liegt
  vor, verweigert). Eine Karte per Drag nach „liegt vor" verschieben trägt sich ins
  Änderungsprotokoll ein und lässt sich mit Strg+Z zurücknehmen; die Tastatur übernehmen Pfeil-
  und Eingabetaste.
- **Regeln, die dem Alltag folgen** — wo Medizinisches steht, muss eine Telefonnummer daneben
  stehen.
- **Der Grund, warum diese Datei verschlüsselt gehört**: hier stehen Gesundheitsangaben von
  Kindern. Einstellungen → Sicherheit → *Verschlüsseln*. Ohne Passphrase ist die Datei danach ein
  Klumpen — auch für Sie.

> Erfundene Namen und Angaben.
```

## Datos de ejemplo

Añade 14 registros de ejemplo realistas para que el archivo no esté vacío al abrirlo. Invéntalos al estilo de los campos anteriores; son ilustración, no los datos del usuario. Dile que sus datos entran por **Import CSV → replace all**.

## Terminado cuando

- `npm run build` produce un único `dist/index.html` autocontenido
- `npm test` pasa
- el archivo se abre con doble clic y muestra los registros de ejemplo
- los campos calculados muestran valores y las reglas rechazan un registro que las incumpla
- los ajustes, colores y página de inicio coinciden con lo especificado arriba

## Antes de entregarla

Decide esto tú, no lo dejes al destinatario: pon `copyright` a quien sea dueño de la herramienta, sustituye el enlace de cabecera que apunta al repositorio de openToolbox y desactiva `examplePrompts` si el destinatario solo introduce datos. Menciona el contador de aperturas (Ajustes → Seguridad) al entregarla.

---

*Generado a partir de `examples/school-trip.domain.js`, el código real de la [demo en vivo](https://m-dohmen.github.io/openToolbox/demos/school-trip/). Regenerar con `npm run prompts`.*

*Todos los datos de la demo son inventados. Ilustra la estructura de una herramienta así — no es asesoramiento legal ni prueba de conformidad.*
