<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Bau mir dieses Werkzeug: Sanierung

*Alles Folgende ist die fachliche Anforderung. Halte dich daran; wo sie schweigt, entscheide selbst und sage, was du angenommen hast.*

## Ausgangspunkt

Nutze die Vorlage **openToolbox**: https://github.com/m-dohmen/openToolbox. Lies dort zuerst `AGENTS.md` — sie ist maßgeblich für Schemaform, Feldtypen und die Regeln, an denen ein Einzeldatei-Build zerbricht. Alles Fachliche kommt in eine einzige Datei, `src/domain.js`.

> Ist der openToolbox-Skill installiert (`claude plugin install opentoolbox@opentoolbox`), genügt es, diese Datei einzufügen — er holt sich die Vorlage selbst.

## Das Problem dahinter

Jemand saniert ein Haus. Für jedes Gewerk holt er drei Angebote ein: eines kommt nie, eines ist doppelt so teuer wie gedacht, und drei Monate später weiß niemand mehr, warum die Wahl auf das mittlere fiel. Am Ende steht die Frage, die jeder Bauherr zu spät stellt: **sind wir noch im Budget?** Dieselbe Form wie ein Projektportfolio, nur privat — und mit Bindefristen, die still ablaufen.

## Was am Ende dastehen muss

Eine einzelne, in sich geschlossene HTML-Datei, per Doppelklick zu öffnen, ohne Server und ohne Installation. Die Datei ist zugleich die Datenbank: Speichern schreibt eine neue HTML-Datei mit den eingebetteten Datensätzen.

## Datenarten

Entitätsschlüssel `trades` — ein Datensatz ist **Gewerk**, mehrere sind **Gewerke**.

**Felder**

| Schlüssel | Beschriftung | Typ | Näheres |
| --- | --- | --- | --- |
| `name` | Gewerk | text | Pflicht |
| `trade` | Kategorie | enum | einer von: Rohbau · Dach · Fenster · Elektro · Sanitär/Heizung · Estrich · Trockenbau · Maler · Boden · Außenanlage |
| `phase` | Stand | enum | einer von: noch nicht angefragt · Angebote eingeholt · beauftragt · in Ausführung · abgenommen |
| `budget` | Budget (€) | number | Tabellenkopf `Budget` |
| `final` | Schlussrechnung (€) | number | Tabellenkopf `Schluss` |
| `start` | Geplanter Beginn | date | Tabellenkopf `Beginn` |
| `end` | Geplantes Ende | date | Tabellenkopf `Ende` |
| `note` | Notiz | text | mehrzeilig |
| `awarded` | Beauftragt (€) | computed | berechnet, nie gespeichert, Tabellenkopf `Auftrag` |
| `variance` | Abweichung zum Budget (€) | computed | berechnet, nie gespeichert, Tabellenkopf `± €` |
| `offerCount` | Angebote | computed | berechnet, nie gespeichert, Tabellenkopf `Ang.` |

**Darstellung**

- Führende Spalte: `name`
- Zweite Zeile darunter: `trade`
- Tabellenspalten, in dieser Reihenfolge: `name`, `trade`, `phase`, `budget`, `awarded`, `variance`, `start`
- Filter in der Seitenleiste: `trade`, `phase`
- In der Übersicht summiert: `budget`
- Zählt nicht mehr als offen, wenn: `r.phase === 'abgenommen'`
- Rot markiert, wenn: `r.phase === 'noch nicht angefragt' && r.start && r.start < iso(30)`

**Berechnete Felder**

Sie werden bei jeder Anzeige gerechnet und nie in den Datensatz geschrieben — eine gespeicherte Ableitung ist in dem Moment falsch, in dem sich eine ihrer Quellen ändert.

- `awarded` (Beauftragt (€)) — `offersOf(r.id).filter((o) => o.state === 'beauftragt').reduce((s, o) => s + (Number(o.amount) || 0), 0)`
- `variance` (Abweichung zum Budget (€)):

  ```js
  const awarded = offersOf(r.id)
    .filter((o) => o.state === 'beauftragt')
    .reduce((s, o) => s + (Number(o.amount) || 0), 0)
  const actual = Number(r.final) || awarded
  if (!actual) return ''
  return (Number(r.budget) || 0) - actual
  ```
- `offerCount` (Angebote) — `offersOf(r.id).length`

**Prüfregeln**

Bedingungen zwischen Feldern. Sie müssen an einer Stelle greifen, damit Formular, CSV-Import und die Vorschläge der KI durch dieselbe Prüfung laufen.

- **Wenn** `r.phase === 'beauftragt' || r.phase === 'in Ausführung' || r.phase === 'abgenommen'` → **Dann** `offersOf(r.id).some((o) => o.state === 'beauftragt')`
  **Meldung:** „Beauftragt heißt: eines der Angebote steht auf „beauftragt".“
- **Wenn** `r.phase === 'abgenommen'` → **Dann** `final`
  **Meldung:** „Nach der Abnahme gehört die Schlussrechnung dazu — sonst bleibt die Abweichung geraten.“
- **Wenn** `Boolean(r.start && r.end)` → **Dann** `r.end >= r.start`
  **Meldung:** „Das Ende kann nicht vor dem Beginn liegen.“

---

Entitätsschlüssel `offers` — ein Datensatz ist **Angebot**, mehrere sind **Angebote**.

**Felder**

| Schlüssel | Beschriftung | Typ | Näheres |
| --- | --- | --- | --- |
| `company` | Firma | text | Pflicht |
| `tradeId` | Gewerk | reference | Pflicht, Referenz auf `trades` |
| `contact` | Ansprechpartner | text | Tabellenkopf `Kontakt` |
| `amount` | Summe brutto (€) | number | Tabellenkopf `€` |
| `received` | Eingegangen am | date | Tabellenkopf `Eingang` |
| `validUntil` | Bindefrist bis | date | Tabellenkopf `Bindefrist` |
| `state` | Status | enum | einer von: angefragt · liegt vor · nachverhandelt · abgelehnt · beauftragt |
| `pdf` | Angebot als Datei | attachment | hochgeladene Datei, im Datensatz abgelegt, Tabellenkopf `PDF` |
| `note` | Notiz | text | mehrzeilig |
| `perBudget` | Anteil am Budget | computed | berechnet, nie gespeichert, Tabellenkopf `% Budget` |

**Darstellung**

- Führende Spalte: `company`
- Tabellenspalten, in dieser Reihenfolge: `company`, `tradeId`, `amount`, `perBudget`, `received`, `validUntil`, `state`
- Filter in der Seitenleiste: `state`
- In der Übersicht summiert: `amount`
- Zählt nicht mehr als offen, wenn: `r.state === 'beauftragt' || r.state === 'abgelehnt'`
- Rot markiert, wenn: `r.state !== 'abgelehnt' && r.state !== 'beauftragt' && r.validUntil && r.validUntil < iso(0)`

**Berechnete Felder**

Sie werden bei jeder Anzeige gerechnet und nie in den Datensatz geschrieben — eine gespeicherte Ableitung ist in dem Moment falsch, in dem sich eine ihrer Quellen ändert.

- `perBudget` (Anteil am Budget):

  ```js
  const trade = ENTITIES.trades.seed().find((t) => t.id === r.tradeId)
  if (!trade?.budget) return ''
  return Math.round(((Number(r.amount) || 0) / trade.budget) * 100) + ' %'
  ```

**Prüfregeln**

Bedingungen zwischen Feldern. Sie müssen an einer Stelle greifen, damit Formular, CSV-Import und die Vorschläge der KI durch dieselbe Prüfung laufen.

- **Wenn** `r.state !== 'angefragt'` → **Dann** `amount`, `received`
  **Meldung:** „Ein vorliegendes Angebot hat eine Summe und ein Eingangsdatum.“
- **Wenn** `r.state === 'abgelehnt'` → **Dann** `note`
  **Meldung:** „Warum abgelehnt? In drei Monaten weiß das sonst niemand mehr.“

---

## Dashboard

Kacheln über den gesamten Bestand, nicht über die gefilterte Ansicht.

- Eine Zahl: **Budget gesamt** — `budget` (Budget (€)) · „€ über alle Gewerke“
- Eine Zahl: **Beauftragt** — `amount` (Summe brutto (€)) (nur Datensätze, die einem Filter entsprechen): `r.state === 'beauftragt'` · „€ bereits vergeben“
- Eine Zahl: **Nicht angefragt** — der Anzahl (nur Datensätze, die einem Filter entsprechen): `r.phase === 'noch nicht angefragt'` · „Gewerke ohne Angebot“
- Ein Ring je Ausprägung von `phase`
- Balken je Ausprägung von `trade`, gemessen an `budget` (Budget (€)) — **Budget je Kategorie (€)**
- Balken je Ausprägung von `state`, gemessen an der Anzahl — **Angebote nach Status**

## Geführte Erfassung

Eine kurze Schrittfolge für jemanden, der eine Sache melden soll und das Werkzeug nicht kennt. Geschrieben wird nichts, bevor der letzte Schritt bestätigt ist — ein Abbruch darf nichts hinterlassen.

Titel: **Angebot erfassen**

> Erst das Gewerk, dann das Angebot dazu. Beides entsteht in einem Durchgang.

1. **Schritt 1 `trades`** — Gewerk: Felder: `name`, `trade`, `budget`, `start`, `end`
2. **Schritt 2 `offers`** — Angebot: Felder: `company`, `tradeId`, `contact`, `amount`, `received`, `validUntil`, `state`, `note`
   nur wenn: `Boolean(drafts.trades?.name)`
3. **Schritt 3** — Prüfen: Zusammenfassung, aus dem Schema erzeugt

Abschluss: „Erfasst. Die Abweichung zum Budget rechnet sich mit.“

## Vorgaben

In `DEFAULT_SETTINGS`, `DEFAULT_COLORS` und `DEFAULT_HOME` in `src/app.jsx` setzen.

- Titel: **Sanierung**
- Untertitel: Gewerke, Angebote und wo das Budget steht
- Dateiname: `sanierung`
- Version: `1.0`
- Oberflächensprache: `de`
- Öffnet als: vollständiges Werkzeug
- Farben: `accent` #6b4a8c · `band` #241b2e · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## Startseite

Die Anwendung öffnet auf diesem Text. Er ist ein kleiner Markdown-Teilsatz — Überschriften, Listen, Zitate, fett, kursiv, Inline-Code und Verweise. Wörtlich übernehmen:

```markdown
# Sanierung: Gewerke und Angebote

Für jedes Gewerk holt man drei Angebote ein. Eines kommt nie, eines ist doppelt so teuer wie
gedacht, und drei Monate später weiß niemand mehr, warum die Wahl auf den mittleren fiel. Am Ende
steht die Frage, die jeder Bauherr zu spät stellt: **sind wir noch im Budget?**

## Was diese Demo zeigt

- **Mehrfachauswahl und Sammelaktionen** — das Kontrollkästchen am Zeilenanfang wählt aus,
  Umschalt+Klick nimmt einen ganzen Bereich dazu, der Kopf wählt alles Sichtbare. In der
  Aktionsleiste darunter setzt der Status aller Gewählten auf einen Wert oder sie werden gemeinsam
  gelöscht — ein Durchlauf, ein Protokollschritt, ein Strg+Z für alles. Zusammen mit dem Filter
  „liegt vor" bekommen die unterlegenen Anbieter eines Gewerks ihre Absage in einem Rutsch; die
  Regel bleibt auch im Schwarm wirksam: die Absage ohne Begründung wird übersprungen und benannt.
- **Duplizieren** — Angebote je Gewerk wiederholen sich: das nächste entsteht als Kopie eines
  vorhandenen (Aktion im Zeilenmenü oder im offenen Datensatz), Gewerk-Verweis und Bindefrist
  bleiben stehen, geändert werden nur Firma, Kontakt und Summe. Die Kopie erscheint im
  Änderungsprotokoll und lässt sich mit Strg+Z wieder entfernen.
- **Zwei Datenarten**: ein Angebot ohne sein Gewerk hat keine Aussage, ein Gewerk bekommt seine
  Zahl erst durch die Angebote.
- **Die Auftragssumme wird nicht getippt**, sie steht im beauftragten Angebot und wird von dort
  geholt. Abweichung zum Budget ebenso.
- **Ablaufende Bindefristen werden rot** — der teuerste übersehene Termin am Bau.
- **Regeln gegen das Vergessen**: ein abgelehntes Angebot verlangt eine Begründung.

> Erfundene Zahlen und Firmen.
```

## Beispieldaten

Lege Gewerke: 10, Angebote: 17 realistische Beispieldatensätze an, damit die Datei beim ersten Öffnen nicht leer ist. Erfinde sie im Stil der Felder oben; sie sind Anschauung, nicht die Daten des Nutzers. Sag ihm, dass seine eigenen Daten über **Import CSV → alle ersetzen** hineinkommen.

## Fertig, wenn

- `npm run build` eine einzelne, geschlossene `dist/index.html` erzeugt
- `npm test` durchläuft
- die Datei per Doppelklick öffnet und die Beispieldatensätze zeigt
- die berechneten Felder Werte zeigen und die Regeln einen verstoßenden Datensatz abweisen
- Einstellungen, Farben und Startseite der Vorgabe oben entsprechen

## Vor der Übergabe

Entscheide das selbst, statt es dem Empfänger zu überlassen: `copyright` auf den Eigentümer des Werkzeugs setzen, den Kopfzeilen-Verweis auf das openToolbox-Repository ersetzen und `examplePrompts` abschalten, wenn der Empfänger nur Daten pflegt. Den Aufrufzähler (Einstellungen → Sicherheit) bei der Übergabe ansprechen, statt ihn später in einem Netzwerkprotokoll entdecken zu lassen.

---

*Erzeugt aus `examples/renovation-quotes.domain.js` — dem laufenden Quelltext der [Live-Demo](https://m-dohmen.github.io/openToolbox/demos/renovation-quotes/). Neu erzeugen mit `npm run prompts`.*

*Alle Daten der Demo sind erfunden. Sie veranschaulicht die Struktur eines solchen Werkzeugs — sie ist keine Rechtsberatung und kein Nachweis von Konformität.*
