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

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

## 出発点

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

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

## 背景にある問題

コンサルティング会社が複数の顧客案件を並行して進めています。案件ごとに予算・責任者・フェーズがあり、そこに紐づくマイルストーンがあります。ステアリング会議で毎回出るのは「まだ予算内か」という問いで、その答えはたいてい、誰も信用していない表計算から手作業で組み立てられます。

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

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

## レコード種別

エンティティキー `projects` — 1 件は **project**、複数は **projects**。

**フィールド**

| キー | ラベル | 型 | 補足 |
| --- | --- | --- | --- |
| `name` | Project | text | 必須 |
| `client` | Client | text | — |
| `lead` | Engagement lead | text | 表ヘッダー `Lead` |
| `phase` | Phase | enum | 次のいずれか：Initiation · Delivery · Rollout · Closed |
| `risk` | Risk | enum | 次のいずれか：low · medium · high |
| `budget` | Budget in kEUR | number | 表ヘッダー `Budget` |
| `spent` | Spent in kEUR | number | 表ヘッダー `Spent` |
| `start` | Start | date | — |
| `end` | Planned end | date | 表ヘッダー `End` |
| `variance` | Budget left | computed | 計算値。保存はしない, 表ヘッダー `Left` |
| `note` | Note | text | 複数行 |

**表示**

- 主列：`name`
- 主列の下に置く 2 行目：`client`
- 表の列（この順）：`name`, `lead`, `phase`, `risk`, `budget`, `variance`, `end`
- サイドバーの絞り込み：`phase`, `risk`
- 概要で合計する：`budget`
- 未完了として数えなくなる条件：`r.phase === 'Closed'`
- 赤で強調する条件：`r.phase !== 'Closed' && r.end && r.end < today()`

**計算フィールド**

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

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

**検証ルール**

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

- **条件** `r.phase !== 'Initiation'` → **要求** `lead`
  **メッセージ:** „A project past initiation needs an engagement lead.“
- **条件** `r.start && r.end` → **要求** `r.end >= r.start`
  **メッセージ:** „The planned end cannot be before the start.“

**指標タイル**

このエンティティに閉じたカタログから宣言します——件数、数値フィールドの合計と平均。描画時に計算され、保存されません。

*フレームワークは平均を小数点以下2桁で、インターフェース言語の小数記号により書式化し、無効な宣言は黙って隠さず読み込み時に名前付きで拒否します。タイルをクリックすると、このバージョンでは絞り込みなしでそのエンティティのリストに移動します。*

- **Running projects** — 件数（フィルタに一致するレコードのみ）: `r.phase !== 'Closed'` · „not yet closed“
- **Spent so far** — `spent` (Spent in kEUR) の合計 · „kEUR, all projects“
- **Average budget** — `budget` (Budget in kEUR) の平均 · „kEUR per project“
- **Budget left** — `variance` (Budget left) の合計 · „kEUR remaining, computed“

---

エンティティキー `milestones` — 1 件は **milestone**、複数は **milestones**。

**フィールド**

| キー | ラベル | 型 | 補足 |
| --- | --- | --- | --- |
| `title` | Milestone | text | 必須 |
| `projectId` | Project | reference | 必須, `projects` への参照 |
| `owner` | Owner | text | — |
| `due` | Due date | date | 表ヘッダー `Due` |
| `status` | Status | enum | 次のいずれか：open · in progress · waiting · done |
| `effort` | Effort in days | number | 表ヘッダー `D` |
| `daysLeft` | Days left | computed | 計算値。保存はしない, 表ヘッダー `Left` |
| `note` | Note | text | 複数行 |

**表示**

- 主列：`title`
- 表の列（この順）：`title`, `projectId`, `owner`, `due`, `daysLeft`, `status`, `effort`
- サイドバーの絞り込み：`status`
- 概要で合計する：`effort`
- 未完了として数えなくなる条件：`r.status === 'done'`
- 赤で強調する条件：`r.status !== 'done' && r.due && r.due < today()`

**計算フィールド**

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

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

**検証ルール**

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

- **条件** `r.status !== 'open'` → **要求** `owner`
  **メッセージ:** „A milestone that has started needs an owner.“

**指標タイル**

このエンティティに閉じたカタログから宣言します——件数、数値フィールドの合計と平均。描画時に計算され、保存されません。

*フレームワークは平均を小数点以下2桁で、インターフェース言語の小数記号により書式化し、無効な宣言は黙って隠さず読み込み時に名前付きで拒否します。タイルをクリックすると、このバージョンでは絞り込みなしでそのエンティティのリストに移動します。*

- **Milestones in progress** — 件数（フィルタに一致するレコードのみ）: `r.status === 'in progress'`
- **Average effort** — `effort` (Effort in days) の平均 · „days per milestone“

---

## ダッシュボード

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

- 数値 1 つ：**Portfolio budget** — `budget` (Budget in kEUR)（フィルタに一致するレコードのみ）: `r.phase !== 'Closed'` · „kEUR, running projects“
- 数値 1 つ：**High risk** — 件数（フィルタに一致するレコードのみ）: `r.risk === 'high'` · „projects needing attention“
- 数値 1 つ：**Overdue milestones** — 件数（フィルタに一致するレコードのみ）: `r.status !== 'done' && r.due && r.due < today()` · „across all projects“
- `phase` の値ごとのドーナツ
- `phase` の値ごとの棒。指標は `budget` (Budget in kEUR) — **Budget by phase**
- `status` の値ごとのドーナツ

## ガイド付き入力

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

タイトル：**Add an engagement**

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

1. **ステップ 1 `projects`** — Engagement: フィールド：`name`, `client`, `lead`, `phase`, `risk`, `budget`, `start`, `end`
2. **ステップ 2 `milestones`** — First milestone: フィールド：`title`, `projectId`, `owner`, `due`, `status`, `effort`
   次の場合のみ表示：`drafts.projects?.phase !== 'Initiation'`
3. **ステップ 3 `milestones`** — More milestones: CSV 取り込み。同じ入力セッションに合流します
   次の場合のみ表示：`Boolean(drafts.projects?.name)`
4. **ステップ 4** — Check: スキーマから生成される確認画面

完了画面：「The engagement is in the file.」

## 既定値

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

- タイトル: **Project portfolio**
- サブタイトル: Engagements, milestones and where the budget stands
- ファイル名: `project-portfolio`
- バージョン: `2.1`
- 画面の言語: `en`
- 起動時の表示: 通常のツール
- 配色: `accent` #0e7c86 · `band` #16202b · `flag` #c2521b · `ok` #2e7d5b · `pending` #d19a0a

## スタートページ

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

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

## サンプルデータ

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

## 完了条件

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

## 引き渡す前に

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

---

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

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