<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Build me this tool: Klassenfahrt

*Everything below is the functional specification. Follow it as written; where it is silent, use your judgement and say what you assumed.*

## Start here

Use the **openToolbox** template: https://github.com/m-dohmen/openToolbox. Read `AGENTS.md` in that repository first — it is the authority on the schema shape, the field types and the rules that break a single-file build. Everything domain-specific goes into one file, `src/domain.js`.

> If you have the openToolbox skill installed (`claude plugin install opentoolbox@opentoolbox`), just paste this file — it fetches the template itself.

## The problem this solves

A class trip is coming up. 28 forms go out, 19 come back, three without a signature, one with an allergy written on the back. Two days before departure the money from four families is still missing. The class teacher keeps this in a sheet she is not allowed to share, because allergies and swimming ability are in it. Almost nothing here is a number — it is a set of states, and the only sum that matters is what is still outstanding. The recurring questions every Monday morning ("whose form is still missing?", "who has not paid?") are the same ones every week — they belong on the list head as saved views, not rebuilt by hand each time. The consent state itself is a small Kanban in disguise: open → waiting → declined, swept left to right as the forms come in.

## What it has to be at the end

One self-contained HTML file, opened by double-click, no server and no installation. The file is also the database: saving writes a new HTML file with the records embedded in it.

## The record

One record is a **Kind**, several are **Kinder**.

**Fields**

| Key | Label | Type | Detail |
| --- | --- | --- | --- |
| `name` | Kind | text | required |
| `guardian` | Erziehungsberechtigte | text | table header `Eltern` |
| `phone` | Telefon für Notfälle | text | table header `Telefon` |
| `consent` | Einverständnis | enum | one of: ausstehend · liegt vor · verweigert, table header `Einv.` |
| `consentForm` | Unterschriebener Zettel | attachment | uploaded file, stored in the record, table header `Zettel` |
| `payment` | Zahlung | enum | one of: offen · teilweise · vollständig · Zuschuss beantragt · erlassen |
| `paid` | Bezahlt (€) | number | table header `€` |
| `swim` | Schwimmabzeichen | enum | one of: ja · nein · unbekannt, table header `Schwimmen` |
| `diet` | Essen (Allergien, vegetarisch …) | text | table header `Essen` |
| `medical` | Medizinisches | text | multi-line, table header `Medizin` |
| `roomWish` | Zimmerwunsch | text | table header `Zimmer` |
| `note` | Notiz | text | multi-line |
| `open` | Noch offen (€) | computed | calculated, never stored, table header `Offen` |

**Presentation**

- Headline column: `name`
- Second line under the headline: `guardian`
- Table columns, in this order: `name`, `guardian`, `consent`, `payment`, `paid`, `open`, `swim`
- Sidebar filters: `consent`, `payment`, `swim`
- Summed in the overview: `paid`
- No longer counts as open when: `r.consent === 'verweigert' || (r.consent === 'liegt vor' && (r.payment === 'vollständig' || r.payment === 'erlassen'))`
- Flagged red when: `!isDone(r)`

**Calculated fields**

These are derived on every render and never written into the record — a stored derivation goes stale the moment one of its inputs changes.

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

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

**Validation rules**

Conditions between fields. They must be enforced in one place so that the edit form, the CSV import and anything the AI proposes all pass through the same check.

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

**Saved views**

Named combinations of query, field filters and sort, offered by the dropdown at the list head. What is declared here is what the tool ships with — recipients save their own additions under `settings.views`. Merge: same name = last edit wins.

`name` (unique), `query` (same as the search box), `filters` (`{ field: spec }`, where a spec with just `v` sets the matching facet and a spec with `op` sets a field filter), and `sort` (`{ key, dir }`, `dir` is `1` or `-1`). `entity` is optional and reserved for multi-entity.

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

**Board**

An optional Kanban per entity, opened from the tab strip next to *List* and *Dashboard*. Without this declaration the view does not exist, the same posture as Dashboard and the guided entry wizard — declaring it is what enables it.

`columnField` (the key of an existing enum field — its `values` define the columns in that order, so the first value sits at the left), `cardFields` (up to three further field keys shown on each card below the title; omit to take the first three non-title, non-column, non-computed, non-attachment fields), and `limit` (per-column card cap, default `50`). Dragging a card to another column writes through the same `mutate` path as the edit form, so the move lands in the undo stack and the change log. Read-only copies render the board with dragging disabled.

- `columnField` — `consent` (Einverständnis); columns come from the enum `values`, in declared order.
- `cardFields` — *Erziehungsberechtigte*, *Telefon für Notfälle*, *Zahlung*.
- records whose value is empty or no longer in `values` land in a small *Unassigned* reservoir at the right.

## Dashboard

Tiles over the whole record set, not the filtered view.

- A single number: **Kinder** — the record count · „in der Klasse“
- A single number: **Noch offen** — the record count (only records matching a filter): `!isDone(r)` · „Zettel oder Geld fehlt“
- A single number: **Eingegangen** — `paid` (Bezahlt (€)) · „€ von 2590 €“
- A ring per value of `payment`
- Bars per value of `consent`, measuring the record count — **Einverständnisse**
- Bars per value of `swim`, measuring the record count — **Schwimmabzeichen**

## Guided entry

A short sequence of steps for someone who has to report one thing and does not know the tool. Nothing is written until the last step is confirmed — abandoning it must leave nothing behind.

Title: **Rückläufer aufnehmen**

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

1. **Step 1** — Kind: fields: `name`, `guardian`, `phone`
2. **Step 2** — Zettel und Geld: fields: `consent`, `payment`, `paid`
3. **Step 3** — Besonderheiten: fields: `swim`, `diet`, `medical`, `roomWish`
   shown only when: `drafts.records?.consent !== 'verweigert'`
4. **Step 4** — Klassenliste einlesen: CSV upload, feeding the same run
5. **Step 5** — Prüfen: summary generated from the schema

Closing screen: “Aufgenommen. Nicht vergessen: Datei speichern.”

## Defaults

Set these in `DEFAULT_SETTINGS`, `DEFAULT_COLORS` and `DEFAULT_HOME` in `src/app.jsx`.

- Title: **Klassenfahrt**
- Subtitle: Rückläufer, Zahlungen und wer noch fehlt
- File name: `klassenfahrt`
- Version: `1.0`
- Interface language: `de`
- Opens as: the full tool
- Colours: `accent` #a33a63 · `band` #2b1823 · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## Start page

The app opens on this text. It is a small Markdown subset — headings, lists, quotes, bold, italic, inline code and links. Use it verbatim:

```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.
```

## Demo data

Add 14 realistic demo records so the file is not empty on first open. Invent them in the style of the examples above; they are illustration, not the user’s data. Tell the user their own data goes in through **Import CSV → replace all**.

## Done when

- `npm run build` produces one self-contained `dist/index.html`
- `npm test` passes
- the file opens by double-click and shows the demo records
- the calculated fields show values, and the rules refuse a record that violates them
- the settings, colours and start page match the specification above

## Before handing it over

Decide these rather than leaving them to the recipient: set `copyright` to whoever owns the tool, replace the header link that points at the openToolbox repository, and switch `examplePrompts` off if the recipient only enters data. Mention the usage counter (Settings → Security) at handover rather than letting someone find it in a network log.

---

*Generated from `examples/school-trip.domain.js` — the working source of the [live demo](https://m-dohmen.github.io/openToolbox/demos/school-trip/). Regenerate with `npm run prompts`.*

*All data in the demo is invented. It illustrates the structure of such a tool — it is not legal advice and not proof of anyone’s compliance.*
