<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Construa esta ferramenta para mim: Klassenfahrt

*Tudo o que segue é a especificação funcional. Siga-a como está; onde ela se calar, decida e diga o que assumiu.*

## Ponto de partida

Use o modelo **openToolbox**: https://github.com/m-dohmen/openToolbox. Leia primeiro o `AGENTS.md` desse repositório — ele é a autoridade sobre a forma do esquema, os tipos de campo e as regras que quebram uma compilação de arquivo único. Tudo o que é do domínio vai em um único arquivo, `src/domain.js`.

> Se o skill do openToolbox estiver instalado (`claude plugin install opentoolbox@opentoolbox`), basta colar este arquivo — ele mesmo busca o modelo.

## O problema por trás disso

Uma excursão escolar se aproxima. Saem 28 autorizações, voltam 19, três sem assinatura, uma com uma alergia anotada no verso. Dois dias antes da partida falta o dinheiro de quatro famílias. A professora mantém isso numa planilha que não pode compartilhar, porque contém alergias e se a criança sabe nadar. Quase nada aqui é número: são estados, e a única soma que importa é o que ainda falta pagar. As perguntas de cada segunda-feira ("de quem falta a autorização?", "quem ainda não pagou?") são as mesmas todas as semanas — pertencem ao topo da lista como vistas guardadas, não reconstruídas à mão a cada vez. O próprio fluxo do consentimento é um pequeno Kanban: pendente → recebido → recusado, varrido da esquerda para a direita conforme os papéis chegam.

## O que precisa existir no fim

Um único arquivo HTML autocontido, aberto com duplo clique, sem servidor e sem instalação. O arquivo também é o banco de dados: salvar escreve um novo HTML com os registros embutidos.

## O registro

Um registro é **Kind**, vários são **Kinder**.

**Campos**

| Chave | Rótulo | Tipo | Detalhe |
| --- | --- | --- | --- |
| `name` | Kind | text | obrigatório |
| `guardian` | Erziehungsberechtigte | text | cabeçalho de tabela `Eltern` |
| `phone` | Telefon für Notfälle | text | cabeçalho de tabela `Telefon` |
| `consent` | Einverständnis | enum | um de: ausstehend · liegt vor · verweigert, cabeçalho de tabela `Einv.` |
| `consentForm` | Unterschriebener Zettel | attachment | arquivo anexado, guardado no registro, cabeçalho de tabela `Zettel` |
| `payment` | Zahlung | enum | um de: offen · teilweise · vollständig · Zuschuss beantragt · erlassen |
| `paid` | Bezahlt (€) | number | cabeçalho de tabela `€` |
| `swim` | Schwimmabzeichen | enum | um de: ja · nein · unbekannt, cabeçalho de tabela `Schwimmen` |
| `diet` | Essen (Allergien, vegetarisch …) | text | cabeçalho de tabela `Essen` |
| `medical` | Medizinisches | text | multilinha, cabeçalho de tabela `Medizin` |
| `roomWish` | Zimmerwunsch | text | cabeçalho de tabela `Zimmer` |
| `note` | Notiz | text | multilinha |
| `open` | Noch offen (€) | computed | calculado, nunca armazenado, cabeçalho de tabela `Offen` |

**Apresentação**

- Coluna principal: `name`
- Segunda linha abaixo: `guardian`
- Colunas da tabela, nesta ordem: `name`, `guardian`, `consent`, `payment`, `paid`, `open`, `swim`
- Filtros da barra lateral: `consent`, `payment`, `swim`
- Somado no resumo: `paid`
- Deixa de contar como aberto quando: `r.consent === 'verweigert' || (r.consent === 'liegt vor' && (r.payment === 'vollständig' || r.payment === 'erlassen'))`
- Marcado em vermelho quando: `!isDone(r)`

**Campos calculados**

São derivados a cada renderização e nunca gravados no registro — uma derivação armazenada fica desatualizada assim que uma de suas entradas muda.

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

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

**Regras de validação**

Condições entre campos. Precisam valer em um único lugar, para que o formulário, a importação CSV e o que a IA propuser passem pela mesma verificação.

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

**Vistas guardadas**

Combinações nomeadas de busca, filtros e ordenação, oferecidas pelo menu na cabeça da lista. O que é declarado aqui é o que a ferramenta traz de fábrica; as vistas do destinatário ficam em `settings.views`. Fusão: mesmo nome = ganha a última edição.

`name` (único), `query` (como o campo de busca), `filters` (`{ campo: spec }`, onde um `spec` com apenas `v` aciona o filtro rápido do mesmo nome e um `spec` com `op` é um filtro de campo) e `sort` (`{ key, dir }`, `dir` vale `1` ou `-1`). `entity` é opcional e fica reservado para múltiplas entidades.

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

**Quadro**

Um Kanban opcional por entidade, aberto a partir da faixa de abas ao lado de *Lista* e *Painel*. Sem essa declaração a vista não existe — mesma postura que o Painel e a captura guiada: é a declaração no esquema que a habilita.

`columnField` (chave de um campo enum existente — seus `values` definem as colunas nessa ordem, de modo que o primeiro valor fica à esquerda), `cardFields` (até três chaves de campo adicionais mostradas em cada cartão sob o título; omitir pega os três primeiros campos que não sejam título, coluna, calculado nem anexo) e `limit` (teto de cartões por coluna, padrão `50`). Arrastar um cartão para outra coluna passa pelo mesmo `mutate` do formulário — o movimento entra na pilha de desfazer e no registro de alterações. Cópias somente leitura mostram o quadro sem arrastar.

- `columnField` — `consent` (Einverständnis); as colunas vêm dos `values` do enum, na ordem declarada.
- `cardFields` — *Erziehungsberechtigte*, *Telefon für Notfälle*, *Zahlung*.
- registros com valor vazio ou que já não está em `values` caem num pequeno reservatório *Sem atribuição* à direita.

## Painel

Blocos sobre todo o conjunto de registros, não sobre a visão filtrada.

- Um número: **Kinder** — a quantidade de registros · „in der Klasse“
- Um número: **Noch offen** — a quantidade de registros (apenas registros que atendem a um filtro): `!isDone(r)` · „Zettel oder Geld fehlt“
- Um número: **Eingegangen** — `paid` (Bezahlt (€)) · „€ von 2590 €“
- Um anel por valor de `payment`
- Barras por valor de `consent`, medindo a quantidade de registros — **Einverständnisse**
- Barras por valor de `swim`, medindo a quantidade de registros — **Schwimmabzeichen**

## Captura guiada

Uma sequência curta de passos para quem precisa relatar uma coisa e não conhece a ferramenta. Nada é gravado antes da confirmação do último passo — abandonar não pode deixar 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. **Passo 1** — Kind: campos: `name`, `guardian`, `phone`
2. **Passo 2** — Zettel und Geld: campos: `consent`, `payment`, `paid`
3. **Passo 3** — Besonderheiten: campos: `swim`, `diet`, `medical`, `roomWish`
   somente quando: `drafts.records?.consent !== 'verweigert'`
4. **Passo 4** — Klassenliste einlesen: upload de CSV, alimentando a mesma sessão
5. **Passo 5** — Prüfen: resumo gerado a partir do esquema

Tela final: “Aufgenommen. Nicht vergessen: Datei speichern.”

## Padrões

Defina em `DEFAULT_SETTINGS`, `DEFAULT_COLORS` e `DEFAULT_HOME` em `src/app.jsx`.

- Título: **Klassenfahrt**
- Subtítulo: Rückläufer, Zahlungen und wer noch fehlt
- Nome do arquivo: `klassenfahrt`
- Versão: `1.0`
- Idioma da interface: `de`
- Abre como: ferramenta completa
- Cores: `accent` #a33a63 · `band` #2b1823 · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## Página inicial

O aplicativo abre com este texto. É um subconjunto pequeno de Markdown: títulos, listas, citações, negrito, itálico, código embutido e links. Use-o 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.
```

## Dados de demonstração

Acrescente 14 registros de demonstração realistas para que o arquivo não esteja vazio ao abrir. Invente-os no estilo dos campos acima; são ilustração, não os dados do usuário. Diga a ele que os próprios dados entram por **Import CSV → replace all**.

## Pronto quando

- `npm run build` produz um único `dist/index.html` autocontido
- `npm test` passa
- o arquivo abre com duplo clique e mostra os registros de demonstração
- os campos calculados mostram valores e as regras recusam um registro que as viole
- as configurações, cores e página inicial correspondem à especificação acima

## Antes de entregar

Decida você, em vez de deixar para o destinatário: defina `copyright` para quem é dono da ferramenta, substitua o link do cabeçalho que aponta para o repositório do openToolbox e desligue `examplePrompts` se o destinatário apenas registra dados. Mencione o contador de aberturas (Configurações → Segurança) na entrega.

---

*Gerado a partir de `examples/school-trip.domain.js`, o código real da [demo ao vivo](https://m-dohmen.github.io/openToolbox/demos/school-trip/). Gerar de novo com `npm run prompts`.*

*Todos os dados da demo são inventados. Ela ilustra a estrutura de uma ferramenta assim — não é aconselhamento jurídico nem prova de conformidade.*
