<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Construa esta ferramenta para mim: Project portfolio

*Tudo o que segue é a especificação funcional. Siga-a como está; onde ela se calar, decida e diga o que assumiu.*

## Ponto de partida

Use o modelo **openToolbox**: https://github.com/m-dohmen/openToolbox. Leia primeiro o `AGENTS.md` desse repositório — ele é a autoridade sobre a forma do esquema, os tipos de campo e as regras que quebram uma compilação de arquivo único. Tudo o que é do domínio vai em um único arquivo, `src/domain.js`.

> Se o skill do openToolbox estiver instalado (`claude plugin install opentoolbox@opentoolbox`), basta colar este arquivo — ele mesmo busca o modelo.

## O problema por trás disso

Uma consultoria toca vários projetos de cliente ao mesmo tempo. Cada um tem orçamento, responsável, fase e marcos que lhe pertencem. A pergunta que se repete em todo comitê é se o projeto ainda cabe no orçamento — e a resposta costuma ser montada à mão a partir de uma planilha em que ninguém confia.

## O que precisa existir no fim

Um único arquivo HTML autocontido, aberto com duplo clique, sem servidor e sem instalação. O arquivo também é o banco de dados: salvar escreve um novo HTML com os registros embutidos.

## Tipos de registro

Chave de entidade `projects` — um registro é **project**, vários são **projects**.

**Campos**

| Chave | Rótulo | Tipo | Detalhe |
| --- | --- | --- | --- |
| `name` | Project | text | obrigatório |
| `client` | Client | text | — |
| `lead` | Engagement lead | text | cabeçalho de tabela `Lead` |
| `phase` | Phase | enum | um de: Initiation · Delivery · Rollout · Closed |
| `risk` | Risk | enum | um de: low · medium · high |
| `budget` | Budget in kEUR | number | cabeçalho de tabela `Budget` |
| `spent` | Spent in kEUR | number | cabeçalho de tabela `Spent` |
| `start` | Start | date | — |
| `end` | Planned end | date | cabeçalho de tabela `End` |
| `variance` | Budget left | computed | calculado, nunca armazenado, cabeçalho de tabela `Left` |
| `note` | Note | text | multilinha |

**Apresentação**

- Coluna principal: `name`
- Segunda linha abaixo: `client`
- Colunas da tabela, nesta ordem: `name`, `lead`, `phase`, `risk`, `budget`, `variance`, `end`
- Filtros da barra lateral: `phase`, `risk`
- Somado no resumo: `budget`
- Deixa de contar como aberto quando: `r.phase === 'Closed'`
- Marcado em vermelho quando: `r.phase !== 'Closed' && r.end && r.end < today()`

**Campos calculados**

São derivados a cada renderização e nunca gravados no registro — uma derivação armazenada fica desatualizada assim que uma de suas entradas muda.

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

**Regras de validação**

Condições entre campos. Precisam valer em um único lugar, para que o formulário, a importação CSV e o que a IA propuser passem pela mesma verificação.

- **Quando** `r.phase !== 'Initiation'` → **Então** `lead`
  **Mensagem:** „A project past initiation needs an engagement lead.“
- **Quando** `r.start && r.end` → **Então** `r.end >= r.start`
  **Mensagem:** „The planned end cannot be before the start.“

**Medidas**

Declaradas nesta entidade a partir de um catálogo fechado: contagem, soma e média sobre campos numéricos. Calculadas ao renderizar, nunca armazenadas.

*O framework formata médias com duas casas decimais na notação decimal do idioma da interface, rejeita declarações inválidas nomeando-as ao carregar em vez de escondê-las, e um clique numa medida leva à lista dessa entidade, sem filtro nesta versão.*

- **Running projects** — a quantidade de registros (apenas registros que atendem a um filtro): `r.phase !== 'Closed'` · „not yet closed“
- **Spent so far** — a soma de `spent` (Spent in kEUR) · „kEUR, all projects“
- **Average budget** — a média de `budget` (Budget in kEUR) · „kEUR per project“
- **Budget left** — a soma de `variance` (Budget left) · „kEUR remaining, computed“

---

Chave de entidade `milestones` — um registro é **milestone**, vários são **milestones**.

**Campos**

| Chave | Rótulo | Tipo | Detalhe |
| --- | --- | --- | --- |
| `title` | Milestone | text | obrigatório |
| `projectId` | Project | reference | obrigatório, referência para `projects` |
| `owner` | Owner | text | — |
| `due` | Due date | date | cabeçalho de tabela `Due` |
| `status` | Status | enum | um de: open · in progress · waiting · done |
| `effort` | Effort in days | number | cabeçalho de tabela `D` |
| `daysLeft` | Days left | computed | calculado, nunca armazenado, cabeçalho de tabela `Left` |
| `note` | Note | text | multilinha |

**Apresentação**

- Coluna principal: `title`
- Colunas da tabela, nesta ordem: `title`, `projectId`, `owner`, `due`, `daysLeft`, `status`, `effort`
- Filtros da barra lateral: `status`
- Somado no resumo: `effort`
- Deixa de contar como aberto quando: `r.status === 'done'`
- Marcado em vermelho quando: `r.status !== 'done' && r.due && r.due < today()`

**Campos calculados**

São derivados a cada renderização e nunca gravados no registro — uma derivação armazenada fica desatualizada assim que uma de suas entradas muda.

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

**Regras de validação**

Condições entre campos. Precisam valer em um único lugar, para que o formulário, a importação CSV e o que a IA propuser passem pela mesma verificação.

- **Quando** `r.status !== 'open'` → **Então** `owner`
  **Mensagem:** „A milestone that has started needs an owner.“

**Medidas**

Declaradas nesta entidade a partir de um catálogo fechado: contagem, soma e média sobre campos numéricos. Calculadas ao renderizar, nunca armazenadas.

*O framework formata médias com duas casas decimais na notação decimal do idioma da interface, rejeita declarações inválidas nomeando-as ao carregar em vez de escondê-las, e um clique numa medida leva à lista dessa entidade, sem filtro nesta versão.*

- **Milestones in progress** — a quantidade de registros (apenas registros que atendem a um filtro): `r.status === 'in progress'`
- **Average effort** — a média de `effort` (Effort in days) · „days per milestone“

---

## Painel

Blocos sobre todo o conjunto de registros, não sobre a visão filtrada.

- Um número: **Portfolio budget** — `budget` (Budget in kEUR) (apenas registros que atendem a um filtro): `r.phase !== 'Closed'` · „kEUR, running projects“
- Um número: **High risk** — a quantidade de registros (apenas registros que atendem a um filtro): `r.risk === 'high'` · „projects needing attention“
- Um número: **Overdue milestones** — a quantidade de registros (apenas registros que atendem a um filtro): `r.status !== 'done' && r.due && r.due < today()` · „across all projects“
- Um anel por valor de `phase`
- Barras por valor de `phase`, medindo `budget` (Budget in kEUR) — **Budget by phase**
- Um anel por valor de `status`

## Captura guiada

Uma sequência curta de passos para quem precisa relatar uma coisa e não conhece a ferramenta. Nada é gravado antes da confirmação do último passo — abandonar não pode deixar rastro.

Título: **Add an engagement**

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

1. **Passo 1 `projects`** — Engagement: campos: `name`, `client`, `lead`, `phase`, `risk`, `budget`, `start`, `end`
2. **Passo 2 `milestones`** — First milestone: campos: `title`, `projectId`, `owner`, `due`, `status`, `effort`
   somente quando: `drafts.projects?.phase !== 'Initiation'`
3. **Passo 3 `milestones`** — More milestones: upload de CSV, alimentando a mesma sessão
   somente quando: `Boolean(drafts.projects?.name)`
4. **Passo 4** — Check: resumo gerado a partir do esquema

Tela final: “The engagement is in the file.”

## Padrões

Defina em `DEFAULT_SETTINGS`, `DEFAULT_COLORS` e `DEFAULT_HOME` em `src/app.jsx`.

- Título: **Project portfolio**
- Subtítulo: Engagements, milestones and where the budget stands
- Nome do arquivo: `project-portfolio`
- Versão: `2.1`
- Idioma da interface: `en`
- Abre como: ferramenta completa
- Cores: `accent` #0e7c86 · `band` #16202b · `flag` #c2521b · `ok` #2e7d5b · `pending` #d19a0a

## Página inicial

O aplicativo abre com este texto. É um subconjunto pequeno de Markdown: títulos, listas, citações, negrito, itálico, código embutido e links. Use-o literalmente:

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

## Dados de demonstração

Acrescente projects: 7, milestones: 13 registros de demonstração realistas para que o arquivo não esteja vazio ao abrir. Invente-os no estilo dos campos acima; são ilustração, não os dados do usuário. Diga a ele que os próprios dados entram por **Import CSV → replace all**.

## Pronto quando

- `npm run build` produz um único `dist/index.html` autocontido
- `npm test` passa
- o arquivo abre com duplo clique e mostra os registros de demonstração
- os campos calculados mostram valores e as regras recusam um registro que as viole
- as configurações, cores e página inicial correspondem à especificação acima

## Antes de entregar

Decida você, em vez de deixar para o destinatário: defina `copyright` para quem é dono da ferramenta, substitua o link do cabeçalho que aponta para o repositório do openToolbox e desligue `examplePrompts` se o destinatário apenas registra dados. Mencione o contador de aberturas (Configurações → Segurança) na entrega.

---

*Gerado a partir de `examples/portfolio.domain.js`, o código real da [demo ao vivo](https://m-dohmen.github.io/openToolbox/demos/portfolio/). Gerar de novo com `npm run prompts`.*

*Todos os dados da demo são inventados. Ela ilustra a estrutura de uma ferramenta assim — não é aconselhamento jurídico nem prova de conformidade.*
