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

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

## 从哪里开始

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

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

## 它解决的问题

自 2026 年 8 月 12 日起，欧盟包装法规（PPWR）要求**每一种投放市场的包装**都要有符合性声明和技术文档。大企业有专门部门来做这件事；只有四十个品项的小作坊，做这件事的是老板本人 —— 然后她发现，关键数据根本不在自己手里，而在纸箱供应商、标签印刷厂和薄膜厂那里。这是一个数据收集问题，不是软件问题。工具要做的，是让「还缺什么、该向谁要」一眼可见。

## 最终必须交付什么

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

## 记录类型

实体键 `packagings` —— 一条记录是 **Verpackung**，多条是 **Verpackungen**。

**字段**

| 键 | 标签 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | Verpackung | text | 必填 |
| `category` | Art | enum | 取值之一：Verkaufsverpackung · Umverpackung · Transportverpackung · Serviceverpackung |
| `articles` | Betroffene Artikel | text | 表头 `Artikel` |
| `docStatus` | Stand der Unterlagen | enum | 取值之一：nicht begonnen · beim Lieferanten angefragt · Angaben liegen vor · Erklärung erstellt, 表头 `Stand` |
| `deadline` | Eigene Frist | date | 表头 `Frist` |
| `responsible` | Zuständig | text | — |
| `doc` | Konformitätserklärung | attachment | 上传的文件，存放在记录内, 表头 `KE` |
| `note` | Notiz | text | 多行 |
| `weight` | Gewicht gesamt (g) | computed | 计算得出，从不存储, 表头 `g` |
| `recyclate` | Rezyklatanteil | computed | 计算得出，从不存储, 表头 `Rez.` |
| `worst` | Schlechteste Klasse | computed | 计算得出，从不存储, 表头 `Klasse` |

**呈现方式**

- 主列：`name`
- 主列下方的第二行：`category`
- 表格列，按此顺序：`name`, `category`, `articles`, `weight`, `recyclate`, `docStatus`, `deadline`
- 侧栏筛选：`category`, `docStatus`
- 不再计为未完成的条件：`r.docStatus === 'Erklärung erstellt'`
- 标红的条件：`r.docStatus !== 'Erklärung erstellt' && r.deadline && r.deadline < today()`

**计算字段**

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

- `weight` (Gewicht gesamt (g)) — `componentsOf(r.id).reduce((sum, c) => sum + (Number(c.weight) || 0), 0)`
- `recyclate` (Rezyklatanteil):

  ```js
  const parts = componentsOf(r.id)
  const total = parts.reduce((s, c) => s + (Number(c.weight) || 0), 0)
  if (!total) return ''
  const share = parts.reduce(
    (s, c) => s + ((Number(c.weight) || 0) * (Number(c.recycled) || 0)) / 100,
    0,
  )
  return Math.round((share / total) * 100) + ' %'
  ```
- `worst` (Schlechteste Klasse):

  ```js
  const grades = componentsOf(r.id)
    .map((c) => c.grade)
    .filter((g) => g && g !== 'noch offen')
  if (!grades.length) return 'noch offen'
  return grades.sort().at(-1)
  ```

**校验规则**

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

- **当** `r.docStatus === 'Erklärung erstellt'` → **则** `responsible`
  **提示语:** „Eine ausgestellte Erklärung braucht eine verantwortliche Person.“
- **当** `r.docStatus === 'Erklärung erstellt'` → **则** `Boolean(r.doc?.data)`
  **提示语:** „Die erstellte Erklärung gehört als Datei an den Datensatz.“

---

实体键 `components` —— 一条记录是 **Bestandteil**，多条是 **Bestandteile**。

**字段**

| 键 | 标签 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | Bestandteil | text | 必填 |
| `packagingId` | Gehört zu | reference | 必填, 引用 `packagings`, 表头 `Verpackung` |
| `material` | Material | enum | 取值之一：Papier/Karton · Kunststoff PE · Kunststoff PET · Kunststoff PP · Glas · Aluminium · Weißblech · Verbund |
| `weight` | Gewicht (g) | number | 表头 `g` |
| `recycled` | Rezyklatanteil (%) | number | 表头 `% Rez.` |
| `grade` | Recyclingklasse | enum | 取值之一：A · B · C · D · E · noch offen, 表头 `Klasse` |
| `source` | Angabe von | enum | 取值之一：Lieferant schriftlich · Datenblatt · selbst geschätzt · fehlt noch, 表头 `Quelle` |
| `supplier` | Lieferant | text | — |
| `note` | Notiz | text | 多行 |

**呈现方式**

- 主列：`name`
- 表格列，按此顺序：`name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`
- 侧栏筛选：`material`, `grade`
- 在概览中求和：`weight`
- 不再计为未完成的条件：`() => false`
- 标红的条件：`r.source === 'fehlt noch'`

**校验规则**

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

- **当** `Number(r.recycled) > 0` → **则** `source`
  **提示语:** „Zu einem Rezyklatanteil gehört, woher die Zahl stammt.“
- **当** `r.source === 'Lieferant schriftlich'` → **则** `supplier`
  **提示语:** „Wenn der Lieferant es schriftlich bestätigt hat, gehört sein Name dazu.“

---

## 仪表板

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

- 一个数字：**Verpackungen** — 记录条数 · „im Register“
- 一个数字：**Noch offen** — 记录条数（仅符合筛选条件的记录）: `r.docStatus !== 'Erklärung erstellt'` · „ohne fertige Erklärung“
- 一个数字：**Angaben fehlen** — 记录条数（仅符合筛选条件的记录）: `r.source === 'fehlt noch'` · „Bestandteile ohne Quelle“
- 按 `docStatus` 的取值分组的环形图
- 按 `material` 的取值分组的条形，度量 `weight` (Gewicht (g)) — **Gewicht je Material (g)**
- 按 `grade` 的取值分组的条形，度量 记录条数 — **Bestandteile je Recyclingklasse**

## 引导式录入

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

标题：**Verpackung aufnehmen**

> Erst die Verpackung, dann ihre Bestandteile. Geschrieben wird nichts, bevor der letzte Schritt bestätigt ist — Abbrechen hinterlässt nichts.

1. **步骤 1 `packagings`** — Verpackung: 字段：`name`, `category`, `articles`, `responsible`, `deadline`
2. **步骤 2 `components`** — Erster Bestandteil: 字段：`name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`, `supplier`
   仅当：`Boolean(drafts.packagings?.name)`
3. **步骤 3 `components`** — Liste vom Lieferanten: CSV 上传，并入同一次录入
   仅当：`Boolean(drafts.packagings?.name)`
4. **步骤 4** — Prüfen: 由 schema 自动生成的摘要

结束页：“Aufgenommen. Was noch fehlt, steht in der Liste unter „Angabe von: fehlt noch".”

## 默认设置

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

- 标题: **Verpackungsregister**
- 副标题: Konformität nach PPWR, Bestandteil für Bestandteil
- 文件名: `verpackungsregister`
- 版本: `1.0`
- 界面语言: `de`
- 打开方式: 完整工具
- 配色: `accent` #2f6b3a · `band` #17241a · `flag` #b4531c · `ok` #2f6b3a · `pending` #c08a12

## 起始页

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

```markdown
# Verpackungsregister nach PPWR

Ab dem **12. August 2026** darf in der EU nur noch in Verkehr gebracht werden, was der neuen
Verpackungsverordnung entspricht — mit Konformitätserklärung und technischer Dokumentation
**je Verpackung**.

Für einen Konzern macht das eine Fachabteilung. Für die Manufaktur mit vierzig Artikeln macht das
die Inhaberin — und stellt fest, dass die entscheidenden Angaben gar nicht bei ihr liegen, sondern
bei Kartonlieferant, Etikettendruckerei und Folienhersteller.

## Was diese Demo zeigt

- **Zwei Datenarten**, weil die Recyclingfähigkeit nicht an der Verpackung hängt, sondern an ihren
  Bestandteilen. Ein Sichtfenster aus der falschen Folie kippt die ganze Einheit.
- **Gerechnet statt gepflegt**: Gesamtgewicht, Rezyklatanteil und die schlechteste Klasse ergeben
  sich aus den Bestandteilen.
- **Regeln, die Schätzungen sichtbar machen** — eine Zahl ohne Quelle lässt sich nicht speichern.
- **Anhänge** für die Erklärung selbst, mit hartem Speicherbudget.

> Erfundene Daten, Veranschaulichung der Struktur. Keine Rechtsberatung und kein Nachweis von
> Konformität.
```

## 示例数据

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

## 完成的标准

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

## 交付之前

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

---

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

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