<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# 请帮我做这个工具: Klassenfahrt

*下面是完整的功能需求。请照此实现；需求没说到的地方由你判断，并说明你的假设。*

## 从哪里开始

使用 **openToolbox** 模板：https://github.com/m-dohmen/openToolbox。先读该仓库里的 `AGENTS.md` —— 它是 schema 形态、字段类型以及「哪些做法会破坏单文件构建」的权威说明。所有业务相关的内容只写进一个文件：`src/domain.js`。

> 如果已安装 openToolbox skill（`claude plugin install opentoolbox@opentoolbox`），直接粘贴本文件即可，它会自己去取模板。

## 它解决的问题

班级要出游了。发出去 28 张回执，收回 19 张，其中三张没签名，一张在背面写着过敏情况。出发前两天，还有四个家庭的钱没交。班主任把这些记在一张不能给别人看的表里 —— 因为里面写着过敏和会不会游泳。这里几乎没有什么是数字，全是状态；唯一要紧的求和，是还差多少钱。每个周一早上反复出现的问题（"谁的回执还没交？""谁还没付钱？"）每周都问一遍 —— 它们属于列表顶部的「保存视图」，而不是每次手动重新搭。而「同意状态」本身就是一个小型 Kanban：待定 → 已收 → 拒绝 —— 回执一到，从左扫到右。

## 最终必须交付什么

一个自包含的 HTML 文件，双击即可打开，无需服务器、无需安装。文件本身就是数据库：保存会写出一个新的 HTML 文件，数据嵌在其中。

## 记录

一条记录是 **Kind**，多条是 **Kinder**。

**字段**

| 键 | 标签 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | Kind | text | 必填 |
| `guardian` | Erziehungsberechtigte | text | 表头 `Eltern` |
| `phone` | Telefon für Notfälle | text | 表头 `Telefon` |
| `consent` | Einverständnis | enum | 取值之一：ausstehend · liegt vor · verweigert, 表头 `Einv.` |
| `consentForm` | Unterschriebener Zettel | attachment | 上传的文件，存放在记录内, 表头 `Zettel` |
| `payment` | Zahlung | enum | 取值之一：offen · teilweise · vollständig · Zuschuss beantragt · erlassen |
| `paid` | Bezahlt (€) | number | 表头 `€` |
| `swim` | Schwimmabzeichen | enum | 取值之一：ja · nein · unbekannt, 表头 `Schwimmen` |
| `diet` | Essen (Allergien, vegetarisch …) | text | 表头 `Essen` |
| `medical` | Medizinisches | text | 多行, 表头 `Medizin` |
| `roomWish` | Zimmerwunsch | text | 表头 `Zimmer` |
| `note` | Notiz | text | 多行 |
| `open` | Noch offen (€) | computed | 计算得出，从不存储, 表头 `Offen` |

**呈现方式**

- 主列：`name`
- 主列下方的第二行：`guardian`
- 表格列，按此顺序：`name`, `guardian`, `consent`, `payment`, `paid`, `open`, `swim`
- 侧栏筛选：`consent`, `payment`, `swim`
- 在概览中求和：`paid`
- 不再计为未完成的条件：`r.consent === 'verweigert' || (r.consent === 'liegt vor' && (r.payment === 'vollständig' || r.payment === 'erlassen'))`
- 标红的条件：`!isDone(r)`

**计算字段**

每次渲染时求值，绝不写回记录 —— 一旦某个输入变化，存下来的派生值当场就是错的。

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

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

**校验规则**

跨字段的条件。必须只在一处生效，使表单、CSV 导入和 AI 提出的改动都走同一套检查。

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

**保存视图**

由列表头部的下拉框按名称提供的「搜索词 + 字段筛选 + 排序」组合。这里声明的就是工具自带的内容；使用者自己的视图保存在 `settings.views`。合并规则：同名以最后一次的修改为准。

`name`（唯一）、`query`（与搜索框相同）、`filters`（`{ 字段: spec }`；只带 `v` 的 `spec` 触发同名快速筛选，带 `op` 的 `spec` 是字段筛选），以及 `sort`（`{ key, dir }`，`dir` 取 `1` 或 `-1`）。`entity` 是可选字段，为多实体预留。

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

**看板**

每个实体可选的看板视图，通过 *列表* 与 *仪表板* 旁边的标签栏打开。没有此声明时视图根本不存在——与仪表板和引导式录入同样的态度：由 schema 中的声明启用它。

`columnField`（已存在的 enum 字段键——其 `values` 按声明顺序决定列，第一个值位于最左）、`cardFields`（最多三个其它字段键，显示在每张卡片标题下方；省略时取前三个非标题、非列、非计算、非附件字段）、`limit`（每列卡片上限，默认 `50`）。把一张卡片拖到另一列走的是表单同样的 `mutate` 路径——变更进入撤销栈和变更日志。只读副本上的看板禁用拖动。

- `columnField` — `consent` (Einverständnis); 列由 enum 的 `values` 决定，按声明顺序排列.
- `cardFields` — *Erziehungsberechtigte*, *Telefon für Notfälle*, *Zahlung*.
- 字段值为空或已不在 `values` 中的记录，落到右侧一个小的「未分配」容器里.

## 仪表板

统计整个记录集，而不是筛选后的视图。

- 一个数字：**Kinder** — 记录条数 · „in der Klasse“
- 一个数字：**Noch offen** — 记录条数（仅符合筛选条件的记录）: `!isDone(r)` · „Zettel oder Geld fehlt“
- 一个数字：**Eingegangen** — `paid` (Bezahlt (€)) · „€ von 2590 €“
- 按 `payment` 的取值分组的环形图
- 按 `consent` 的取值分组的条形，度量 记录条数 — **Einverständnisse**
- 按 `swim` 的取值分组的条形，度量 记录条数 — **Schwimmabzeichen**

## 引导式录入

给「只需报告一件事、并不熟悉这个工具」的人用的一串短步骤。在最后一步确认之前不写入任何数据 —— 中途放弃必须不留痕迹。

标题：**Rückläufer aufnehmen**

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

1. **步骤 1** — Kind: 字段：`name`, `guardian`, `phone`
2. **步骤 2** — Zettel und Geld: 字段：`consent`, `payment`, `paid`
3. **步骤 3** — Besonderheiten: 字段：`swim`, `diet`, `medical`, `roomWish`
   仅当：`drafts.records?.consent !== 'verweigert'`
4. **步骤 4** — Klassenliste einlesen: CSV 上传，并入同一次录入
5. **步骤 5** — Prüfen: 由 schema 自动生成的摘要

结束页：“Aufgenommen. Nicht vergessen: Datei speichern.”

## 默认设置

在 `src/app.jsx` 的 `DEFAULT_SETTINGS`、`DEFAULT_COLORS` 和 `DEFAULT_HOME` 中设置。

- 标题: **Klassenfahrt**
- 副标题: Rückläufer, Zahlungen und wer noch fehlt
- 文件名: `klassenfahrt`
- 版本: `1.0`
- 界面语言: `de`
- 打开方式: 完整工具
- 配色: `accent` #a33a63 · `band` #2b1823 · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## 起始页

应用以这段文字作为起始页。它是一个很小的 Markdown 子集：标题、列表、引用、加粗、斜体、行内代码和链接。请原样使用：

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

## 示例数据

添加 14 条真实感的示例记录，使文件初次打开时不为空。按上面的字段风格自行编写；它们只是示例，不是用户的数据。告诉用户，他自己的数据通过 **Import CSV → replace all** 导入。

## 完成的标准

- `npm run build` 产出单个自包含的 `dist/index.html`
- `npm test` 通过
- 双击可打开，并显示示例记录
- 计算字段有值，规则会拒绝违反它们的记录
- 设置、配色和起始页与上面的规格一致

## 交付之前

这些由你决定，不要留给接收方：把 `copyright` 设为工具的归属方，替换指向 openToolbox 仓库的顶栏链接，若接收方只录入数据就关闭 `examplePrompts`。交付时主动说明使用计数器（设置 → 安全）。

---

*由 `examples/school-trip.domain.js` 生成 —— 即[在线演示](https://m-dohmen.github.io/openToolbox/demos/school-trip/)的真实源码。重新生成：`npm run prompts`。*

*演示中的数据均为虚构。它展示的是这类工具的结构，不构成法律建议，也不能作为合规证明。*
