<!-- Generated by scripts/build-prompts.mjs — do not edit by hand. -->
# Construis-moi cet outil: Prüfbuch Betriebsmittel

*Tout ce qui suit est la spécification fonctionnelle. Suis-la telle quelle ; là où elle se tait, tranche toi-même et dis ce que tu as supposé.*

## Point de départ

Utilise le modèle **openToolbox** : https://github.com/m-dohmen/openToolbox. Lis d’abord `AGENTS.md` dans ce dépôt — il fait autorité sur la forme du schéma, les types de champs et les règles qui cassent une construction en fichier unique. Tout le métier tient dans un seul fichier, `src/domain.js`.

> Si le skill openToolbox est installé (`claude plugin install opentoolbox@opentoolbox`), il suffit de coller ce fichier — il récupère le modèle lui-même.

## Le problème traité

Chaque atelier, chaque entreprise artisanale, chaque crèche possède des appareils à contrôler à intervalles réguliers : la perceuse, la rallonge, la machine à café, le décapeur thermique. Le contrôle prend quelques minutes. L’administration engloutit l’après-midi, parce que le procès-verbal dort dans un classeur, les échéances dans la tête du chef, et qu’après un accident l’assureur pose exactement une question : **quand cet appareil a-t-il été contrôlé pour la dernière fois ?**

## Ce qui doit exister à la fin

Un seul fichier HTML autonome, ouvert par double-clic, sans serveur ni installation. Le fichier est aussi la base de données : enregistrer écrit un nouveau HTML avec les enregistrements intégrés.

## L’enregistrement

Un enregistrement est **Betriebsmittel**, plusieurs sont **Betriebsmittel**.

**Champs**

| Clé | Libellé | Type | Détail |
| --- | --- | --- | --- |
| `name` | Gerät | text | obligatoire |
| `kind` | Art | enum | l’une de : Handmaschine · Verlängerung/Leitung · Küchengerät · Messgerät · Ladegerät · Ortsfest |
| `serial` | Inventar-/Seriennummer | text | en-tête de tableau `Nr.` |
| `location` | Standort | enum | l’une de : Werkstatt · Baustellenwagen · Lager · Büro · Küche · ausgeliehen, en-tête de tableau `Ort` |
| `holder` | In der Hand von | text | en-tête de tableau `Bei` |
| `interval` | Prüfintervall (Monate) | enum | l’une de : 6 · 12 · 24, en-tête de tableau `Int.` |
| `lastTest` | Letzte Prüfung | date | en-tête de tableau `Geprüft` |
| `tester` | Prüfer | text | — |
| `result` | Ergebnis | enum | l’une de : bestanden · bestanden mit Mangel · nicht bestanden · noch nicht geprüft |
| `defect` | Festgestellter Mangel | text | multiligne, en-tête de tableau `Mangel` |
| `protocol` | Prüfprotokoll | attachment | fichier joint, stocké dans l’enregistrement, en-tête de tableau `Protokoll` |
| `note` | Notiz | text | multiligne |
| `due` | Nächste Prüfung | computed | calculé, jamais stocké, en-tête de tableau `Fällig` |
| `daysLeft` | Tage bis zur Prüfung | computed | calculé, jamais stocké, en-tête de tableau `Tage` |

**Présentation**

- Colonne principale : `name`
- Deuxième ligne en dessous : `location`
- Colonnes du tableau, dans cet ordre : `name`, `kind`, `location`, `lastTest`, `due`, `daysLeft`, `result`
- Filtres de la barre latérale : `kind`, `location`, `result`
- Ne compte plus comme ouvert quand : `() => false`
- Signalé en rouge quand : `{ if (r.result === 'nicht bestanden') return true const d = dueDate(r) return !d || d < iso(0) }`

**Champs calculés**

Ils sont dérivés à chaque affichage et jamais écrits dans l’enregistrement : une dérivation stockée devient fausse dès qu’une de ses sources change.

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

**Règles de validation**

Conditions entre champs. Elles doivent s’appliquer en un seul endroit, afin que le formulaire, l’import CSV et les propositions de l’IA passent par le même contrôle.

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

## Tableau de bord

Tuiles sur l’ensemble des enregistrements, pas sur la vue filtrée.

- Un nombre : **Betriebsmittel** — le nombre d’enregistrements · „im Bestand“
- Un nombre : **Fällig oder gesperrt** — le nombre d’enregistrements (uniquement les enregistrements correspondant à un filtre): `isOverdue(r)` · „sofort anfassen“
- Un nombre : **In den nächsten 60 Tagen** — le nombre d’enregistrements (uniquement les enregistrements correspondant à un filtre): `{ const d = dueDate(r) return d && d >= iso(0) && d <= iso(60) }` · „Prüftermin planen“
- Un anneau par valeur de `result`
- Barres par valeur de `location`, mesurant le nombre d’enregistrements — **Geräte je Standort**
- Barres par valeur de `kind`, mesurant le nombre d’enregistrements — **Geräte je Art**

## Saisie guidée

Une courte séquence d’étapes pour quelqu’un qui doit signaler une chose et ne connaît pas l’outil. Rien n’est écrit avant la validation de la dernière étape : abandonner ne doit rien laisser.

Titre : **Prüfung eintragen**

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

1. **Étape 1** — Gerät: champs : `name`, `kind`, `serial`, `location`, `holder`, `interval`
2. **Étape 2** — Prüfung: champs : `lastTest`, `tester`, `result`, `defect`, `note`
3. **Étape 3** — Liste einlesen: téléversement CSV, alimentant la même session
   uniquement si : `Boolean(drafts.records?.name)`
4. **Étape 4** — Prüfen: récapitulatif généré à partir du schéma

Écran final : « Eingetragen. Die nächste Fälligkeit rechnet sich von selbst. »

## Valeurs par défaut

À définir dans `DEFAULT_SETTINGS`, `DEFAULT_COLORS` et `DEFAULT_HOME` dans `src/app.jsx`.

- Titre: **Prüfbuch Betriebsmittel**
- Sous-titre: Wiederkehrende Prüfungen nach DGUV Vorschrift 3
- Nom de fichier: `pruefbuch`
- Version: `1.0`
- Langue de l’interface: `de`
- S’ouvre en: outil complet
- Couleurs: `accent` #a8541f · `band` #2a1d14 · `flag` #b3341f · `ok` #3f7a3f · `pending` #c99411

## Page d’accueil

L’application s’ouvre sur ce texte. C’est un sous-ensemble de Markdown : titres, listes, citations, gras, italique, code en ligne et liens. À reprendre tel quel :

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

## Données de démonstration

Ajoute 13 enregistrements de démonstration réalistes pour que le fichier ne soit pas vide à l’ouverture. Invente-les dans l’esprit des champs ci-dessus ; ce sont des illustrations, pas les données de l’utilisateur. Dis-lui que ses propres données entrent par **Import CSV → replace all**.

## Terminé quand

- `npm run build` produit un seul `dist/index.html` autonome
- `npm test` passe
- le fichier s’ouvre par double-clic et affiche les enregistrements de démonstration
- les champs calculés affichent des valeurs et les règles refusent un enregistrement fautif
- les réglages, les couleurs et la page d’accueil correspondent à la spécification ci-dessus

## Avant de la livrer

Tranche toi-même plutôt que de laisser le destinataire décider : mets `copyright` au nom du propriétaire de l’outil, remplace le lien d’en-tête pointant vers le dépôt openToolbox, et désactive `examplePrompts` si le destinataire ne fait que saisir des données. Mentionne le compteur d’ouvertures (Réglages → Sécurité) à la livraison.

---

*Généré à partir de `examples/equipment-testing.domain.js`, le code réel de la [démo en ligne](https://m-dohmen.github.io/openToolbox/demos/equipment-testing/). Régénérer avec `npm run prompts`.*

*Toutes les données de la démo sont inventées. Elle illustre la structure d’un tel outil — ce n’est ni un conseil juridique ni une preuve de conformité.*
