---
title: "Logbook to Billing"
slug: "logbook-to-billing"
category: "Patterns"
hosts: "ninox"
---

# Logbook to Billing (Zeiterfassung → Aufwandsberechnung)

## AI Defaults (read first)

- **Zwei getrennte Konzepte:** `TimeEntry` (einzelner Zeiteintrag) und `BillingPeriod` (Abrechnungszeitraum mit Soll-Stunden/Budget). Nicht vermischen.
- **`billingMode` pro Zeiteintrag** entscheidet den Ausgabepfad: `contingent` (zählt auf ein Kontingent), `billable_extra` (wird separat abgerechnet), `non_billable` (zählt nirgends).
- **Zwei Ausgabepfade, kein Automatismus dazwischen:** Stunden-Abrechnung (→ Rechnungsposition) und Budget/Kontingent-Nachweis (→ Aufwandsposition) sind unabhängige Auswertungen auf denselben `TimeEntry`-Daten — nicht ein einziger kombinierter Bericht.
- **Kennzahlen sind reine Aggregationen** (`sum`, `min`, `round`) auf gefilterten `TimeEntry`-Listen — kein separates Aggregat-Objekt in der Datenbank nötig, kann pro Abfrage berechnet werden.
- **Texte sind Config, kein Hardcoding:** Rechnungs- und Aufwandszeilen entstehen aus Platzhalter-Templates (`invoiceLineTemplate`, `effortLineTemplate`), nicht aus fest verdrahteten Sätzen im Formelcode.
- **Kein lauffähiges Space-Beispiel in diesem Dokument** — nur Konzept, Datenmodell und illustrative Formeln. Konkrete Umsetzung ist projektspezifisch.

---

## Problem

Viele Dienstleistungsverträge laufen nach demselben Muster: Mitarbeitende erfassen Zeiteinträge (Logbuch), und am Ende einer Periode muss daraus entweder

1. eine **Rechnungsposition** werden (Stunden × Satz), oder
2. ein **Aufwandsnachweis** ohne Rechnungsbetrag (wie viel von einem Kontingent/Budget wurde verbraucht).

Ohne ein gemeinsames Datenmodell entsteht das für jedes Projekt neu — mit leicht unterschiedlichen Feldnamen und Formeln, die schwer wiederverwendbar sind.

## Datenmodell

### `TimeEntry` (Logbuch-Zeile)

| Feld | Typ | Bedeutung |
|------|-----|-----------|
| `start` / `end` | Datum/Zeit | Beginn/Ende des Zeiteintrags |
| `durationHours` | Zahl (berechnet) | `(end - start) / 3600000` |
| `employeeRef` | Referenz | Mitarbeiter:in |
| `contractRef` | Referenz | Zugehöriger Vertrag/Kunde |
| `locationType` | Enum | `onsite` \| `office` \| `remote` |
| `activityLabel` | Text | Freitext oder Kategorie der Tätigkeit |
| `billingMode` | Enum | `contingent` \| `billable_extra` \| `non_billable` |
| `billedAt` | Datum (optional) | Gesetzt, sobald der Eintrag abgerechnet wurde |

### `BillingPeriod` (Abrechnungszeitraum)

| Feld | Typ | Bedeutung |
|------|-----|-----------|
| `contractRef` | Referenz | Zugehöriger Vertrag/Kunde |
| `periodStart` / `periodEnd` | Datum | Grenzen des Zeitraums (z. B. Kalenderjahr, Quartal, Vertragslaufzeit) |
| `targetHours` | Zahl | Soll-Stunden für den Zeitraum |
| `hourlyRate` | Zahl | Stundensatz |
| `budgetAmount` | Zahl (optional) | Alternativ zu `targetHours`, wenn in Geld statt Stunden budgetiert wird |

## Formeln (illustrativ)

```javascript
let actualHours := sum(
    (select TimeEntry where
        contractRef = period.contractRef
        and billingMode = "contingent"
        and start >= period.periodStart
        and start <= period.periodEnd
    ).durationHours
);

let remainingHours := period.targetHours - actualHours;

let progressPercent := min(100, round(actualHours / period.targetHours * 100));

let billableQueue := (select TimeEntry where
    contractRef = period.contractRef
    and billingMode = "billable_extra"
    and billedAt = null
);
```

## Zwei Ausgabepfade

### 1. Stunden-Abrechnung → Rechnungsposition

Für Zeiteinträge mit `billingMode = "billable_extra"` und `billedAt = null`:

1. `billableQueue` gruppieren (z. B. nach Periode + `activityLabel`)
2. Pro Gruppe: `lineAmount := sum(durationHours) * hourlyRate`
3. Zeilentext aus `invoiceLineTemplate` (siehe Textbaustein-Mechanismus)
4. Nach Übernahme in die Rechnung: `billedAt` auf allen enthaltenen Zeiteinträgen setzen

### 2. Budget/Kontingent → Aufwandsposition

Kein Rechnungsbetrag, sondern ein **Statusnachweis** pro `BillingPeriod`:

- `actualHours`, `targetHours`, `remainingHours`, `progressPercent`
- Zeilentext aus `effortLineTemplate`
- Typische Darstellung: `arcCustomProgressBar` oder `arcCustomKpiBar` für den Fortschritt, `arcCustomTable` für die Liste der Perioden/Verträge

## Textbaustein-Mechanismus

Statt fixer Sätze im Formelcode ein Config-Objekt mit Platzhaltern — der Wortlaut bleibt pro Projekt austauschbar, ohne die Formeln anzufassen:

```javascript
let cfg := {
    labels: { onsite: "Vor Ort", office: "Büro", remote: "Remote" },
    invoiceLineTemplate: "{hours} Std. {activityLabel} ({period}) à {rate} €",
    effortLineTemplate: "{actualHours} von {targetHours} Std. genutzt ({progressPercent} %), Rest {remainingHours} Std."
};
```

Platzhalter werden vor der Ausgabe per einfachem String-Replace aufgelöst (z. B. `replace(cfg.invoiceLineTemplate, "{hours}", text(hours))` verkettet für jeden Platzhalter).

## Beispiele

### Fortschrittsanzeige für ein Kontingent

```javascript
arcCustomProgressBar({
    uniqueId: "contingent-progress-" + period.Nr,
    value: progressPercent,
    label: replace(
        replace(cfg.effortLineTemplate, "{actualHours}", text(actualHours)),
        "{targetHours}", text(period.targetHours)
    )
})
```

### Offene abrechenbare Stunden als Tabelle

```javascript
arcCustomTable({
    uniqueId: "billable-queue-" + contractRef.Nr,
    table: [
        {
            columns: [
                { value: activityLabel },
                { value: text(durationHours) + " Std." },
                { value: text(durationHours * hourlyRate) + " €" }
            ]
        }
        // ... eine Zeile pro Eintrag aus billableQueue, gruppiert
    ]
})
```

## Siehe auch

- [`data-bag-actions`](./data-bag-actions.md) — partielle Updates, falls Filter/Auswahl-Zustand pro Periode zwischengespeichert wird
- `docs/widgets/custom-kpi-bar.md`, `docs/widgets/custom-progress-bar.md` — Fortschritts-/Kennzahlen-Darstellung
- `docs/widgets/custom-table.md` — Listendarstellung für Zeiteinträge/Rechnungspositionen
