<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# このツールを作ってください: Sanierung

*以下がすべて機能要件です。書かれているとおりに実装してください。書かれていない点は自分で判断し、何を前提にしたかを述べてください。*

## 出発点

**openToolbox** テンプレートを使ってください：https://github.com/m-dohmen/openToolbox。まずそのリポジトリの `AGENTS.md` を読むこと — スキーマの形、フィールド型、単一ファイルビルドを壊す禁止事項について、そこが正典です。業務固有の内容はすべて `src/domain.js` 一つに収めます。

> openToolbox skill を導入済みなら（`claude plugin install opentoolbox@opentoolbox`）、このファイルを貼るだけで足ります。テンプレートは skill が自分で取得します。

## 背景にある問題

ある人が家を改修しています。工種ごとに 3 社から見積もりを取る。1 社は永遠に返ってこない。1 社は想定の倍。そして 3 か月後、なぜ真ん中の 1 社に決めたのか誰も覚えていない。最後に、すべての施主が手遅れになってから尋ねる問いが来ます。**まだ予算内か。** 案件ポートフォリオと同じ構造ですが、こちらは私生活の話で、しかも静かに切れていく見積有効期限があります。

## 最終的に何が出来ていればよいか

ダブルクリックで開ける自己完結型の HTML ファイル 1 つ。サーバーもインストールも不要です。ファイル自体がデータベースでもあり、保存するとレコードを埋め込んだ新しい HTML ファイルが書き出されます。

## レコード種別

エンティティキー `trades` — 1 件は **Gewerk**、複数は **Gewerke**。

**フィールド**

| キー | ラベル | 型 | 補足 |
| --- | --- | --- | --- |
| `name` | Gewerk | text | 必須 |
| `trade` | Kategorie | enum | 次のいずれか：Rohbau · Dach · Fenster · Elektro · Sanitär/Heizung · Estrich · Trockenbau · Maler · Boden · Außenanlage |
| `phase` | Stand | enum | 次のいずれか：noch nicht angefragt · Angebote eingeholt · beauftragt · in Ausführung · abgenommen |
| `budget` | Budget (€) | number | 表ヘッダー `Budget` |
| `final` | Schlussrechnung (€) | number | 表ヘッダー `Schluss` |
| `start` | Geplanter Beginn | date | 表ヘッダー `Beginn` |
| `end` | Geplantes Ende | date | 表ヘッダー `Ende` |
| `note` | Notiz | text | 複数行 |
| `awarded` | Beauftragt (€) | computed | 計算値。保存はしない, 表ヘッダー `Auftrag` |
| `variance` | Abweichung zum Budget (€) | computed | 計算値。保存はしない, 表ヘッダー `± €` |
| `offerCount` | Angebote | computed | 計算値。保存はしない, 表ヘッダー `Ang.` |

**表示**

- 主列：`name`
- 主列の下に置く 2 行目：`trade`
- 表の列（この順）：`name`, `trade`, `phase`, `budget`, `awarded`, `variance`, `start`
- サイドバーの絞り込み：`trade`, `phase`
- 概要で合計する：`budget`
- 未完了として数えなくなる条件：`r.phase === 'abgenommen'`
- 赤で強調する条件：`r.phase === 'noch nicht angefragt' && r.start && r.start < iso(30)`

**計算フィールド**

描画のたびに算出し、レコードには絶対に書き戻しません。保存した派生値は、入力のどれかが変わった瞬間に誤りになります。

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

**検証ルール**

フィールドをまたぐ条件です。入力フォーム、CSV 取り込み、AI の提案がすべて同じチェックを通るよう、一箇所で効かせてください。

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

---

エンティティキー `offers` — 1 件は **Angebot**、複数は **Angebote**。

**フィールド**

| キー | ラベル | 型 | 補足 |
| --- | --- | --- | --- |
| `company` | Firma | text | 必須 |
| `tradeId` | Gewerk | reference | 必須, `trades` への参照 |
| `contact` | Ansprechpartner | text | 表ヘッダー `Kontakt` |
| `amount` | Summe brutto (€) | number | 表ヘッダー `€` |
| `received` | Eingegangen am | date | 表ヘッダー `Eingang` |
| `validUntil` | Bindefrist bis | date | 表ヘッダー `Bindefrist` |
| `state` | Status | enum | 次のいずれか：angefragt · liegt vor · nachverhandelt · abgelehnt · beauftragt |
| `pdf` | Angebot als Datei | attachment | アップロードしたファイル。レコード内に格納, 表ヘッダー `PDF` |
| `note` | Notiz | text | 複数行 |
| `perBudget` | Anteil am Budget | computed | 計算値。保存はしない, 表ヘッダー `% Budget` |

**表示**

- 主列：`company`
- 表の列（この順）：`company`, `tradeId`, `amount`, `perBudget`, `received`, `validUntil`, `state`
- サイドバーの絞り込み：`state`
- 概要で合計する：`amount`
- 未完了として数えなくなる条件：`r.state === 'beauftragt' || r.state === 'abgelehnt'`
- 赤で強調する条件：`r.state !== 'abgelehnt' && r.state !== 'beauftragt' && r.validUntil && r.validUntil < iso(0)`

**計算フィールド**

描画のたびに算出し、レコードには絶対に書き戻しません。保存した派生値は、入力のどれかが変わった瞬間に誤りになります。

- `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) + ' %'
  ```

**検証ルール**

フィールドをまたぐ条件です。入力フォーム、CSV 取り込み、AI の提案がすべて同じチェックを通るよう、一箇所で効かせてください。

- **条件** `r.state !== 'angefragt'` → **要求** `amount`, `received`
  **メッセージ:** „Ein vorliegendes Angebot hat eine Summe und ein Eingangsdatum.“
- **条件** `r.state === 'abgelehnt'` → **要求** `note`
  **メッセージ:** „Warum abgelehnt? In drei Monaten weiß das sonst niemand mehr.“

---

## ダッシュボード

絞り込み後ではなく、レコード全体を対象とするタイルです。

- 数値 1 つ：**Budget gesamt** — `budget` (Budget (€)) · „€ über alle Gewerke“
- 数値 1 つ：**Beauftragt** — `amount` (Summe brutto (€))（フィルタに一致するレコードのみ）: `r.state === 'beauftragt'` · „€ bereits vergeben“
- 数値 1 つ：**Nicht angefragt** — 件数（フィルタに一致するレコードのみ）: `r.phase === 'noch nicht angefragt'` · „Gewerke ohne Angebot“
- `phase` の値ごとのドーナツ
- `trade` の値ごとの棒。指標は `budget` (Budget (€)) — **Budget je Kategorie (€)**
- `state` の値ごとの棒。指標は 件数 — **Angebote nach Status**

## ガイド付き入力

1 件だけ報告すればよく、ツールに不慣れな人のための短い手順です。最後の手順を確定するまで何も書き込まれません。途中でやめても何も残らないこと。

タイトル：**Angebot erfassen**

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

1. **ステップ 1 `trades`** — Gewerk: フィールド：`name`, `trade`, `budget`, `start`, `end`
2. **ステップ 2 `offers`** — Angebot: フィールド：`company`, `tradeId`, `contact`, `amount`, `received`, `validUntil`, `state`, `note`
   次の場合のみ表示：`Boolean(drafts.trades?.name)`
3. **ステップ 3** — Prüfen: スキーマから生成される確認画面

完了画面：「Erfasst. Die Abweichung zum Budget rechnet sich mit.」

## 既定値

`src/app.jsx` の `DEFAULT_SETTINGS`、`DEFAULT_COLORS`、`DEFAULT_HOME` に設定します。

- タイトル: **Sanierung**
- サブタイトル: Gewerke, Angebote und wo das Budget steht
- ファイル名: `sanierung`
- バージョン: `1.0`
- 画面の言語: `de`
- 起動時の表示: 通常のツール
- 配色: `accent` #6b4a8c · `band` #241b2e · `flag` #b4442e · `ok` #3f7a5c · `pending` #c08a12

## スタートページ

アプリはこの文章から始まります。Markdown の小さな部分集合（見出し、箇条書き、引用、太字、斜体、インラインコード、リンク）です。そのまま使ってください：

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

## サンプルデータ

初回起動時に空にならないよう、現実味のあるサンプルレコードを Gewerke: 10, Angebote: 17 件用意してください。上のフィールドに合わせて創作します。これは例示であってユーザーのデータではありません。本番データは **Import CSV → replace all** から入れる、と伝えてください。

## 完了条件

- `npm run build` が自己完結した `dist/index.html` を 1 つ出力する
- `npm test` が通る
- ダブルクリックで開き、サンプルレコードが表示される
- 計算フィールドに値が出て、ルールに反するレコードは保存を拒否される
- 設定・配色・スタートページが上の仕様どおりである

## 引き渡す前に

受け取る側に委ねず、自分で決めてください：`copyright` をツールの所有者に設定し、openToolbox リポジトリを指すヘッダーリンクを差し替え、相手がデータ入力だけなら `examplePrompts` を切ります。利用カウンター（設定 → セキュリティ）は引き渡し時に自分から伝えること。

---

*`examples/renovation-quotes.domain.js` から生成 — [ライブデモ](https://m-dohmen.github.io/openToolbox/demos/renovation-quotes/) の実際のソースです。再生成は `npm run prompts`。*

*デモのデータはすべて架空です。この種のツールの構造を示すものであり、法的助言でも適合性の証明でもありません。*
