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

*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

From 12 August 2026 the EU packaging regulation (PPWR) requires a declaration of conformity and technical documentation **for every packaging placed on the market**. A corporation has a department for that. A small manufacturer with forty articles has the owner — who then finds that the decisive figures are not hers at all: they sit with the box supplier, the label printer and the film manufacturer. This is a data-collection problem, not a software problem. The tool has to make visible what is still missing and where it has to come from.

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

## Record types

Entity key `packagings` — one record is a **Verpackung**, several are **Verpackungen**.

**Fields**

| Key | Label | Type | Detail |
| --- | --- | --- | --- |
| `name` | Verpackung | text | required |
| `category` | Art | enum | one of: Verkaufsverpackung · Umverpackung · Transportverpackung · Serviceverpackung |
| `articles` | Betroffene Artikel | text | table header `Artikel` |
| `docStatus` | Stand der Unterlagen | enum | one of: nicht begonnen · beim Lieferanten angefragt · Angaben liegen vor · Erklärung erstellt, table header `Stand` |
| `deadline` | Eigene Frist | date | table header `Frist` |
| `responsible` | Zuständig | text | — |
| `doc` | Konformitätserklärung | attachment | uploaded file, stored in the record, table header `KE` |
| `note` | Notiz | text | multi-line |
| `weight` | Gewicht gesamt (g) | computed | calculated, never stored, table header `g` |
| `recyclate` | Rezyklatanteil | computed | calculated, never stored, table header `Rez.` |
| `worst` | Schlechteste Klasse | computed | calculated, never stored, table header `Klasse` |

**Presentation**

- Headline column: `name`
- Second line under the headline: `category`
- Table columns, in this order: `name`, `category`, `articles`, `weight`, `recyclate`, `docStatus`, `deadline`
- Sidebar filters: `category`, `docStatus`
- No longer counts as open when: `r.docStatus === 'Erklärung erstellt'`
- Flagged red when: `r.docStatus !== 'Erklärung erstellt' && r.deadline && r.deadline < today()`

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

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

**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.docStatus === 'Erklärung erstellt'` → **Then** `responsible`
  **Message:** „Eine ausgestellte Erklärung braucht eine verantwortliche Person.“
- **When** `r.docStatus === 'Erklärung erstellt'` → **Then** `Boolean(r.doc?.data)`
  **Message:** „Die erstellte Erklärung gehört als Datei an den Datensatz.“

---

Entity key `components` — one record is a **Bestandteil**, several are **Bestandteile**.

**Fields**

| Key | Label | Type | Detail |
| --- | --- | --- | --- |
| `name` | Bestandteil | text | required |
| `packagingId` | Gehört zu | reference | required, reference to `packagings`, table header `Verpackung` |
| `material` | Material | enum | one of: Papier/Karton · Kunststoff PE · Kunststoff PET · Kunststoff PP · Glas · Aluminium · Weißblech · Verbund |
| `weight` | Gewicht (g) | number | table header `g` |
| `recycled` | Rezyklatanteil (%) | number | table header `% Rez.` |
| `grade` | Recyclingklasse | enum | one of: A · B · C · D · E · noch offen, table header `Klasse` |
| `source` | Angabe von | enum | one of: Lieferant schriftlich · Datenblatt · selbst geschätzt · fehlt noch, table header `Quelle` |
| `supplier` | Lieferant | text | — |
| `note` | Notiz | text | multi-line |

**Presentation**

- Headline column: `name`
- Table columns, in this order: `name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`
- Sidebar filters: `material`, `grade`
- Summed in the overview: `weight`
- No longer counts as open when: `() => false`
- Flagged red when: `r.source === 'fehlt noch'`

**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** `Number(r.recycled) > 0` → **Then** `source`
  **Message:** „Zu einem Rezyklatanteil gehört, woher die Zahl stammt.“
- **When** `r.source === 'Lieferant schriftlich'` → **Then** `supplier`
  **Message:** „Wenn der Lieferant es schriftlich bestätigt hat, gehört sein Name dazu.“

---

## Dashboard

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

- A single number: **Verpackungen** — the record count · „im Register“
- A single number: **Noch offen** — the record count (only records matching a filter): `r.docStatus !== 'Erklärung erstellt'` · „ohne fertige Erklärung“
- A single number: **Angaben fehlen** — the record count (only records matching a filter): `r.source === 'fehlt noch'` · „Bestandteile ohne Quelle“
- A ring per value of `docStatus`
- Bars per value of `material`, measuring `weight` (Gewicht (g)) — **Gewicht je Material (g)**
- Bars per value of `grade`, measuring the record count — **Bestandteile je Recyclingklasse**

## 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: **Verpackung aufnehmen**

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

1. **Step 1 `packagings`** — Verpackung: fields: `name`, `category`, `articles`, `responsible`, `deadline`
2. **Step 2 `components`** — Erster Bestandteil: fields: `name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`, `supplier`
   shown only when: `Boolean(drafts.packagings?.name)`
3. **Step 3 `components`** — Liste vom Lieferanten: CSV upload, feeding the same run
   shown only when: `Boolean(drafts.packagings?.name)`
4. **Step 4** — Prüfen: summary generated from the schema

Closing screen: “Aufgenommen. Was noch fehlt, steht in der Liste unter „Angabe von: fehlt noch".”

## Defaults

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

- Title: **Verpackungsregister**
- Subtitle: Konformität nach PPWR, Bestandteil für Bestandteil
- File name: `verpackungsregister`
- Version: `1.0`
- Interface language: `de`
- Opens as: the full tool
- Colours: `accent` #2f6b3a · `band` #17241a · `flag` #b4531c · `ok` #2f6b3a · `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
# 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.
```

## Demo data

Add Verpackungen: 8, Bestandteile: 15 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/ppwr-packaging.domain.js` — the working source of the [live demo](https://m-dohmen.github.io/openToolbox/demos/ppwr-packaging/). 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.*
