<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Build me this tool: Prüfbuch Betriebsmittel

*Everything below is the functional specification. Follow it as written; where it is silent, use your judgement and say what you assumed.*

## Start here

Use the **openToolbox** template: https://github.com/m-dohmen/openToolbox. Read `AGENTS.md` in that repository first — it is the authority on the schema shape, the field types and the rules that break a single-file build. Everything domain-specific goes into one file, `src/domain.js`.

> If you have the openToolbox skill installed (`claude plugin install opentoolbox@opentoolbox`), just paste this file — it fetches the template itself.

## The problem this solves

Every workshop, trade business and childcare centre has equipment that must be tested at regular intervals — the drill, the extension lead, the coffee machine, the heat gun. The test itself takes minutes. The administration eats the afternoon, because the protocol sits in a binder, the intervals sit in the foreman’s head, and after an accident the accident insurer asks exactly one question: **when was this device last tested?**

## What it has to be at the end

One self-contained HTML file, opened by double-click, no server and no installation. The file is also the database: saving writes a new HTML file with the records embedded in it.

## The record

One record is a **Betriebsmittel**, several are **Betriebsmittel**.

**Fields**

| Key | Label | Type | Detail |
| --- | --- | --- | --- |
| `name` | Gerät | text | required |
| `kind` | Art | enum | one of: Handmaschine · Verlängerung/Leitung · Küchengerät · Messgerät · Ladegerät · Ortsfest |
| `serial` | Inventar-/Seriennummer | text | table header `Nr.` |
| `location` | Standort | enum | one of: Werkstatt · Baustellenwagen · Lager · Büro · Küche · ausgeliehen, table header `Ort` |
| `holder` | In der Hand von | text | table header `Bei` |
| `interval` | Prüfintervall (Monate) | enum | one of: 6 · 12 · 24, table header `Int.` |
| `lastTest` | Letzte Prüfung | date | table header `Geprüft` |
| `tester` | Prüfer | text | — |
| `result` | Ergebnis | enum | one of: bestanden · bestanden mit Mangel · nicht bestanden · noch nicht geprüft |
| `defect` | Festgestellter Mangel | text | multi-line, table header `Mangel` |
| `protocol` | Prüfprotokoll | attachment | uploaded file, stored in the record, table header `Protokoll` |
| `note` | Notiz | text | multi-line |
| `due` | Nächste Prüfung | computed | calculated, never stored, table header `Fällig` |
| `daysLeft` | Tage bis zur Prüfung | computed | calculated, never stored, table header `Tage` |

**Presentation**

- Headline column: `name`
- Second line under the headline: `location`
- Table columns, in this order: `name`, `kind`, `location`, `lastTest`, `due`, `daysLeft`, `result`
- Sidebar filters: `kind`, `location`, `result`
- No longer counts as open when: `() => false`
- Flagged red when: `{ if (r.result === 'nicht bestanden') return true const d = dueDate(r) return !d || d < iso(0) }`

**Calculated fields**

These are derived on every render and never written into the record — a stored derivation goes stale the moment one of its inputs changes.

- `due` (Nächste Prüfung):

  ```js
  const d = dueDate(r)
  if (!d) return ''
  const [y, m, day] = d.split('-')
  return `${day}.${m}.${y}`
  ```
- `daysLeft` (Tage bis zur Prüfung):

  ```js
  const d = localDateFromIso(dueDate(r))
  if (!d) return ''
  // Beide Seiten als ganze lokale Tage: Date.UTC über die Tages-
  // komponenten hält die Differenz exakt ganzzahlig - ohne die
  // Rundungsrettung, die ab UTC+12 kippt, und ohne den Mix aus
  // UTC-Mitternacht und Lokal-Mitternacht.
  const now = new Date()
  return (
    (Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) -
      Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())) /
    86400000
  )
  ```

**Validation rules**

Conditions between fields. They must be enforced in one place so that the edit form, the CSV import and anything the AI proposes all pass through the same check.

- **When** `r.result !== 'noch nicht geprüft'` → **Then** `lastTest`, `tester`
  **Message:** „Ein Prüfergebnis ohne Datum und Prüfer ist im Ernstfall wertlos.“
- **When** `r.result === 'bestanden mit Mangel' || r.result === 'nicht bestanden'` → **Then** `defect`
  **Message:** „Zu einem Mangel gehört, worin er besteht.“
- **When** `r.result === 'nicht bestanden'` → **Then** `note`
  **Message:** „Bei „nicht bestanden" gehört in die Notiz, wo das Gerät jetzt ist — es darf nicht weiterlaufen.“

## Dashboard

Tiles over the whole record set, not the filtered view.

- A single number: **Betriebsmittel** — the record count · „im Bestand“
- A single number: **Fällig oder gesperrt** — the record count (only records matching a filter): `isOverdue(r)` · „sofort anfassen“
- A single number: **In den nächsten 60 Tagen** — the record count (only records matching a filter): `{ const d = dueDate(r) return d && d >= iso(0) && d <= iso(60) }` · „Prüftermin planen“
- A ring per value of `result`
- Bars per value of `location`, measuring the record count — **Geräte je Standort**
- Bars per value of `kind`, measuring the record count — **Geräte je Art**

## Guided entry

A short sequence of steps for someone who has to report one thing and does not know the tool. Nothing is written until the last step is confirmed — abandoning it must leave nothing behind.

Title: **Prüfung eintragen**

> Drei Schritte. Für den ersten Aufbau eines Bestands hilft der Schritt „Liste einlesen".

1. **Step 1** — Gerät: fields: `name`, `kind`, `serial`, `location`, `holder`, `interval`
2. **Step 2** — Prüfung: fields: `lastTest`, `tester`, `result`, `defect`, `note`
3. **Step 3** — Liste einlesen: CSV upload, feeding the same run
   shown only when: `Boolean(drafts.records?.name)`
4. **Step 4** — Prüfen: summary generated from the schema

Closing screen: “Eingetragen. Die nächste Fälligkeit rechnet sich von selbst.”

## Defaults

Set these in `DEFAULT_SETTINGS`, `DEFAULT_COLORS` and `DEFAULT_HOME` in `src/app.jsx`.

- Title: **Prüfbuch Betriebsmittel**
- Subtitle: Wiederkehrende Prüfungen nach DGUV Vorschrift 3
- File name: `pruefbuch`
- Version: `1.0`
- Interface language: `de`
- Opens as: the full tool
- Colours: `accent` #a8541f · `band` #2a1d14 · `flag` #b3341f · `ok` #3f7a3f · `pending` #c99411

## Start page

The app opens on this text. It is a small Markdown subset — headings, lists, quotes, bold, italic, inline code and links. Use it verbatim:

```markdown
# Prüfbuch für ortsveränderliche Betriebsmittel

Bohrmaschine, Verlängerung, Kaffeemaschine, Heißluftgebläse: alles muss regelmäßig geprüft werden.
Die Prüfung selbst dauert Minuten. Die Verwaltung frisst den Nachmittag — weil das Protokoll im
Ordner liegt, die Fristen im Kopf des Meisters, und die Berufsgenossenschaft nach einem Unfall
genau eine Frage stellt: **wann wurde dieses Gerät zuletzt geprüft?**

## Was diese Demo zeigt

- **Sortierbare Spalten** — ein Klick auf den Spaltenkopf ordnet: Zahlen nach Größe („Tage"
  reicht von deutlich überfällig bis weit in die Zukunft), Daten chronologisch („Geprüft" spannt
  zwei Jahre), Text alphabetisch; der dritte Klick stellt die Reihenfolge des Datenblocks wieder
  her, leere Werte bleiben unten.
- **Nichts wird summiert, alles gerechnet**: die Fälligkeit ergibt sich aus letzter Prüfung und
  Intervall, die Restzeit daraus, die rote Markierung wieder daraus.
- **Regeln, die dem Ernstfall standhalten** — ein Ergebnis ohne Datum und Prüfer lässt sich nicht
  speichern, ein „nicht bestanden" nicht ohne die Angabe, wo das Gerät jetzt ist.
- **Anhänge** für das Prüfprotokoll am Gerät selbst.

> Erfundene Daten. Veranschaulichung der Struktur, keine Aussage über den Umfang Ihrer Pflichten.
```

## Demo data

Add 13 realistic demo records so the file is not empty on first open. Invent them in the style of the examples above; they are illustration, not the user’s data. Tell the user their own data goes in through **Import CSV → replace all**.

## Done when

- `npm run build` produces one self-contained `dist/index.html`
- `npm test` passes
- the file opens by double-click and shows the demo records
- the calculated fields show values, and the rules refuse a record that violates them
- the settings, colours and start page match the specification above

## Before handing it over

Decide these rather than leaving them to the recipient: set `copyright` to whoever owns the tool, replace the header link that points at the openToolbox repository, and switch `examplePrompts` off if the recipient only enters data. Mention the usage counter (Settings → Security) at handover rather than letting someone find it in a network log.

---

*Generated from `examples/equipment-testing.domain.js` — the working source of the [live demo](https://m-dohmen.github.io/openToolbox/demos/equipment-testing/). Regenerate with `npm run prompts`.*

*All data in the demo is invented. It illustrates the structure of such a tool — it is not legal advice and not proof of anyone’s compliance.*
