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

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

À partir du 12 août 2026, le règlement européen sur les emballages (PPWR) exige une déclaration de conformité et une documentation technique **pour chaque emballage mis sur le marché**. Un grand groupe a un service pour cela. Une petite fabrication de quarante articles a sa dirigeante — qui découvre que les données décisives ne sont pas chez elle : elles sont chez le cartonnier, l’imprimeur d’étiquettes et le fabricant de film. C’est un problème de collecte, pas de logiciel. L’outil doit rendre visible ce qui manque et d’où cela doit venir.

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

## Types d’enregistrement

Clé d’entité `packagings` — un enregistrement est **Verpackung**, plusieurs sont **Verpackungen**.

**Champs**

| Clé | Libellé | Type | Détail |
| --- | --- | --- | --- |
| `name` | Verpackung | text | obligatoire |
| `category` | Art | enum | l’une de : Verkaufsverpackung · Umverpackung · Transportverpackung · Serviceverpackung |
| `articles` | Betroffene Artikel | text | en-tête de tableau `Artikel` |
| `docStatus` | Stand der Unterlagen | enum | l’une de : nicht begonnen · beim Lieferanten angefragt · Angaben liegen vor · Erklärung erstellt, en-tête de tableau `Stand` |
| `deadline` | Eigene Frist | date | en-tête de tableau `Frist` |
| `responsible` | Zuständig | text | — |
| `doc` | Konformitätserklärung | attachment | fichier joint, stocké dans l’enregistrement, en-tête de tableau `KE` |
| `note` | Notiz | text | multiligne |
| `weight` | Gewicht gesamt (g) | computed | calculé, jamais stocké, en-tête de tableau `g` |
| `recyclate` | Rezyklatanteil | computed | calculé, jamais stocké, en-tête de tableau `Rez.` |
| `worst` | Schlechteste Klasse | computed | calculé, jamais stocké, en-tête de tableau `Klasse` |

**Présentation**

- Colonne principale : `name`
- Deuxième ligne en dessous : `category`
- Colonnes du tableau, dans cet ordre : `name`, `category`, `articles`, `weight`, `recyclate`, `docStatus`, `deadline`
- Filtres de la barre latérale : `category`, `docStatus`
- Ne compte plus comme ouvert quand : `r.docStatus === 'Erklärung erstellt'`
- Signalé en rouge quand : `r.docStatus !== 'Erklärung erstellt' && r.deadline && r.deadline < today()`

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

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

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

---

Clé d’entité `components` — un enregistrement est **Bestandteil**, plusieurs sont **Bestandteile**.

**Champs**

| Clé | Libellé | Type | Détail |
| --- | --- | --- | --- |
| `name` | Bestandteil | text | obligatoire |
| `packagingId` | Gehört zu | reference | obligatoire, référence vers `packagings`, en-tête de tableau `Verpackung` |
| `material` | Material | enum | l’une de : Papier/Karton · Kunststoff PE · Kunststoff PET · Kunststoff PP · Glas · Aluminium · Weißblech · Verbund |
| `weight` | Gewicht (g) | number | en-tête de tableau `g` |
| `recycled` | Rezyklatanteil (%) | number | en-tête de tableau `% Rez.` |
| `grade` | Recyclingklasse | enum | l’une de : A · B · C · D · E · noch offen, en-tête de tableau `Klasse` |
| `source` | Angabe von | enum | l’une de : Lieferant schriftlich · Datenblatt · selbst geschätzt · fehlt noch, en-tête de tableau `Quelle` |
| `supplier` | Lieferant | text | — |
| `note` | Notiz | text | multiligne |

**Présentation**

- Colonne principale : `name`
- Colonnes du tableau, dans cet ordre : `name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`
- Filtres de la barre latérale : `material`, `grade`
- Totalisé dans l’aperçu : `weight`
- Ne compte plus comme ouvert quand : `() => false`
- Signalé en rouge quand : `r.source === 'fehlt noch'`

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

---

## Tableau de bord

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

- Un nombre : **Verpackungen** — le nombre d’enregistrements · „im Register“
- Un nombre : **Noch offen** — le nombre d’enregistrements (uniquement les enregistrements correspondant à un filtre): `r.docStatus !== 'Erklärung erstellt'` · „ohne fertige Erklärung“
- Un nombre : **Angaben fehlen** — le nombre d’enregistrements (uniquement les enregistrements correspondant à un filtre): `r.source === 'fehlt noch'` · „Bestandteile ohne Quelle“
- Un anneau par valeur de `docStatus`
- Barres par valeur de `material`, mesurant `weight` (Gewicht (g)) — **Gewicht je Material (g)**
- Barres par valeur de `grade`, mesurant le nombre d’enregistrements — **Bestandteile je Recyclingklasse**

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

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

1. **Étape 1 `packagings`** — Verpackung: champs : `name`, `category`, `articles`, `responsible`, `deadline`
2. **Étape 2 `components`** — Erster Bestandteil: champs : `name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`, `supplier`
   uniquement si : `Boolean(drafts.packagings?.name)`
3. **Étape 3 `components`** — Liste vom Lieferanten: téléversement CSV, alimentant la même session
   uniquement si : `Boolean(drafts.packagings?.name)`
4. **Étape 4** — Prüfen: récapitulatif généré à partir du schéma

Écran final : « Aufgenommen. Was noch fehlt, steht in der Liste unter „Angabe von: fehlt noch". »

## Valeurs par défaut

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

- Titre: **Verpackungsregister**
- Sous-titre: Konformität nach PPWR, Bestandteil für Bestandteil
- Nom de fichier: `verpackungsregister`
- Version: `1.0`
- Langue de l’interface: `de`
- S’ouvre en: outil complet
- Couleurs: `accent` #2f6b3a · `band` #17241a · `flag` #b4531c · `ok` #2f6b3a · `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
# 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.
```

## Données de démonstration

Ajoute Verpackungen: 8, Bestandteile: 15 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/ppwr-packaging.domain.js`, le code réel de la [démo en ligne](https://m-dohmen.github.io/openToolbox/demos/ppwr-packaging/). 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é.*
