<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Constrúyeme esta herramienta: Verpackungsregister

*Todo lo que sigue es la especificación funcional. Síguela tal como está; donde calle, decide tú y di qué supusiste.*

## Punto de partida

Usa la plantilla **openToolbox**: https://github.com/m-dohmen/openToolbox. Lee primero `AGENTS.md` en ese repositorio — es la autoridad sobre la forma del esquema, los tipos de campo y las reglas que rompen una compilación de archivo único. Todo lo específico del dominio va en un solo archivo, `src/domain.js`.

> Si tienes instalado el skill de openToolbox (`claude plugin install opentoolbox@opentoolbox`), basta con pegar este archivo — él mismo se trae la plantilla.

## El problema que resuelve

Desde el 12 de agosto de 2026 el reglamento europeo de envases (PPWR) exige una declaración de conformidad y documentación técnica **para cada envase puesto en el mercado**. Una gran empresa tiene un departamento para eso. Un taller con cuarenta artículos tiene a su dueña — que descubre que los datos decisivos no son suyos: están en el proveedor de cartón, la imprenta de etiquetas y el fabricante de film. Es un problema de recopilación, no de software. La herramienta debe mostrar qué falta y de dónde tiene que venir.

## Qué debe quedar al final

Un único archivo HTML autocontenido, que se abre con doble clic, sin servidor y sin instalación. El archivo es también la base de datos: guardar escribe un nuevo HTML con los registros incrustados.

## Tipos de registro

Clave de entidad `packagings` — un registro es **Verpackung**, varios son **Verpackungen**.

**Campos**

| Clave | Etiqueta | Tipo | Detalle |
| --- | --- | --- | --- |
| `name` | Verpackung | text | obligatorio |
| `category` | Art | enum | uno de: Verkaufsverpackung · Umverpackung · Transportverpackung · Serviceverpackung |
| `articles` | Betroffene Artikel | text | cabecera de tabla `Artikel` |
| `docStatus` | Stand der Unterlagen | enum | uno de: nicht begonnen · beim Lieferanten angefragt · Angaben liegen vor · Erklärung erstellt, cabecera de tabla `Stand` |
| `deadline` | Eigene Frist | date | cabecera de tabla `Frist` |
| `responsible` | Zuständig | text | — |
| `doc` | Konformitätserklärung | attachment | archivo adjunto, guardado en el registro, cabecera de tabla `KE` |
| `note` | Notiz | text | varias líneas |
| `weight` | Gewicht gesamt (g) | computed | calculado, nunca almacenado, cabecera de tabla `g` |
| `recyclate` | Rezyklatanteil | computed | calculado, nunca almacenado, cabecera de tabla `Rez.` |
| `worst` | Schlechteste Klasse | computed | calculado, nunca almacenado, cabecera de tabla `Klasse` |

**Presentación**

- Columna principal: `name`
- Segunda línea debajo: `category`
- Columnas de la tabla, en este orden: `name`, `category`, `articles`, `weight`, `recyclate`, `docStatus`, `deadline`
- Filtros de la barra lateral: `category`, `docStatus`
- Deja de contar como abierto cuando: `r.docStatus === 'Erklärung erstellt'`
- Se marca en rojo cuando: `r.docStatus !== 'Erklärung erstellt' && r.deadline && r.deadline < today()`

**Campos calculados**

Se derivan en cada renderizado y nunca se escriben en el registro: una derivación almacenada queda obsoleta en cuanto cambia una de sus entradas.

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

**Reglas de validación**

Condiciones entre campos. Deben aplicarse en un único lugar para que el formulario, la importación CSV y lo que proponga la IA pasen por la misma comprobación.

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

---

Clave de entidad `components` — un registro es **Bestandteil**, varios son **Bestandteile**.

**Campos**

| Clave | Etiqueta | Tipo | Detalle |
| --- | --- | --- | --- |
| `name` | Bestandteil | text | obligatorio |
| `packagingId` | Gehört zu | reference | obligatorio, referencia a `packagings`, cabecera de tabla `Verpackung` |
| `material` | Material | enum | uno de: Papier/Karton · Kunststoff PE · Kunststoff PET · Kunststoff PP · Glas · Aluminium · Weißblech · Verbund |
| `weight` | Gewicht (g) | number | cabecera de tabla `g` |
| `recycled` | Rezyklatanteil (%) | number | cabecera de tabla `% Rez.` |
| `grade` | Recyclingklasse | enum | uno de: A · B · C · D · E · noch offen, cabecera de tabla `Klasse` |
| `source` | Angabe von | enum | uno de: Lieferant schriftlich · Datenblatt · selbst geschätzt · fehlt noch, cabecera de tabla `Quelle` |
| `supplier` | Lieferant | text | — |
| `note` | Notiz | text | varias líneas |

**Presentación**

- Columna principal: `name`
- Columnas de la tabla, en este orden: `name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`
- Filtros de la barra lateral: `material`, `grade`
- Sumado en el resumen: `weight`
- Deja de contar como abierto cuando: `() => false`
- Se marca en rojo cuando: `r.source === 'fehlt noch'`

**Reglas de validación**

Condiciones entre campos. Deben aplicarse en un único lugar para que el formulario, la importación CSV y lo que proponga la IA pasen por la misma comprobación.

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

---

## Panel

Fichas sobre todo el conjunto de registros, no sobre la vista filtrada.

- Un número: **Verpackungen** — el número de registros · „im Register“
- Un número: **Noch offen** — el número de registros (solo registros que cumplen un filtro): `r.docStatus !== 'Erklärung erstellt'` · „ohne fertige Erklärung“
- Un número: **Angaben fehlen** — el número de registros (solo registros que cumplen un filtro): `r.source === 'fehlt noch'` · „Bestandteile ohne Quelle“
- Un anillo por valor de `docStatus`
- Barras por valor de `material`, midiendo `weight` (Gewicht (g)) — **Gewicht je Material (g)**
- Barras por valor de `grade`, midiendo el número de registros — **Bestandteile je Recyclingklasse**

## Captura guiada

Una secuencia breve de pasos para quien tiene que reportar una cosa y no conoce la herramienta. No se escribe nada hasta confirmar el último paso: abandonarla no debe dejar rastro.

Título: **Verpackung aufnehmen**

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

1. **Paso 1 `packagings`** — Verpackung: campos: `name`, `category`, `articles`, `responsible`, `deadline`
2. **Paso 2 `components`** — Erster Bestandteil: campos: `name`, `packagingId`, `material`, `weight`, `recycled`, `grade`, `source`, `supplier`
   solo cuando: `Boolean(drafts.packagings?.name)`
3. **Paso 3 `components`** — Liste vom Lieferanten: subida de CSV, que alimenta la misma sesión
   solo cuando: `Boolean(drafts.packagings?.name)`
4. **Paso 4** — Prüfen: resumen generado a partir del esquema

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

## Valores por defecto

Ponlos en `DEFAULT_SETTINGS`, `DEFAULT_COLORS` y `DEFAULT_HOME` en `src/app.jsx`.

- Título: **Verpackungsregister**
- Subtítulo: Konformität nach PPWR, Bestandteil für Bestandteil
- Nombre de archivo: `verpackungsregister`
- Versión: `1.0`
- Idioma de la interfaz: `de`
- Se abre como: herramienta completa
- Colores: `accent` #2f6b3a · `band` #17241a · `flag` #b4531c · `ok` #2f6b3a · `pending` #c08a12

## Página de inicio

La aplicación abre con este texto. Es un subconjunto de Markdown: títulos, listas, citas, negrita, cursiva, código en línea y enlaces. Úsalo literalmente:

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

## Datos de ejemplo

Añade Verpackungen: 8, Bestandteile: 15 registros de ejemplo realistas para que el archivo no esté vacío al abrirlo. Invéntalos al estilo de los campos anteriores; son ilustración, no los datos del usuario. Dile que sus datos entran por **Import CSV → replace all**.

## Terminado cuando

- `npm run build` produce un único `dist/index.html` autocontenido
- `npm test` pasa
- el archivo se abre con doble clic y muestra los registros de ejemplo
- los campos calculados muestran valores y las reglas rechazan un registro que las incumpla
- los ajustes, colores y página de inicio coinciden con lo especificado arriba

## Antes de entregarla

Decide esto tú, no lo dejes al destinatario: pon `copyright` a quien sea dueño de la herramienta, sustituye el enlace de cabecera que apunta al repositorio de openToolbox y desactiva `examplePrompts` si el destinatario solo introduce datos. Menciona el contador de aperturas (Ajustes → Seguridad) al entregarla.

---

*Generado a partir de `examples/ppwr-packaging.domain.js`, el código real de la [demo en vivo](https://m-dohmen.github.io/openToolbox/demos/ppwr-packaging/). Regenerar con `npm run prompts`.*

*Todos los datos de la demo son inventados. Ilustra la estructura de una herramienta así — no es asesoramiento legal ni prueba de conformidad.*
