Arc Rider Docs · Markdown

Developer Kit · Patterns

Logbook to Billing

Logbook to Billing (Zeiterfassung → Aufwandsberechnung)

AI Defaults
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)

FeldTypBedeutung
start / endDatum/ZeitBeginn/Ende des Zeiteintrags
durationHoursZahl (berechnet)(end - start) / 3600000
employeeRefReferenzMitarbeiter:in
contractRefReferenzZugehöriger Vertrag/Kunde
locationTypeEnumonsite \office \remote
activityLabelTextFreitext oder Kategorie der Tätigkeit
billingModeEnumcontingent \billable_extra \non_billable
billedAtDatum (optional)Gesetzt, sobald der Eintrag abgerechnet wurde

BillingPeriod (Abrechnungszeitraum)

FeldTypBedeutung
contractRefReferenzZugehöriger Vertrag/Kunde
periodStart / periodEndDatumGrenzen des Zeitraums (z. B. Kalenderjahr, Quartal, Vertragslaufzeit)
targetHoursZahlSoll-Stunden für den Zeitraum
hourlyRateZahlStundensatz
budgetAmountZahl (optional)Alternativ zu targetHours, wenn in Geld statt Stunden budgetiert wird

Formeln (illustrativ)

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 = &quot;billable_extra&quot; and billedAt = null );</code></pre>

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:

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

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

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

  • <a href="./data-bag-actions.md"><code>data-bag-actions</code></a> — 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