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

*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

Eine Beratung führt mehrere Kundenprojekte gleichzeitig. Jedes hat Budget, Verantwortliche, Phase und Meilensteine, die dazugehören. Die Frage in jedem Lenkungsausschuss ist, ob das Projekt noch im Budget liegt — und die Antwort wird meist von Hand aus einer Tabelle zusammengesucht, der niemand traut.

## 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 `projects` — ein Datensatz ist **project**, mehrere sind **projects**.

**Felder**

| Schlüssel | Beschriftung | Typ | Näheres |
| --- | --- | --- | --- |
| `name` | Project | text | Pflicht |
| `client` | Client | text | — |
| `lead` | Engagement lead | text | Tabellenkopf `Lead` |
| `phase` | Phase | enum | einer von: Initiation · Delivery · Rollout · Closed |
| `risk` | Risk | enum | einer von: low · medium · high |
| `budget` | Budget in kEUR | number | Tabellenkopf `Budget` |
| `spent` | Spent in kEUR | number | Tabellenkopf `Spent` |
| `start` | Start | date | — |
| `end` | Planned end | date | Tabellenkopf `End` |
| `variance` | Budget left | computed | berechnet, nie gespeichert, Tabellenkopf `Left` |
| `note` | Note | text | mehrzeilig |

**Darstellung**

- Führende Spalte: `name`
- Zweite Zeile darunter: `client`
- Tabellenspalten, in dieser Reihenfolge: `name`, `lead`, `phase`, `risk`, `budget`, `variance`, `end`
- Filter in der Seitenleiste: `phase`, `risk`
- In der Übersicht summiert: `budget`
- Zählt nicht mehr als offen, wenn: `r.phase === 'Closed'`
- Rot markiert, wenn: `r.phase !== 'Closed' && r.end && r.end < today()`

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

- `variance` (Budget left) — `(Number(r.budget) || 0) - (Number(r.spent) || 0)`

**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 !== 'Initiation'` → **Dann** `lead`
  **Meldung:** „A project past initiation needs an engagement lead.“
- **Wenn** `r.start && r.end` → **Dann** `r.end >= r.start`
  **Meldung:** „The planned end cannot be before the start.“

**Kennzahl-Kacheln**

An dieser Entität aus einem geschlossenen Katalog deklariert — count, Summe und Mittelwert über Zahlenfelder. Gerechnet beim Rendern, nie gespeichert.

*Das Framework formatiert Mittelwerte mit zwei Nachkommastellen im Dezimalzeichen der Oberflächensprache, weist ungültige Deklarationen beim Laden benannt zurück, statt sie still zu verbergen, und ein Klick auf eine Kachel springt ungefiltert zur Liste dieser Entität.*

- **Running projects** — die Anzahl der Datensätze (nur Datensätze, die einem Filter entsprechen): `r.phase !== 'Closed'` · „not yet closed“
- **Spent so far** — die Summe von `spent` (Spent in kEUR) · „kEUR, all projects“
- **Average budget** — der Mittelwert von `budget` (Budget in kEUR) · „kEUR per project“
- **Budget left** — die Summe von `variance` (Budget left) · „kEUR remaining, computed“

---

Entitätsschlüssel `milestones` — ein Datensatz ist **milestone**, mehrere sind **milestones**.

**Felder**

| Schlüssel | Beschriftung | Typ | Näheres |
| --- | --- | --- | --- |
| `title` | Milestone | text | Pflicht |
| `projectId` | Project | reference | Pflicht, Referenz auf `projects` |
| `owner` | Owner | text | — |
| `due` | Due date | date | Tabellenkopf `Due` |
| `status` | Status | enum | einer von: open · in progress · waiting · done |
| `effort` | Effort in days | number | Tabellenkopf `D` |
| `daysLeft` | Days left | computed | berechnet, nie gespeichert, Tabellenkopf `Left` |
| `note` | Note | text | mehrzeilig |

**Darstellung**

- Führende Spalte: `title`
- Tabellenspalten, in dieser Reihenfolge: `title`, `projectId`, `owner`, `due`, `daysLeft`, `status`, `effort`
- Filter in der Seitenleiste: `status`
- In der Übersicht summiert: `effort`
- Zählt nicht mehr als offen, wenn: `r.status === 'done'`
- Rot markiert, wenn: `r.status !== 'done' && r.due && r.due < today()`

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

- `daysLeft` (Days left):

  ```js
  if (!r.due || r.status === 'done') return ''
  const due = localDateFromIso(r.due)
  if (!due) return ''
  // Both sides as whole local calendar days: Date.UTC on the day
  // components keeps the difference an exact day count, free of the
  // daylight-saving hours that break a plain millisecond division -
  // and of mixing a UTC-midnight constructor with local midnight.
  const now = new Date()
  const days =
    (Date.UTC(due.getFullYear(), due.getMonth(), due.getDate()) -
      Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())) /
    86400000
  return days
  ```

**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.status !== 'open'` → **Dann** `owner`
  **Meldung:** „A milestone that has started needs an owner.“

**Kennzahl-Kacheln**

An dieser Entität aus einem geschlossenen Katalog deklariert — count, Summe und Mittelwert über Zahlenfelder. Gerechnet beim Rendern, nie gespeichert.

*Das Framework formatiert Mittelwerte mit zwei Nachkommastellen im Dezimalzeichen der Oberflächensprache, weist ungültige Deklarationen beim Laden benannt zurück, statt sie still zu verbergen, und ein Klick auf eine Kachel springt ungefiltert zur Liste dieser Entität.*

- **Milestones in progress** — die Anzahl der Datensätze (nur Datensätze, die einem Filter entsprechen): `r.status === 'in progress'`
- **Average effort** — der Mittelwert von `effort` (Effort in days) · „days per milestone“

---

## Dashboard

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

- Eine Zahl: **Portfolio budget** — `budget` (Budget in kEUR) (nur Datensätze, die einem Filter entsprechen): `r.phase !== 'Closed'` · „kEUR, running projects“
- Eine Zahl: **High risk** — der Anzahl (nur Datensätze, die einem Filter entsprechen): `r.risk === 'high'` · „projects needing attention“
- Eine Zahl: **Overdue milestones** — der Anzahl (nur Datensätze, die einem Filter entsprechen): `r.status !== 'done' && r.due && r.due < today()` · „across all projects“
- Ein Ring je Ausprägung von `phase`
- Balken je Ausprägung von `phase`, gemessen an `budget` (Budget in kEUR) — **Budget by phase**
- Ein Ring je Ausprägung von `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: **Add an engagement**

> Three steps: the engagement, its first milestone, then a check before anything is written.

1. **Schritt 1 `projects`** — Engagement: Felder: `name`, `client`, `lead`, `phase`, `risk`, `budget`, `start`, `end`
2. **Schritt 2 `milestones`** — First milestone: Felder: `title`, `projectId`, `owner`, `due`, `status`, `effort`
   nur wenn: `drafts.projects?.phase !== 'Initiation'`
3. **Schritt 3 `milestones`** — More milestones: CSV-Upload, zahlt in denselben Durchlauf ein
   nur wenn: `Boolean(drafts.projects?.name)`
4. **Schritt 4** — Check: Zusammenfassung, aus dem Schema erzeugt

Abschluss: „The engagement is in the file.“

## Vorgaben

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

- Titel: **Project portfolio**
- Untertitel: Engagements, milestones and where the budget stands
- Dateiname: `project-portfolio`
- Version: `2.1`
- Oberflächensprache: `en`
- Öffnet als: vollständiges Werkzeug
- Farben: `accent` #0e7c86 · `band` #16202b · `flag` #c2521b · `ok` #2e7d5b · `pending` #d19a0a

## 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
# Project portfolio

A worked example built with **openToolbox**: engagements, their milestones, and where the budget
stands. Everything you see comes out of one file, `src/domain.js`.

## What to try

- **List** — two record types that reference each other, calculated columns, filters that count
- **Search and filter** — the box in the header searches every field of both record types at once,
  and the sidebar narrows by contained text and by number and date ranges; active filters appear
  as removable chips above the table
- **Dashboard** — the same data as tiles, drawn without a charting library, plus a due-date widget
  grouping milestones into overdue, this week and the next 30 days, and metric tiles — running
  projects, spend, averages — computed from the data rather than typed
- **Guided entry** — a short wizard that creates an engagement and its first milestone in one run
- **Merge a file** — reconcile a copy that came back from someone else

> This page is editable in the app itself. In a tool you deliver, put here what the recipients
> need: what it is for, who maintains it, and where to ask.
```

## Beispieldaten

Lege projects: 7, milestones: 13 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/portfolio.domain.js` — dem laufenden Quelltext der [Live-Demo](https://m-dohmen.github.io/openToolbox/demos/portfolio/). 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.*
