<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Construis-moi cet outil: Klassenfahrt

*Tout ce qui suit est la spécification fonctionnelle. Suis-la telle quelle ; là où elle se tait, tranche toi-même et dis ce que tu as supposé.*

## Point de départ

Utilise le modèle **openToolbox** : https://github.com/m-dohmen/openToolbox. Lis d’abord `AGENTS.md` dans ce dépôt — il fait autorité sur la forme du schéma, les types de champs et les règles qui cassent une construction en fichier unique. Tout le métier tient dans un seul fichier, `src/domain.js`.

> Si le skill openToolbox est installé (`claude plugin install opentoolbox@opentoolbox`), il suffit de coller ce fichier — il récupère le modèle lui-même.

## Le problème traité

Un voyage scolaire approche. 28 autorisations partent, 19 reviennent, trois sans signature, une avec une allergie notée au dos. Deux jours avant le départ, l’argent de quatre familles manque encore. L’enseignante tient cela dans un tableau qu’elle n’a pas le droit de partager, parce qu’il contient des allergies et le niveau de natation. Presque rien ici n’est un nombre : ce sont des états, et la seule somme qui compte est le reste dû. Les questions du lundi matin (« qui n’a pas rendu l’autorisation ? », « qui n’a pas payé ? ») sont les mêmes chaque semaine — elles ont leur place en tête de liste comme vues enregistrées, et non reconstruites à la main à chaque ouverture. Le parcours du consentement est lui-même un petit Kanban : en attente → reçu → refusé, balayé de gauche à droite au fil des retours.

## Ce qui doit exister à la fin

Un seul fichier HTML autonome, ouvert par double-clic, sans serveur ni installation. Le fichier est aussi la base de données : enregistrer écrit un nouveau HTML avec les enregistrements intégrés.

## L’enregistrement

Un enregistrement est **Kind**, plusieurs sont **Kinder**.

**Champs**

| Clé | Libellé | Type | Détail |
| --- | --- | --- | --- |
| `name` | Kind | text | obligatoire |
| `guardian` | Erziehungsberechtigte | text | en-tête de tableau `Eltern` |
| `phone` | Telefon für Notfälle | text | en-tête de tableau `Telefon` |
| `consent` | Einverständnis | enum | l’une de : ausstehend · liegt vor · verweigert, en-tête de tableau `Einv.` |
| `consentForm` | Unterschriebener Zettel | attachment | fichier joint, stocké dans l’enregistrement, en-tête de tableau `Zettel` |
| `payment` | Zahlung | enum | l’une de : offen · teilweise · vollständig · Zuschuss beantragt · erlassen |
| `paid` | Bezahlt (€) | number | en-tête de tableau `€` |
| `swim` | Schwimmabzeichen | enum | l’une de : ja · nein · unbekannt, en-tête de tableau `Schwimmen` |
| `diet` | Essen (Allergien, vegetarisch …) | text | en-tête de tableau `Essen` |
| `medical` | Medizinisches | text | multiligne, en-tête de tableau `Medizin` |
| `roomWish` | Zimmerwunsch | text | en-tête de tableau `Zimmer` |
| `note` | Notiz | text | multiligne |
| `open` | Noch offen (€) | computed | calculé, jamais stocké, en-tête de tableau `Offen` |

**Présentation**

- Colonne principale : `name`
- Deuxième ligne en dessous : `guardian`
- Colonnes du tableau, dans cet ordre : `name`, `guardian`, `consent`, `payment`, `paid`, `open`, `swim`
- Filtres de la barre latérale : `consent`, `payment`, `swim`
- Totalisé dans l’aperçu : `paid`
- Ne compte plus comme ouvert quand : `r.consent === 'verweigert' || (r.consent === 'liegt vor' && (r.payment === 'vollständig' || r.payment === 'erlassen'))`
- Signalé en rouge quand : `!isDone(r)`

**Champs calculés**

Ils sont dérivés à chaque affichage et jamais écrits dans l’enregistrement : une dérivation stockée devient fausse dès qu’une de ses sources change.

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

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

**Règles de validation**

Conditions entre champs. Elles doivent s’appliquer en un seul endroit, afin que le formulaire, l’import CSV et les propositions de l’IA passent par le même contrôle.

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

**Vues enregistrées**

Combinaisons nommées de recherche, filtres et tri, proposées par le menu déroulant en tête de liste. Ce qui est déclaré ici est ce que l’outil embarque ; les propres vues du destinataire vivent sous `settings.views`. Fusion : même nom = la dernière édition gagne.

`name` (unique), `query` (comme la zone de recherche), `filters` (`{ champ : spec }`, où un `spec` avec seulement `v` actionne le filtre rapide du même nom, et un `spec` avec `op` est un filtre de champ) et `sort` (`{ key, dir }`, `dir` vaut `1` ou `-1`). `entity` est optionnel et réservé au multi-entité.

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

**Tableau**

Un Kanban facultatif par entité, ouvert depuis la barre d’onglets à côté de *Liste* et *Tableau de bord*. Sans cette déclaration, la vue n’existe pas — même posture que pour le tableau de bord et la saisie guidée : c’est la déclaration dans le schéma qui l’active.

`columnField` (clé d’un champ enum existant — ses `values` définissent les colonnes dans cet ordre, la première valeur à gauche), `cardFields` (jusqu’à trois autres clés de champ affichées sous le titre de chaque carte ; omis, prend les trois premiers champs qui ne sont ni titre, ni colonne, ni calculé, ni pièce jointe) et `limit` (plafond de cartes par colonne, par défaut `50`). Faire glisser une carte vers une autre colonne passe par le même `mutate` que le formulaire — le mouvement entre dans la pile d’annulation et dans le journal des modifications. Les copies en lecture seule affichent le tableau sans glisser-déposer.

- `columnField` — `consent` (Einverständnis); les colonnes viennent des `values` de l’enum, dans l’ordre déclaré.
- `cardFields` — *Erziehungsberechtigte*, *Telefon für Notfälle*, *Zahlung*.
- les enregistrements dont la valeur est vide ou n’est plus dans `values` aboutissent dans un petit réservoir « Non assignés » à droite.

## Tableau de bord

Tuiles sur l’ensemble des enregistrements, pas sur la vue filtrée.

- Un nombre : **Kinder** — le nombre d’enregistrements · „in der Klasse“
- Un nombre : **Noch offen** — le nombre d’enregistrements (uniquement les enregistrements correspondant à un filtre): `!isDone(r)` · „Zettel oder Geld fehlt“
- Un nombre : **Eingegangen** — `paid` (Bezahlt (€)) · „€ von 2590 €“
- Un anneau par valeur de `payment`
- Barres par valeur de `consent`, mesurant le nombre d’enregistrements — **Einverständnisse**
- Barres par valeur de `swim`, mesurant le nombre d’enregistrements — **Schwimmabzeichen**

## Saisie guidée

Une courte séquence d’étapes pour quelqu’un qui doit signaler une chose et ne connaît pas l’outil. Rien n’est écrit avant la validation de la dernière étape : abandonner ne doit rien laisser.

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

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

1. **Étape 1** — Kind: champs : `name`, `guardian`, `phone`
2. **Étape 2** — Zettel und Geld: champs : `consent`, `payment`, `paid`
3. **Étape 3** — Besonderheiten: champs : `swim`, `diet`, `medical`, `roomWish`
   uniquement si : `drafts.records?.consent !== 'verweigert'`
4. **Étape 4** — Klassenliste einlesen: téléversement CSV, alimentant la même session
5. **Étape 5** — Prüfen: récapitulatif généré à partir du schéma

Écran final : « Aufgenommen. Nicht vergessen: Datei speichern. »

## Valeurs par défaut

À définir dans `DEFAULT_SETTINGS`, `DEFAULT_COLORS` et `DEFAULT_HOME` dans `src/app.jsx`.

- Titre: **Klassenfahrt**
- Sous-titre: Rückläufer, Zahlungen und wer noch fehlt
- Nom de fichier: `klassenfahrt`
- Version: `1.0`
- Langue de l’interface: `de`
- S’ouvre en: outil complet
- Couleurs: `accent` #a33a63 · `band` #2b1823 · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## Page d’accueil

L’application s’ouvre sur ce texte. C’est un sous-ensemble de Markdown : titres, listes, citations, gras, italique, code en ligne et liens. À reprendre tel quel :

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

## Données de démonstration

Ajoute 14 enregistrements de démonstration réalistes pour que le fichier ne soit pas vide à l’ouverture. Invente-les dans l’esprit des champs ci-dessus ; ce sont des illustrations, pas les données de l’utilisateur. Dis-lui que ses propres données entrent par **Import CSV → replace all**.

## Terminé quand

- `npm run build` produit un seul `dist/index.html` autonome
- `npm test` passe
- le fichier s’ouvre par double-clic et affiche les enregistrements de démonstration
- les champs calculés affichent des valeurs et les règles refusent un enregistrement fautif
- les réglages, les couleurs et la page d’accueil correspondent à la spécification ci-dessus

## Avant de la livrer

Tranche toi-même plutôt que de laisser le destinataire décider : mets `copyright` au nom du propriétaire de l’outil, remplace le lien d’en-tête pointant vers le dépôt openToolbox, et désactive `examplePrompts` si le destinataire ne fait que saisir des données. Mentionne le compteur d’ouvertures (Réglages → Sécurité) à la livraison.

---

*Généré à partir de `examples/school-trip.domain.js`, le code réel de la [démo en ligne](https://m-dohmen.github.io/openToolbox/demos/school-trip/). Régénérer avec `npm run prompts`.*

*Toutes les données de la démo sont inventées. Elle illustre la structure d’un tel outil — ce n’est ni un conseil juridique ni une preuve de conformité.*
